@lxzy/code-snippet 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.gitignore +7 -0
  3. package/index.html +7 -0
  4. package/package.json +15 -44
  5. package/routes/index.ts +1 -1
  6. package/routes/menu.ts +6 -0
  7. package/src/app-vite.tsx +74 -0
  8. package/src/layouts/index.tsx +6 -24
  9. package/src/main.tsx +21 -9
  10. package/src/pages/activityCenter/activitySystemManagement/add.tsx +1 -1
  11. package/src/pages/activityCenter/activitySystemManagement/addPages/components/ChannelLinkModal.tsx +1 -2
  12. package/src/pages/activityCenter/activitySystemManagement/addPages/components/CustomLink.tsx +1 -2
  13. package/src/pages/activityCenter/activitySystemManagement/addPages/components/CustomPage.tsx +1 -1
  14. package/src/pages/activityCenter/activitySystemManagement/addPages/components/SystemPage.tsx +1 -1
  15. package/src/pages/activityCenter/activitySystemManagement/addPages/index.tsx +1 -1
  16. package/src/pages/activityCenter/activitySystemManagement/index.tsx +2 -1
  17. package/src/pages/home.tsx +8 -4
  18. package/src/pages/index.module.less +5 -0
  19. package/src/pages/ly/ProFormUpload/icons/audio.png +0 -0
  20. package/src/pages/ly/ProFormUpload/icons/excel.png +0 -0
  21. package/src/pages/ly/ProFormUpload/icons/file.png +0 -0
  22. package/src/pages/ly/ProFormUpload/icons/pdf.png +0 -0
  23. package/src/pages/ly/ProFormUpload/icons/ppt.png +0 -0
  24. package/src/pages/ly/ProFormUpload/icons/rar.png +0 -0
  25. package/src/pages/ly/ProFormUpload/icons/txt.png +0 -0
  26. package/src/pages/ly/ProFormUpload/icons/video.png +0 -0
  27. package/src/pages/ly/ProFormUpload/icons/word.png +0 -0
  28. package/src/pages/ly/ProFormUpload/icons/zip.png +0 -0
  29. package/src/pages/ly/ProFormUpload/index.less +158 -0
  30. package/src/pages/ly/ProFormUpload/index.tsx +276 -0
  31. package/src/pages/ly/ProFormUpload/service.ts +79 -0
  32. package/src/pages/style.less +2 -0
  33. package/src/umi/index.ts +2 -2
  34. package/vite.config.ts +55 -0
  35. package/src/App.tsx +0 -74
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(pnpm pub *)",
5
+ "Bash(git add *)",
6
+ "Bash(git commit *)"
7
+ ]
8
+ }
9
+ }
package/.gitignore CHANGED
@@ -9,9 +9,16 @@
9
9
 
10
10
  # production
11
11
  /dist
12
+ /build
12
13
 
13
14
  # misc
14
15
  .DS_Store
15
16
 
16
17
  # vite
17
18
  /vite.config.ts.timestamp-*
19
+
20
+ .omo/run-continuation/
21
+ .omo/plans/
22
+ .omo/notepads/
23
+ .omo/boulder.json
24
+ .codegraph
package/index.html CHANGED
@@ -4,6 +4,13 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Code Snippet</title>
7
+ <link
8
+ rel="stylesheet"
9
+ href="https://g.alicdn.com/code/lib/antd/5.27.2/reset.min.css"
10
+ />
11
+ <script src="https://g.alicdn.com/code/lib/react/18.2.0/umd/react.%MODE%.min.js"></script>
12
+ <script src="https://g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.%MODE%.min.js"></script>
13
+ <script src="https://g.alicdn.com/code/lib/??dayjs/1.11.10/dayjs.min.js,antd/5.27.2/antd.min.js"></script>
7
14
  </head>
8
15
  <body>
9
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lxzy/code-snippet",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "files": [
5
5
  "*",
6
6
  "!dist",
@@ -17,66 +17,37 @@
17
17
  "pub": "npm publish"
18
18
  },
19
19
  "dependencies": {
20
- "@ant-design/icons": "^4.7.0",
21
- "@ant-design/pro-descriptions": "^1.10.0",
22
- "@ant-design/pro-form": "^1.52.0",
23
- "@ant-design/pro-layout": "^6.32.0",
24
- "@ant-design/pro-list": "^1.21.44",
25
- "@ant-design/pro-table": "^2.61.0",
26
- "@wangeditor/editor": "^5.1.23",
27
- "@wangeditor/editor-for-react": "^1.0.6",
20
+ "@ant-design/icons": "^5",
21
+ "@ant-design/pro-components": "^2.6.0",
22
+ "@ice/stark-app": "^1.5.0",
28
23
  "ahooks": "^3.1.9",
29
- "ali-oss": "^6.16.0",
30
- "antd": "^4.22.7",
24
+ "antd": "5.27.2",
31
25
  "axios": "^1.18.1",
32
26
  "classnames": "^2.3.0",
33
- "clipboard": "^2.0.8",
34
- "core-decorators": "^0.20.0",
35
- "crypto-js": "^4.1.1",
36
27
  "dayjs": "^1.11.0",
37
28
  "echarts": "^5.4.3",
38
- "eta": "^1.12.3",
39
- "exceljs": "^4.3.0",
40
- "fs-extra": "^10.0.0",
41
- "got": "^11.8.2",
29
+ "esbuild": "^0.28.1",
42
30
  "history-with-query": "^4.10.4",
43
- "jotai": "^1.6.1",
44
- "lodash": "^4.17.0",
45
31
  "lodash-es": "^4.17.21",
32
+ "mockjs": "^1.1.0",
46
33
  "moment": "^2.29.0",
47
- "number-precision": "^1.5.2",
48
- "omit.js": "^2.0.2",
49
- "rc-menu": "^9.1.0",
50
- "rc-util": "^5.16.0",
51
- "react": "^17.0.0",
52
- "react-avatar-editor": "^13.0.2",
53
- "react-beautiful-dnd": "^13.1.0",
54
- "react-color": "^2.19.3",
55
- "react-dev-inspector": "^1.7.0",
56
- "react-dom": "^17.0.0",
57
- "react-helmet-async": "^1.2.0",
58
- "react-infinite-scroll-component": "^6.1.0",
59
- "react-router-dom": "5",
60
- "swagger-typescript-api": "10.0.2",
34
+ "react": "^18.0.0",
35
+ "react-dom": "^18.0.0",
36
+ "react-router-dom": "^6",
61
37
  "tslib": "^2.8.1",
62
38
  "unstated-next": "^1.1.0",
63
- "validator": "^13.7.0",
64
- "wangeditor": "^4.7.11"
39
+ "validator": "^13.7.0"
65
40
  },
66
41
  "devDependencies": {
67
- "@babel/plugin-proposal-class-properties": "^7.18.6",
68
- "@babel/plugin-proposal-decorators": "^7.29.7",
69
42
  "@types/node": "^22.1.0",
70
- "@types/react": "^17.0.0",
71
- "@types/react-dom": "^17.0.0",
72
- "@types/react-router-dom": "5",
43
+ "@types/react": "^18.0.0",
44
+ "@types/react-dom": "^18.0.0",
73
45
  "@vitejs/plugin-react": "^6.0.0",
74
46
  "less": "^4.6.7",
75
- "lint-staged": "^10.0.7",
76
47
  "prettier": "^2.2.0",
77
48
  "typescript": "^5.0.0",
78
49
  "vite": "^8.1.4",
79
- "vite-plugin-mock": "^3.0.2",
80
- "yorkie": "^2.0.0"
50
+ "vite-plugin-external": "^6.2.2",
51
+ "vite-plugin-mock": "^3.0.2"
81
52
  }
82
53
  }
package/routes/index.ts CHANGED
@@ -4,7 +4,7 @@ const flattenRoutes = (routes) => {
4
4
  const result = [];
5
5
  routes.forEach((route) => {
6
6
  if (route.component) {
7
- result.push({ path: route.path, component: route.component });
7
+ result.push(route);
8
8
  }
9
9
  if (route.routes) {
10
10
  result.push(...flattenRoutes(route.routes));
package/routes/menu.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  export default [
2
+ {
3
+ path: '/',
4
+ component: 'home',
5
+ redirect: '/home',
6
+ },
2
7
  {
3
8
  path: '/home',
4
9
  name: '首页',
@@ -9,6 +14,7 @@ export default [
9
14
  path: '/login',
10
15
  name: '登录',
11
16
  component: 'login',
17
+ hideInMenu: true,
12
18
  layout: false,
13
19
  },
14
20
  {
@@ -0,0 +1,74 @@
1
+ import { Suspense, lazy, useEffect } from 'react';
2
+ import {
3
+ Routes,
4
+ Route,
5
+ Navigate,
6
+ useLocation,
7
+ BrowserRouter,
8
+ } from 'react-router-dom';
9
+
10
+ import history from '@/umi';
11
+ import BasicLayout from '@/layouts/index';
12
+ import { getBasename } from '@ice/stark-app';
13
+ import routes from '../routes';
14
+ import { PageLoading } from '@ant-design/pro-components';
15
+ import { ConfigProvider } from 'antd';
16
+ import moment from 'moment';
17
+ import 'moment/dist/locale/zh-cn';
18
+ moment.locale('zh-cn');
19
+
20
+ const pageModules = import.meta.glob([
21
+ './pages/**/*.tsx',
22
+ '!./pages/**/component/**',
23
+ '!./pages/**/components/**',
24
+ ]);
25
+
26
+ const componentCache = {};
27
+ const loadComponent = (path) => {
28
+ if (!componentCache[path]) {
29
+ const factory: any =
30
+ pageModules[`./pages/${path}.tsx`] ||
31
+ pageModules[`./pages/${path}/index.tsx`];
32
+ componentCache[path] = lazy(factory);
33
+ }
34
+ return componentCache[path];
35
+ };
36
+
37
+ const RouteWrapper = ({ componentPath }) => {
38
+ const location = useLocation();
39
+ const Component = loadComponent(componentPath);
40
+ return <Component history={history} location={location} />;
41
+ };
42
+
43
+ const App = () => {
44
+ return (
45
+ <ConfigProvider>
46
+ <BrowserRouter basename={getBasename()}>
47
+ <BasicLayout>
48
+ <Suspense fallback={<PageLoading />}>
49
+ <Routes>
50
+ {routes.map((route, i) =>
51
+ route.redirect ? (
52
+ <Route
53
+ key={route.path || i}
54
+ path={route.path}
55
+ element={<Navigate to={route.redirect} replace />}
56
+ />
57
+ ) : (
58
+ <Route
59
+ key={route.path || i}
60
+ path={route.path}
61
+ element={<RouteWrapper componentPath={route.component} />}
62
+ />
63
+ ),
64
+ )}
65
+ <Route path="*" element={<Navigate to="/404" replace />} />
66
+ </Routes>
67
+ </Suspense>
68
+ </BasicLayout>
69
+ </BrowserRouter>
70
+ </ConfigProvider>
71
+ );
72
+ };
73
+
74
+ export default App;
@@ -1,8 +1,9 @@
1
- import ProLayout from '@ant-design/pro-layout';
1
+ import { ProLayout } from '@ant-design/pro-components';
2
2
  import history from '@/umi';
3
3
  import menuRoutes from '../../routes/menu';
4
4
  import getIcon from '@/utils/getIcon';
5
- import { useLocation } from 'react-router-dom';
5
+ import { useLocation, Link } from 'react-router-dom';
6
+ import { isInIcestark } from '@ice/stark-app';
6
7
 
7
8
  const menuDataRender = (menuList) => {
8
9
  return menuList.map((item) => {
@@ -19,38 +20,19 @@ const BasicLayout = (props) => {
19
20
  const { children } = props;
20
21
  const location = useLocation();
21
22
 
22
- const isLayoutFalse = (routes, pathname) => {
23
- for (const route of routes) {
24
- if (pathname === route.path || pathname.startsWith(route.path + '/')) {
25
- if (route.layout === false) return true;
26
- }
27
- if (route.routes) {
28
- if (isLayoutFalse(route.routes, pathname)) return true;
29
- }
30
- }
31
- return false;
32
- };
33
-
34
- if (isLayoutFalse(menuRoutes, location.pathname)) {
35
- return <>{children}</>;
36
- }
37
-
38
23
  return (
39
24
  <ProLayout
40
25
  title="管理平台"
41
- // pure
26
+ pure={isInIcestark()}
42
27
  logo={false}
43
28
  layout="mix"
44
- fixSiderbar={false}
45
- fixedHeader
46
29
  location={location}
47
30
  route={{
48
31
  path: '/',
49
32
  routes: menuDataRender(menuRoutes),
50
33
  }}
51
- menuItemRender={(item, dom) => (
52
- <a onClick={() => history.push(item.path || '/')}>{dom}</a>
53
- )}
34
+ menuItemRender={(item, dom) => <Link to={item.path}>{dom}</Link>}
35
+ locale="zh-CN"
54
36
  >
55
37
  {children}
56
38
  </ProLayout>
package/src/main.tsx CHANGED
@@ -1,12 +1,24 @@
1
1
  import React from 'react';
2
- import ReactDOM from 'react-dom';
3
- import App from './App';
4
- import 'antd/es/notification/style';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './app-vite';
5
4
  import './global.css';
5
+ import { setLibraryName, isInIcestark } from '@ice/stark-app';
6
6
 
7
- ReactDOM.render(
8
- <React.StrictMode>
9
- <App />
10
- </React.StrictMode>,
11
- document.getElementById('root'),
12
- );
7
+ let root;
8
+
9
+ if (!isInIcestark()) {
10
+ root = createRoot(document.getElementById('root'));
11
+ root.render(<App />);
12
+ }
13
+
14
+ export function mount(props) {
15
+ root = createRoot(props.container);
16
+ root.render(<App />);
17
+ }
18
+
19
+ export function unmount(props) {
20
+ root?.unmount();
21
+ }
22
+
23
+ // 注意:`setLibraryName` 的入参需要与工程配置的 output.library 保持一致
24
+ setLibraryName('CodeSnippet');
@@ -8,7 +8,7 @@ import {
8
8
  ProFormRadio,
9
9
  ProFormDependency,
10
10
  ProFormDateTimeRangePicker,
11
- } from '@ant-design/pro-form';
11
+ } from '@ant-design/pro-components';
12
12
  import moment from 'moment';
13
13
  import axios from 'axios';
14
14
 
@@ -1,8 +1,7 @@
1
1
  import React, { useRef, useState } from 'react';
2
2
  import { Modal, Button, Input, message } from 'antd';
3
3
  import { PlusOutlined } from '@ant-design/icons';
4
- import ProTable from '@ant-design/pro-table';
5
- import { ModalForm, ProFormText } from '@ant-design/pro-form';
4
+ import { ProTable, ModalForm, ProFormText } from '@ant-design/pro-components';
6
5
  import axios from 'axios';
7
6
 
8
7
  const ChannelLinkModal = (props) => {
@@ -1,8 +1,7 @@
1
1
  import React, { useRef, useState } from 'react';
2
2
  import { Button, Modal, Radio, Input, message, Space } from 'antd';
3
3
  import { PlusOutlined } from '@ant-design/icons';
4
- import ProTable from '@ant-design/pro-table';
5
- import { ModalForm } from '@ant-design/pro-form';
4
+ import { ProTable, ModalForm } from '@ant-design/pro-components';
6
5
  import axios from 'axios';
7
6
 
8
7
  const CustomLink = () => {
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Button } from 'antd';
3
- import ProTable from '@ant-design/pro-table';
3
+ import { ProTable } from '@ant-design/pro-components';
4
4
  import axios from 'axios';
5
5
  import ChannelLinkModal from './ChannelLinkModal';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Button } from 'antd';
3
- import ProTable from '@ant-design/pro-table';
3
+ import { ProTable } from '@ant-design/pro-components';
4
4
  import axios from 'axios';
5
5
 
6
6
  const SystemPage = () => {
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useState } from 'react';
2
- import { PageContainer } from '@ant-design/pro-layout';
2
+ import { PageContainer } from '@ant-design/pro-components';
3
3
  import SystemPage from './components/SystemPage';
4
4
  import CustomPage from './components/CustomPage';
5
5
  import CustomLink from './components/CustomLink';
@@ -8,7 +8,8 @@ import {
8
8
  Select,
9
9
  Space,
10
10
  } from 'antd';
11
- import ProTable, { ActionType } from '@ant-design/pro-table';
11
+ import { ProTable } from '@ant-design/pro-components';
12
+ import type { ActionType } from '@ant-design/pro-components';
12
13
  import axios from 'axios';
13
14
  import AddDrawer from './add';
14
15
  import history from '@/umi';
@@ -1,7 +1,11 @@
1
- import React, { useState } from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
+ import styles from './index.module.less';
3
+ import { DatePicker } from 'antd';
2
4
 
3
5
  export default function Home(props) {
4
- console.log(props);
5
- const { history, location } = props;
6
- return <div>home</div>;
6
+ return (
7
+ <div>
8
+ <DatePicker></DatePicker>
9
+ </div>
10
+ );
7
11
  }
@@ -0,0 +1,5 @@
1
+ a {
2
+ }
3
+
4
+ b {
5
+ }
@@ -0,0 +1,158 @@
1
+ // 包裹容器:卡片模式下自定义卡片因多了文件名而高于 antd 默认项高,
2
+ // 需把列表项容器高度改为自适应,否则换行后会与下一行重叠。
3
+ // antd v5 用 cssinjs 运行时注入样式,与本地选择器特异性相同时会覆盖本地样式,故用 !important 提升优先级。
4
+ .wrapper {
5
+ :global {
6
+ .ant-upload-list-picture-card {
7
+ align-items: flex-start;
8
+ }
9
+
10
+ .ant-upload-list-picture-card .ant-upload-list-item-container {
11
+ height: auto !important;
12
+ }
13
+ }
14
+ }
15
+
16
+ .card {
17
+ position: relative;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ width: 104px;
22
+ }
23
+
24
+ .thumb {
25
+ position: relative;
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ width: 104px;
30
+ height: 104px;
31
+ padding: 8px;
32
+ overflow: hidden;
33
+ background-color: #fafafa;
34
+ border: 1px solid #d9d9d9;
35
+ border-radius: 8px;
36
+ cursor: pointer;
37
+
38
+ img {
39
+ max-width: 100%;
40
+ max-height: 100%;
41
+ object-fit: contain;
42
+ }
43
+
44
+ video {
45
+ max-width: 100%;
46
+ max-height: 100%;
47
+ object-fit: contain;
48
+ }
49
+ }
50
+
51
+ .mask {
52
+ position: absolute;
53
+ inset: 0;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ gap: 12px;
58
+ color: #fff;
59
+ font-size: 16px;
60
+ background-color: rgba(0, 0, 0, 0.45);
61
+ opacity: 0;
62
+ transition: opacity 0.2s;
63
+
64
+ span {
65
+ cursor: pointer;
66
+
67
+ &:hover {
68
+ color: #40a9ff;
69
+ }
70
+ }
71
+ }
72
+
73
+ .thumb:hover .mask {
74
+ opacity: 1;
75
+ }
76
+
77
+ .name {
78
+ width: 104px;
79
+ margin-top: 4px;
80
+ overflow: hidden;
81
+ color: rgba(0, 0, 0, 0.65);
82
+ font-size: 12px;
83
+ line-height: 18px;
84
+ white-space: nowrap;
85
+ text-overflow: ellipsis;
86
+ text-align: center;
87
+ }
88
+
89
+ .error {
90
+ color: #ff4d4f;
91
+ }
92
+
93
+ // 列表模式(picture / text)
94
+ .listItem {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ padding: 4px 8px;
99
+ border-radius: 6px;
100
+
101
+ &:hover {
102
+ background-color: #f5f5f5;
103
+
104
+ .listActions {
105
+ opacity: 1;
106
+ }
107
+ }
108
+ }
109
+
110
+ .listThumb {
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ flex: none;
115
+ width: 40px;
116
+ height: 40px;
117
+ overflow: hidden;
118
+ cursor: pointer;
119
+
120
+ img,
121
+ video {
122
+ max-width: 100%;
123
+ max-height: 100%;
124
+ object-fit: contain;
125
+ }
126
+ }
127
+
128
+ .listName {
129
+ flex: auto;
130
+ overflow: hidden;
131
+ color: rgba(0, 0, 0, 0.65);
132
+ font-size: 14px;
133
+ white-space: nowrap;
134
+ text-overflow: ellipsis;
135
+ cursor: pointer;
136
+
137
+ &:hover {
138
+ color: #40a9ff;
139
+ }
140
+ }
141
+
142
+ .listActions {
143
+ display: flex;
144
+ flex: none;
145
+ gap: 12px;
146
+ color: rgba(0, 0, 0, 0.45);
147
+ font-size: 14px;
148
+ opacity: 0;
149
+ transition: opacity 0.2s;
150
+
151
+ span {
152
+ cursor: pointer;
153
+
154
+ &:hover {
155
+ color: #40a9ff;
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,276 @@
1
+ import {
2
+ ProFormUploadButton,
3
+ type ProFormUploadButtonProps,
4
+ } from "@ant-design/pro-components";
5
+ import { DeleteOutlined, EyeOutlined, LoadingOutlined } from "@ant-design/icons";
6
+ import { Image, Modal, Tooltip, type UploadFile, type UploadProps } from "antd";
7
+ import React, { useState } from "react";
8
+ import styles from "./index.less";
9
+ import { mockUpload } from "./service";
10
+ import pdfIcon from "./icons/pdf.png";
11
+ import wordIcon from "./icons/word.png";
12
+ import excelIcon from "./icons/excel.png";
13
+ import pptIcon from "./icons/ppt.png";
14
+ import txtIcon from "./icons/txt.png";
15
+ import zipIcon from "./icons/zip.png";
16
+ import rarIcon from "./icons/rar.png";
17
+ import audioIcon from "./icons/audio.png";
18
+ import videoIcon from "./icons/video.png";
19
+ import fileIcon from "./icons/file.png";
20
+
21
+ /**
22
+ * ProFormUploadButton 二次封装
23
+ * 能力:
24
+ * 1. 完全透传 ProFormUploadButton 的所有属性与方法;
25
+ * 2. 允许上传所有类型文件;图片点击放大预览,视频点击弹窗播放;
26
+ * 3. 常见文档类型按后缀展示不同的本地图标,文件名过长时悬停显示全名;
27
+ * 4. 默认卡片(picture-card)展示,可通过 listType 切换其它模式。
28
+ */
29
+
30
+ // 文件类型图标(本地资源,来源 icons8)
31
+ const FILE_TYPE_ICON: Record<string, string> = {
32
+ pdf: pdfIcon,
33
+ doc: wordIcon,
34
+ docx: wordIcon,
35
+ xls: excelIcon,
36
+ xlsx: excelIcon,
37
+ ppt: pptIcon,
38
+ pptx: pptIcon,
39
+ txt: txtIcon,
40
+ zip: zipIcon,
41
+ rar: rarIcon,
42
+ "7z": zipIcon,
43
+ mp3: audioIcon,
44
+ wav: audioIcon,
45
+ video: videoIcon,
46
+ unknown: fileIcon,
47
+ };
48
+
49
+ const getExt = (file: UploadFile) => {
50
+ const target = file.name || file.url || "";
51
+ const match = /\.([a-z0-9]+)(?:\?.*)?$/i.exec(target);
52
+ return match ? match[1].toLowerCase() : "";
53
+ };
54
+
55
+ const isVideoFile = (file: UploadFile) =>
56
+ file.type?.startsWith("video") ||
57
+ /\.(mp4|webm|ogg|mov|m4v)$/i.test(file.name || file.url || "");
58
+
59
+ const isImageFile = (file: UploadFile) =>
60
+ !!file.thumbUrl ||
61
+ file.type?.startsWith("image") ||
62
+ /\.(png|jpe?g|gif|svg|webp|bmp)$/i.test(file.name || file.url || "");
63
+
64
+ // 缓存本地文件生成的 blob 地址,避免重复创建
65
+ const blobUrlCache = new WeakMap<object, string>();
66
+ const getBlobUrl = (file: UploadFile) => {
67
+ const origin = file.originFileObj as unknown as File | undefined;
68
+ if (!origin) return "";
69
+ let url = blobUrlCache.get(origin);
70
+ if (!url) {
71
+ url = URL.createObjectURL(origin);
72
+ blobUrlCache.set(origin, url);
73
+ }
74
+ return url;
75
+ };
76
+
77
+ // 获取可预览/播放的地址:优先后端 url,其次缩略图,最后本地选择的文件
78
+ const getFileUrl = (file: UploadFile) =>
79
+ file.url || file.thumbUrl || getBlobUrl(file);
80
+
81
+ // 根据文件类型返回缩略图节点(图片/视频/文档图标)
82
+ const renderThumb = (file: UploadFile) => {
83
+ if (isVideoFile(file)) {
84
+ // 视频直接用 video 元素占位(展示首帧)
85
+ return <video src={getFileUrl(file)} muted preload="metadata" />;
86
+ }
87
+ if (isImageFile(file)) {
88
+ return <img src={getFileUrl(file)} alt={file.name} />;
89
+ }
90
+ return (
91
+ <img
92
+ src={FILE_TYPE_ICON[getExt(file)] || FILE_TYPE_ICON.unknown}
93
+ alt={file.name}
94
+ />
95
+ );
96
+ };
97
+
98
+ const ProFormUpload: React.FC<ProFormUploadButtonProps> = (props) => {
99
+ const { fieldProps, listType, ...rest } = props;
100
+ const finalListType = listType ?? fieldProps?.listType ?? "picture-card";
101
+
102
+ // 图片放大预览
103
+ const [imgPreview, setImgPreview] = useState<{ visible: boolean; url: string }>(
104
+ { visible: false, url: "" }
105
+ );
106
+ // 视频弹窗播放
107
+ const [videoPreview, setVideoPreview] = useState<{
108
+ open: boolean;
109
+ url: string;
110
+ title: string;
111
+ }>({ open: false, url: "", title: "" });
112
+
113
+ // 默认预览逻辑:图片放大、视频弹窗、其它文档新窗口打开
114
+ const defaultPreview: UploadProps["onPreview"] = (file) => {
115
+ const url = getFileUrl(file);
116
+ if (!url) return;
117
+ if (isImageFile(file)) {
118
+ setImgPreview({ visible: true, url });
119
+ } else if (isVideoFile(file)) {
120
+ setVideoPreview({ open: true, url, title: file.name });
121
+ } else {
122
+ window.open(url, "_blank");
123
+ }
124
+ };
125
+
126
+ const handlePreview: UploadProps["onPreview"] = (file) => {
127
+ if (fieldProps?.onPreview) {
128
+ fieldProps.onPreview(file);
129
+ return;
130
+ }
131
+ defaultPreview(file);
132
+ };
133
+
134
+ // 默认上传:form-data 单文件逐个上传(选一次传一次),当前为模拟实现
135
+ const defaultCustomRequest: NonNullable<
136
+ UploadProps["customRequest"]
137
+ > = async ({ file, onSuccess, onError, onProgress }) => {
138
+ try {
139
+ const result = await mockUpload(file as File, (percent) =>
140
+ onProgress?.({ percent })
141
+ );
142
+ // 把上传结果透传给 antd,onSuccess 的响应会写入 file.response
143
+ onSuccess?.(result);
144
+ } catch (err) {
145
+ onError?.(err as Error);
146
+ }
147
+ };
148
+
149
+ // 卡片模式下的自定义渲染:缩略图 + 悬停操作 + 文件名 Tooltip
150
+ const defaultItemRender: UploadProps["itemRender"] = (
151
+ _originNode,
152
+ file,
153
+ _fileList,
154
+ actions
155
+ ) => {
156
+ // 非卡片模式(picture / text):自定义行布局,缩略图同样按类型展示
157
+ if (finalListType !== "picture-card") {
158
+ const uploading = file.status === "uploading";
159
+ return (
160
+ <div className={styles.listItem}>
161
+ <div
162
+ className={styles.listThumb}
163
+ onClick={() => !uploading && handlePreview(file)}
164
+ >
165
+ {uploading ? <LoadingOutlined /> : renderThumb(file)}
166
+ </div>
167
+ <Tooltip title={file.name}>
168
+ <span
169
+ className={
170
+ file.status === "error"
171
+ ? `${styles.listName} ${styles.error}`
172
+ : styles.listName
173
+ }
174
+ onClick={() => !uploading && handlePreview(file)}
175
+ >
176
+ {file.name}
177
+ </span>
178
+ </Tooltip>
179
+ <span className={styles.listActions}>
180
+ <EyeOutlined
181
+ onClick={(e) => {
182
+ e.stopPropagation();
183
+ handlePreview(file);
184
+ }}
185
+ />
186
+ <DeleteOutlined
187
+ onClick={(e) => {
188
+ e.stopPropagation();
189
+ actions.remove();
190
+ }}
191
+ />
192
+ </span>
193
+ </div>
194
+ );
195
+ }
196
+ const uploading = file.status === "uploading";
197
+ return (
198
+ <div className={styles.card}>
199
+ <div className={styles.thumb} onClick={() => !uploading && handlePreview(file)}>
200
+ {uploading ? (
201
+ <LoadingOutlined style={{ fontSize: 24 }} />
202
+ ) : (
203
+ renderThumb(file)
204
+ )}
205
+ {!uploading && (
206
+ <div className={styles.mask}>
207
+ <EyeOutlined
208
+ onClick={(e) => {
209
+ e.stopPropagation();
210
+ handlePreview(file);
211
+ }}
212
+ />
213
+ <DeleteOutlined
214
+ onClick={(e) => {
215
+ e.stopPropagation();
216
+ actions.remove();
217
+ }}
218
+ />
219
+ </div>
220
+ )}
221
+ </div>
222
+ <Tooltip title={file.name}>
223
+ <div
224
+ className={file.status === "error" ? `${styles.name} ${styles.error}` : styles.name}
225
+ >
226
+ {file.name}
227
+ </div>
228
+ </Tooltip>
229
+ </div>
230
+ );
231
+ };
232
+
233
+ return (
234
+ <div className={styles.wrapper}>
235
+ <ProFormUploadButton
236
+ {...rest}
237
+ listType={finalListType}
238
+ fieldProps={{
239
+ multiple: true,
240
+ // 演示默认放开所有类型,可被外部 accept 覆盖
241
+ accept: "*",
242
+ ...fieldProps,
243
+ onPreview: handlePreview,
244
+ customRequest: fieldProps?.customRequest ?? defaultCustomRequest,
245
+ itemRender: fieldProps?.itemRender ?? defaultItemRender,
246
+ }}
247
+ />
248
+
249
+ {/* 图片放大预览(antd Image 内置缩放/旋转) */}
250
+ <Image
251
+ style={{ display: "none" }}
252
+ src={imgPreview.url}
253
+ preview={{
254
+ visible: imgPreview.visible,
255
+ src: imgPreview.url,
256
+ onVisibleChange: (visible) =>
257
+ setImgPreview((prev) => ({ ...prev, visible })),
258
+ }}
259
+ />
260
+
261
+ {/* 视频在线播放弹窗 */}
262
+ <Modal
263
+ open={videoPreview.open}
264
+ title={videoPreview.title}
265
+ footer={null}
266
+ width={720}
267
+ destroyOnClose
268
+ onCancel={() => setVideoPreview((prev) => ({ ...prev, open: false }))}
269
+ >
270
+ <video src={videoPreview.url} controls autoPlay style={{ width: "100%" }} />
271
+ </Modal>
272
+ </div>
273
+ );
274
+ };
275
+
276
+ export default ProFormUpload;
@@ -0,0 +1,79 @@
1
+ import type { UploadFile } from "antd";
2
+
3
+ /**
4
+ * ProFormUpload 默认上传实现
5
+ *
6
+ * 说明:
7
+ * - 采用 form-data(multipart/form-data)方式,单文件逐个上传(选一次传一次);
8
+ * - 当前为「模拟实现」,不发真实请求:随机进度 + 延时后返回一个可访问地址;
9
+ * - 后续接真实接口时,只需把 mockUpload 里的模拟逻辑替换为下方注释的 UploadFile 调用即可,
10
+ * 组件层无需改动。
11
+ */
12
+
13
+ export interface UploadResult {
14
+ /** 上传成功后可访问/预览的地址 */
15
+ url: string;
16
+ /** 后端返回的文件名(可选) */
17
+ name?: string;
18
+ /** 原始响应,便于业务侧扩展使用 */
19
+ raw?: any;
20
+ }
21
+
22
+ /** 上传接口地址(后续替换为真实地址) */
23
+ export const UPLOAD_URL = "/api/upload";
24
+
25
+ /**
26
+ * 模拟上传:构造 form-data,模拟上传进度与耗时,最终返回一个可预览地址。
27
+ * @param file 待上传文件
28
+ * @param onProgress 进度回调(0-100)
29
+ */
30
+ export async function mockUpload(
31
+ file: File,
32
+ onProgress?: (percent: number) => void
33
+ ): Promise<UploadResult> {
34
+ // 与真实上传保持一致:字段以 form-data 组装
35
+ const formData = new FormData();
36
+ formData.append("file", file);
37
+
38
+ // —— 模拟进度 ——
39
+ await new Promise<void>((resolve) => {
40
+ let percent = 0;
41
+ const timer = setInterval(() => {
42
+ percent += Math.random() * 30;
43
+ if (percent >= 100) {
44
+ percent = 100;
45
+ onProgress?.(percent);
46
+ clearInterval(timer);
47
+ resolve();
48
+ } else {
49
+ onProgress?.(Math.floor(percent));
50
+ }
51
+ }, 200);
52
+ });
53
+
54
+ // —— 模拟返回:本地 blob 地址,图片/视频可直接预览 ——
55
+ const url = URL.createObjectURL(file);
56
+ return { url, name: file.name };
57
+
58
+ /**
59
+ * TODO: 接真实接口时,删除上面的模拟逻辑,改为如下写法:
60
+ *
61
+ * import { UploadFile as uploadRequest } from "@/utils/request";
62
+ * const res = await uploadRequest(UPLOAD_URL, formData);
63
+ * // 假设后端返回 { code: 200, data: { url, name } }
64
+ * if (res.code !== 200) throw new Error(res.msg || "上传失败");
65
+ * return { url: res.data.url, name: res.data.name, raw: res };
66
+ */
67
+ }
68
+
69
+ /** 上传成功后,把结果回填到 UploadFile,供列表展示/表单取值 */
70
+ export const fillUploadedFile = (
71
+ file: UploadFile,
72
+ result: UploadResult
73
+ ): UploadFile => ({
74
+ ...file,
75
+ status: "done",
76
+ url: result.url,
77
+ name: result.name ?? file.name,
78
+ response: result.raw ?? result,
79
+ });
@@ -0,0 +1,2 @@
1
+ b {
2
+ }
package/src/umi/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { createHashHistory } from 'history-with-query';
1
+ import { createBrowserHistory } from 'history-with-query';
2
2
  import { useModel, ModelProvider } from './useModel';
3
3
 
4
- const history = createHashHistory();
4
+ const history = createBrowserHistory();
5
5
 
6
6
  export { history, useModel, ModelProvider };
7
7
  export default history;
package/vite.config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineConfig } from 'vite';
2
2
  import react from '@vitejs/plugin-react';
3
3
  import { viteMockServe } from 'vite-plugin-mock';
4
+ import pluginExternal from 'vite-plugin-external';
4
5
  import path from 'path';
5
6
 
6
7
  export default defineConfig(({ command }) => ({
@@ -16,6 +17,13 @@ export default defineConfig(({ command }) => ({
16
17
  mockPath: 'mock',
17
18
  enable: command === 'serve',
18
19
  }),
20
+ pluginExternal({
21
+ externals: {
22
+ react: 'React',
23
+ 'react-dom': 'ReactDOM',
24
+ antd: 'antd',
25
+ },
26
+ }),
19
27
  ],
20
28
  resolve: {
21
29
  alias: {
@@ -36,6 +44,53 @@ export default defineConfig(({ command }) => ({
36
44
  },
37
45
  },
38
46
  },
47
+ build: {
48
+ outDir: 'build',
49
+ lib: {
50
+ entry: path.resolve(__dirname, 'src/main.tsx'),
51
+ name: 'CodeSnippet',
52
+ formats: ['iife'],
53
+ // fileName: (format) => `code-snippet.${format}.js`,
54
+ },
55
+ // rollupOptions: {
56
+ // external: [
57
+ // 'react',
58
+ // 'react-dom',
59
+ // 'antd',
60
+ // // 'moment',
61
+ // // 'react-router-dom',
62
+ // // '@ant-design/pro-layout',
63
+ // // '@ant-design/pro-table',
64
+ // // '@ant-design/pro-form',
65
+ // // '@ant-design/pro-descriptions',
66
+ // // '@ant-design/pro-list',
67
+ // // '@ant-design/icons',
68
+ // // '@ice/stark-app',
69
+ // // 'ahooks',
70
+ // // 'axios',
71
+ // ],
72
+ // output: {
73
+ // globals: {
74
+ // react: 'React',
75
+ // 'react-dom': 'ReactDOM',
76
+ // antd: 'antd',
77
+ // // moment: 'moment',
78
+ // // 'react-router-dom': 'ReactRouterDOM',
79
+ // // '@ant-design/pro-layout': 'AntDesignProLayout',
80
+ // // '@ant-design/pro-table': 'AntDesignProTable',
81
+ // // '@ant-design/pro-form': 'AntDesignProForm',
82
+ // // '@ant-design/pro-descriptions': 'AntDesignProDescriptions',
83
+ // // '@ant-design/pro-list': 'AntDesignProList',
84
+ // // '@ant-design/icons': 'AntDesignIcons',
85
+ // // '@ice/stark-app': 'IceStarkApp',
86
+ // // ahooks: 'ahooks',
87
+ // // axios: 'axios',
88
+ // // classnames: 'classNames',
89
+ // // dayjs: 'dayjs',
90
+ // },
91
+ // },
92
+ // },
93
+ },
39
94
  server: {
40
95
  port: 8000,
41
96
  open: true,
package/src/App.tsx DELETED
@@ -1,74 +0,0 @@
1
- import React, { Suspense, lazy } from 'react';
2
- import { Switch, Route, Redirect, useHistory, useLocation } from 'react-router-dom';
3
- import { Router } from 'react-router-dom';
4
- import history from '@/umi';
5
- import BasicLayout from '@/layouts/index';
6
- import routes from '../routes';
7
-
8
- const pageModules = import.meta.glob('./pages/**/*.tsx');
9
-
10
- const componentCache = {};
11
- const loadComponent = (path) => {
12
- if (!componentCache[path]) {
13
- const factory: any =
14
- pageModules[`./pages/${path}.tsx`] ||
15
- pageModules[`./pages/${path}/index.tsx`];
16
- if (!factory) {
17
- throw new Error(`Page component not found: ${path}`);
18
- }
19
- componentCache[path] = lazy(factory);
20
- }
21
- return componentCache[path];
22
- };
23
-
24
- const parseQuery = (search) => {
25
- if (!search) return {};
26
- const params = new URLSearchParams(search);
27
- const query = {};
28
- for (const [key, value] of params) {
29
- query[key] = value;
30
- }
31
- return query;
32
- };
33
-
34
- const RouteWrapper = ({ componentPath }) => {
35
- const routeHistory = useHistory();
36
- const routeLocation = useLocation();
37
- const Component = loadComponent(componentPath);
38
- return (
39
- <Component
40
- history={routeHistory}
41
- location={{ ...routeLocation, query: parseQuery(routeLocation.search) }}
42
- />
43
- );
44
- };
45
-
46
- const App = () => {
47
- return (
48
- <Router history={history}>
49
- <BasicLayout>
50
- <Suspense fallback={null}>
51
- <Switch>
52
- {routes.map((route, i) =>
53
- route.redirect ? (
54
- <Route key={route.path || i} path={route.path} exact>
55
- <Redirect to={route.redirect} />
56
- </Route>
57
- ) : (
58
- <Route
59
- key={route.path || i}
60
- path={route.path}
61
- exact={route.path !== '/404'}
62
- render={() => <RouteWrapper componentPath={route.component} />}
63
- />
64
- ),
65
- )}
66
- <Redirect to="/404" />
67
- </Switch>
68
- </Suspense>
69
- </BasicLayout>
70
- </Router>
71
- );
72
- };
73
-
74
- export default App;