@lxzy/code-snippet 0.0.15 → 0.0.16
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.
- package/package.json +3 -3
- package/src/app-vite.tsx +18 -18
- package/src/components/ErrorBoundary/index.tsx +66 -0
- package/src/layouts/index.tsx +2 -0
- package/src/pages/home.tsx +3 -1
- package/vite.config.ts +2 -11
- package/.claude/settings.local.json +0 -9
- package/src/pages/ly/ProFormUpload/icons/audio.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/excel.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/file.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/pdf.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/ppt.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/rar.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/txt.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/video.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/word.png +0 -0
- package/src/pages/ly/ProFormUpload/icons/zip.png +0 -0
- package/src/pages/ly/ProFormUpload/index.less +0 -158
- package/src/pages/ly/ProFormUpload/index.tsx +0 -276
- package/src/pages/ly/ProFormUpload/service.ts +0 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lxzy/code-snippet",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"files": [
|
|
5
5
|
"*",
|
|
6
6
|
"!dist",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"@types/node": "^22.1.0",
|
|
43
43
|
"@types/react": "^18.0.0",
|
|
44
44
|
"@types/react-dom": "^18.0.0",
|
|
45
|
-
"@vitejs/plugin-react": "^
|
|
45
|
+
"@vitejs/plugin-react-swc": "^3",
|
|
46
46
|
"less": "^4.6.7",
|
|
47
47
|
"prettier": "^2.2.0",
|
|
48
48
|
"typescript": "^5.0.0",
|
|
49
|
-
"vite": "^
|
|
49
|
+
"vite": "^6.0.0",
|
|
50
50
|
"vite-plugin-external": "^6.2.2",
|
|
51
51
|
"vite-plugin-mock": "^3.0.2"
|
|
52
52
|
}
|
package/src/app-vite.tsx
CHANGED
|
@@ -47,24 +47,24 @@ const App = () => {
|
|
|
47
47
|
<BasicLayout>
|
|
48
48
|
<Suspense fallback={<PageLoading />}>
|
|
49
49
|
<Routes>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
68
|
</BasicLayout>
|
|
69
69
|
</BrowserRouter>
|
|
70
70
|
</ConfigProvider>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button, Result } from 'antd';
|
|
3
|
+
|
|
4
|
+
class ErrorBoundary extends React.Component {
|
|
5
|
+
state = { hasError: false, isChunkError: false, errorMessage: '' };
|
|
6
|
+
|
|
7
|
+
static getDerivedStateFromError(error) {
|
|
8
|
+
const isChunkError =
|
|
9
|
+
error?.name === 'ChunkLoadError' ||
|
|
10
|
+
error?.message?.includes('Loading chunk') ||
|
|
11
|
+
error?.message?.includes('Failed to fetch dynamically imported module');
|
|
12
|
+
|
|
13
|
+
return { hasError: true, isChunkError, errorMessage: error.message };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
componentDidCatch(error, errorInfo) {
|
|
17
|
+
console.error('ErrorBoundary caught:', error, errorInfo);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
handleRefresh = () => {
|
|
21
|
+
window.location.reload();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
handleRetry = () => {
|
|
25
|
+
this.setState({ hasError: false, isChunkError: false });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
if (this.state.hasError) {
|
|
30
|
+
if (this.state.isChunkError) {
|
|
31
|
+
return (
|
|
32
|
+
<Result
|
|
33
|
+
status="warning"
|
|
34
|
+
title="资源加载失败"
|
|
35
|
+
subTitle="页面资源加载异常,可能是网络问题或版本更新导致"
|
|
36
|
+
extra={[
|
|
37
|
+
<Button type="primary" key="refresh" onClick={this.handleRefresh}>
|
|
38
|
+
刷新浏览器
|
|
39
|
+
</Button>,
|
|
40
|
+
<Button key="retry" onClick={this.handleRetry}>
|
|
41
|
+
重试
|
|
42
|
+
</Button>,
|
|
43
|
+
]}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Result
|
|
50
|
+
status="error"
|
|
51
|
+
title="页面出错了"
|
|
52
|
+
subTitle={this.state.errorMessage}
|
|
53
|
+
extra={
|
|
54
|
+
<Button type="primary" onClick={this.handleRefresh}>
|
|
55
|
+
刷新浏览器
|
|
56
|
+
</Button>
|
|
57
|
+
}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return this.props.children;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default ErrorBoundary;
|
package/src/layouts/index.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import menuRoutes from '../../routes/menu';
|
|
|
4
4
|
import getIcon from '@/utils/getIcon';
|
|
5
5
|
import { useLocation, Link } from 'react-router-dom';
|
|
6
6
|
import { isInIcestark } from '@ice/stark-app';
|
|
7
|
+
import ErrorBoundary from '@/components/ErrorBoundary';
|
|
7
8
|
|
|
8
9
|
const menuDataRender = (menuList) => {
|
|
9
10
|
return menuList.map((item) => {
|
|
@@ -33,6 +34,7 @@ const BasicLayout = (props) => {
|
|
|
33
34
|
}}
|
|
34
35
|
menuItemRender={(item, dom) => <Link to={item.path}>{dom}</Link>}
|
|
35
36
|
locale="zh-CN"
|
|
37
|
+
ErrorBoundary={ErrorBoundary}
|
|
36
38
|
>
|
|
37
39
|
{children}
|
|
38
40
|
</ProLayout>
|
package/src/pages/home.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import styles from './index.module.less';
|
|
3
3
|
import { DatePicker } from 'antd';
|
|
4
4
|
|
|
5
5
|
export default function Home(props) {
|
|
6
|
+
useEffect(() => {}, [a]);
|
|
7
|
+
const a = 1;
|
|
6
8
|
return (
|
|
7
9
|
<div>
|
|
8
10
|
<DatePicker></DatePicker>
|
package/vite.config.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
|
-
import react from '@vitejs/plugin-react';
|
|
2
|
+
import react from '@vitejs/plugin-react-swc';
|
|
3
3
|
import { viteMockServe } from 'vite-plugin-mock';
|
|
4
|
-
import pluginExternal from 'vite-plugin-external';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
|
|
7
6
|
export default defineConfig(({ command }) => ({
|
|
@@ -17,13 +16,6 @@ export default defineConfig(({ command }) => ({
|
|
|
17
16
|
mockPath: 'mock',
|
|
18
17
|
enable: command === 'serve',
|
|
19
18
|
}),
|
|
20
|
-
pluginExternal({
|
|
21
|
-
externals: {
|
|
22
|
-
react: 'React',
|
|
23
|
-
'react-dom': 'ReactDOM',
|
|
24
|
-
antd: 'antd',
|
|
25
|
-
},
|
|
26
|
-
}),
|
|
27
19
|
],
|
|
28
20
|
resolve: {
|
|
29
21
|
alias: {
|
|
@@ -31,7 +23,6 @@ export default defineConfig(({ command }) => ({
|
|
|
31
23
|
umi: path.resolve(__dirname, './src/umi'),
|
|
32
24
|
'~antd': path.resolve(__dirname, './node_modules/antd'),
|
|
33
25
|
},
|
|
34
|
-
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
|
|
35
26
|
},
|
|
36
27
|
css: {
|
|
37
28
|
preprocessorOptions: {
|
|
@@ -49,7 +40,7 @@ export default defineConfig(({ command }) => ({
|
|
|
49
40
|
lib: {
|
|
50
41
|
entry: path.resolve(__dirname, 'src/main.tsx'),
|
|
51
42
|
name: 'CodeSnippet',
|
|
52
|
-
formats: ['
|
|
43
|
+
formats: ['umd'],
|
|
53
44
|
// fileName: (format) => `code-snippet.${format}.js`,
|
|
54
45
|
},
|
|
55
46
|
// rollupOptions: {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,158 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,276 +0,0 @@
|
|
|
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;
|
|
@@ -1,79 +0,0 @@
|
|
|
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
|
-
});
|