@lxzy/code-snippet 0.0.16 → 0.0.18

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 (37) hide show
  1. package/.editorconfig +0 -0
  2. package/.omo/run-continuation/ses_06cbc1e51ffeeonXDa7nL4s4eI.json +10 -0
  3. package/.omo/run-continuation/ses_082cd79efffeCIKYhEEM7T5tIa.json +10 -0
  4. package/build/cdn/antd.min.css +10 -0
  5. package/build/cdn/antd.min.js +26 -0
  6. package/build/cdn/axios.min.js +3 -0
  7. package/build/cdn/lodash.min.js +140 -0
  8. package/build/cdn/moment.min.js +2 -0
  9. package/build/cdn/react-dom.development.min.js +1 -0
  10. package/build/cdn/react-dom.production.min.js +245 -0
  11. package/build/cdn/react.development.min.js +1 -0
  12. package/build/cdn/react.production.min.js +31 -0
  13. package/build/code-snippet.css +2 -0
  14. package/build/code-snippet.iife.js +219 -0
  15. package/index.html +1 -1
  16. package/package.json +14 -10
  17. package/src/app-vite.tsx +30 -25
  18. package/src/components/ErrorBoundary/index.tsx +1 -21
  19. package/src/{global.css → global.less} +8 -0
  20. package/src/pages/home.tsx +63 -4
  21. package/src/pages/ly/ProFormUpload/icons/audio.png +0 -0
  22. package/src/pages/ly/ProFormUpload/icons/excel.png +0 -0
  23. package/src/pages/ly/ProFormUpload/icons/file.png +0 -0
  24. package/src/pages/ly/ProFormUpload/icons/pdf.png +0 -0
  25. package/src/pages/ly/ProFormUpload/icons/ppt.png +0 -0
  26. package/src/pages/ly/ProFormUpload/icons/rar.png +0 -0
  27. package/src/pages/ly/ProFormUpload/icons/txt.png +0 -0
  28. package/src/pages/ly/ProFormUpload/icons/video.png +0 -0
  29. package/src/pages/ly/ProFormUpload/icons/word.png +0 -0
  30. package/src/pages/ly/ProFormUpload/icons/zip.png +0 -0
  31. package/src/pages/ly/ProFormUpload/index.less +158 -0
  32. package/src/pages/ly/ProFormUpload/index.tsx +276 -0
  33. package/src/pages/ly/ProFormUpload/service.ts +79 -0
  34. package/src/umi/index.ts +2 -2
  35. package/tsconfig.json +10 -5
  36. package/vite.config.ts +0 -44
  37. package/src/main.tsx +0 -24
@@ -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
+ });
package/src/umi/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { createBrowserHistory } from 'history-with-query';
1
+ import { createHashHistory } from 'history-with-query';
2
2
  import { useModel, ModelProvider } from './useModel';
3
3
 
4
- const history = createBrowserHistory();
4
+ const history = createHashHistory();
5
5
 
6
6
  export { history, useModel, ModelProvider };
7
7
  export default history;
package/tsconfig.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "module": "ESNext",
4
- "target": "ESNext",
5
- "lib": ["DOM", "ESNext", "DOM.Iterable"],
6
- "moduleResolution": "bundler",
3
+ "module": "esnext",
4
+ "target": "esnext",
5
+ "moduleResolution": "node",
7
6
  "resolveJsonModule": true,
8
7
  "importHelpers": true,
9
8
  "jsx": "react-jsx",
@@ -21,5 +20,11 @@
21
20
  "emitDecoratorMetadata": true,
22
21
  "skipLibCheck": true
23
22
  },
24
- "include": ["mock/**/*", "src/**/*", "routes/**/*", "typings.d.ts", "vite.*.ts"]
23
+ "include": [
24
+ "mock/**/*",
25
+ "src/**/*",
26
+ "routes/**/*",
27
+ "typings.d.ts",
28
+ "vite.*.ts"
29
+ ]
25
30
  }
package/vite.config.ts CHANGED
@@ -37,50 +37,6 @@ export default defineConfig(({ command }) => ({
37
37
  },
38
38
  build: {
39
39
  outDir: 'build',
40
- lib: {
41
- entry: path.resolve(__dirname, 'src/main.tsx'),
42
- name: 'CodeSnippet',
43
- formats: ['umd'],
44
- // fileName: (format) => `code-snippet.${format}.js`,
45
- },
46
- // rollupOptions: {
47
- // external: [
48
- // 'react',
49
- // 'react-dom',
50
- // 'antd',
51
- // // 'moment',
52
- // // 'react-router-dom',
53
- // // '@ant-design/pro-layout',
54
- // // '@ant-design/pro-table',
55
- // // '@ant-design/pro-form',
56
- // // '@ant-design/pro-descriptions',
57
- // // '@ant-design/pro-list',
58
- // // '@ant-design/icons',
59
- // // '@ice/stark-app',
60
- // // 'ahooks',
61
- // // 'axios',
62
- // ],
63
- // output: {
64
- // globals: {
65
- // react: 'React',
66
- // 'react-dom': 'ReactDOM',
67
- // antd: 'antd',
68
- // // moment: 'moment',
69
- // // 'react-router-dom': 'ReactRouterDOM',
70
- // // '@ant-design/pro-layout': 'AntDesignProLayout',
71
- // // '@ant-design/pro-table': 'AntDesignProTable',
72
- // // '@ant-design/pro-form': 'AntDesignProForm',
73
- // // '@ant-design/pro-descriptions': 'AntDesignProDescriptions',
74
- // // '@ant-design/pro-list': 'AntDesignProList',
75
- // // '@ant-design/icons': 'AntDesignIcons',
76
- // // '@ice/stark-app': 'IceStarkApp',
77
- // // ahooks: 'ahooks',
78
- // // axios: 'axios',
79
- // // classnames: 'classNames',
80
- // // dayjs: 'dayjs',
81
- // },
82
- // },
83
- // },
84
40
  },
85
41
  server: {
86
42
  port: 8000,
package/src/main.tsx DELETED
@@ -1,24 +0,0 @@
1
- import React from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import App from './app-vite';
4
- import './global.css';
5
- import { setLibraryName, isInIcestark } from '@ice/stark-app';
6
-
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');