@spacecorexdev/core 0.0.4 → 0.0.5

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 (132) hide show
  1. package/package.json +2 -36
  2. package/src/api/_base.ts +243 -0
  3. package/src/api/index.ts +17 -0
  4. package/src/api/project.ts +1345 -0
  5. package/src/api/user.ts +1174 -0
  6. package/src/contract.ts +50 -0
  7. package/src/hooks/index.ts +2 -0
  8. package/{dist/hooks/index.js → src/hooks/useLongPress.ts} +53 -49
  9. package/src/hooks/useOnlineStatus.ts +22 -0
  10. package/src/index.ts +14 -0
  11. package/src/types/id.ts +2 -0
  12. package/src/types/index.ts +5 -0
  13. package/src/types/project/base.ts +63 -0
  14. package/src/types/project/contract.ts +79 -0
  15. package/src/types/project/contractStatus.tsx +15 -0
  16. package/src/types/project/index.ts +5 -0
  17. package/src/types/project/member.ts +46 -0
  18. package/src/types/project/progress.ts +43 -0
  19. package/src/types/projectRole.ts +17 -0
  20. package/src/types/theme.ts +1 -0
  21. package/src/types/user/index.ts +1 -0
  22. package/src/types/user/role.ts +7 -0
  23. package/src/ui/CommentTimeline/index.tsx +330 -0
  24. package/src/ui/FileManager/FileContextMenu.tsx +628 -0
  25. package/src/ui/FileManager/FileGridView.tsx +529 -0
  26. package/src/ui/FileManager/FileTableView.tsx +405 -0
  27. package/src/ui/FileManager/FileToolbar.tsx +119 -0
  28. package/src/ui/FileManager/FileVersionHistoryModal.tsx +175 -0
  29. package/src/ui/FileManager/MoveCopyToProjectModal.tsx +119 -0
  30. package/src/ui/FileManager/ShareToWechatModal.tsx +419 -0
  31. package/src/ui/FileManager/hooks/useFileManagerActions.ts +2532 -0
  32. package/src/ui/FileManager/hooks/useFileManagerState.ts +419 -0
  33. package/src/ui/FileManager/index.tsx +580 -0
  34. package/src/ui/FileManager/projectFileManagerApi.ts +57 -0
  35. package/src/ui/FileManager/types.ts +184 -0
  36. package/src/ui/FileManager/utils/fileDeltaWorker.ts +193 -0
  37. package/src/ui/FileManager/utils/fileTree.ts +178 -0
  38. package/src/ui/FileManager/utils/fileUpload.ts +714 -0
  39. package/src/ui/FileManager/utils/fileUtils.ts +65 -0
  40. package/src/ui/InvoiceRecordsList/index.tsx +813 -0
  41. package/src/ui/MyFileSelectorModal.tsx +261 -0
  42. package/src/ui/ProjectSelector/index.tsx +472 -0
  43. package/src/ui/ProjectSelector/projectCache.ts +40 -0
  44. package/src/ui/RoleSelector/index.tsx +473 -0
  45. package/src/ui/RoleSelector/roleCache.ts +40 -0
  46. package/src/ui/UIAlert.tsx +399 -0
  47. package/src/ui/UIAlertExample.tsx +129 -0
  48. package/src/ui/UIConfirm.tsx +335 -0
  49. package/src/ui/UIConfirmExample.tsx +165 -0
  50. package/src/ui/UIDatePicker.tsx +146 -0
  51. package/src/ui/UIDateRangePicker.tsx +38 -0
  52. package/src/ui/UIFilePreview/ExcelEditor/index.tsx +617 -0
  53. package/src/ui/UIFilePreview/ExcelEditor/types.ts +24 -0
  54. package/src/ui/UIFilePreview/ExportDropdown.css +85 -0
  55. package/src/ui/UIFilePreview/ExportDropdown.tsx +109 -0
  56. package/src/ui/UIFilePreview/IndexedDBUtils.ts +564 -0
  57. package/src/ui/UIFilePreview/MarkdownEditor/exportMarkdownPdf.ts +576 -0
  58. package/src/ui/UIFilePreview/MarkdownEditor/index.tsx +677 -0
  59. package/src/ui/UIFilePreview/MarkdownEditor/markdownEditor.css +1005 -0
  60. package/src/ui/UIFilePreview/MarkdownEditor/types.ts +15 -0
  61. package/src/ui/UIFilePreview/PPTXEditor/index.tsx +602 -0
  62. package/src/ui/UIFilePreview/RichDocEditor/exportRDocMarkdown.ts +77 -0
  63. package/src/ui/UIFilePreview/RichDocEditor/exportRDocPdf.ts +374 -0
  64. package/src/ui/UIFilePreview/RichDocEditor/index.tsx +730 -0
  65. package/src/ui/UIFilePreview/RichDocEditor/richdoc-editor.css +59 -0
  66. package/src/ui/UIFilePreview/UIFilePreview.tsx +611 -0
  67. package/src/ui/UIFilePreview/WordEditor/index.tsx +3933 -0
  68. package/src/ui/UIFilePreview/WordEditor/style.ts +1213 -0
  69. package/src/ui/UIFilePreview/WordEditor/word-editor.css +999 -0
  70. package/src/ui/UIFilePreview/filePreview.css +193 -0
  71. package/src/ui/UIFilePreview/filetypes/archive.tsx +66 -0
  72. package/src/ui/UIFilePreview/filetypes/cad.tsx +287 -0
  73. package/src/ui/UIFilePreview/filetypes/excel.tsx +355 -0
  74. package/src/ui/UIFilePreview/filetypes/image.tsx +20 -0
  75. package/src/ui/UIFilePreview/filetypes/imageLoader.ts +63 -0
  76. package/src/ui/UIFilePreview/filetypes/pdf.tsx +156 -0
  77. package/src/ui/UIFilePreview/filetypes/ppt.tsx +535 -0
  78. package/src/ui/UIFilePreview/filetypes/pptExportPdf.ts +523 -0
  79. package/src/ui/UIFilePreview/filetypes/video.tsx +482 -0
  80. package/src/ui/UIFilePreview/filetypes/word.tsx +245 -0
  81. package/src/ui/UIFilePreview/filetypes/wordExportMarkdown.ts +86 -0
  82. package/src/ui/UIFilePreview/filetypes/wordExportPdf.tsx +427 -0
  83. package/src/ui/UIFilePreview/index.ts +35 -0
  84. package/src/ui/UIFilePreview/pdfPagination.ts +143 -0
  85. package/src/ui/UIFilePreview/previewUtils.tsx +531 -0
  86. package/src/ui/UIFileUpload.tsx +382 -0
  87. package/src/ui/UIImageUpload.tsx +195 -0
  88. package/src/ui/UILoading.tsx +255 -0
  89. package/src/ui/UILoadingExample.tsx +139 -0
  90. package/src/ui/UISelect.tsx +105 -0
  91. package/src/ui/UIToast.tsx +381 -0
  92. package/src/ui/UIToastExample.tsx +69 -0
  93. package/src/ui/UIToastSingle.tsx +279 -0
  94. package/src/ui/UIViewToggle.tsx +64 -0
  95. package/src/ui/UserSelector/index.tsx +583 -0
  96. package/src/ui/index.ts +84 -0
  97. package/src/ui/upload/image.ts +135 -0
  98. package/src/ui/upload/index.ts +73 -0
  99. package/src/ui/upload/submitFile.ts +106 -0
  100. package/src/ui/upload/submitForm.ts +124 -0
  101. package/src/ui/upload/upload.err.ts +15 -0
  102. package/src/ui/upload/upload.ts +250 -0
  103. package/src/utils/CacheDepartmentInfo.ts +372 -0
  104. package/src/utils/CacheFileContent.ts +315 -0
  105. package/src/utils/CachePermissionInfo.ts +235 -0
  106. package/src/utils/CacheRoleInfo.ts +303 -0
  107. package/src/utils/CacheUserInfo.ts +818 -0
  108. package/src/utils/CacheViewToggleInfo.ts +182 -0
  109. package/src/utils/MessageContentUtils.ts +268 -0
  110. package/src/utils/clipboard.ts +70 -0
  111. package/src/utils/companyEmail.ts +19 -0
  112. package/src/utils/forbidDebugger.ts +59 -0
  113. package/src/utils/formatters.ts +94 -0
  114. package/src/utils/htmlUtils.tsx +59 -0
  115. package/src/utils/index.ts +18 -0
  116. package/src/utils/keyboard.ts +114 -0
  117. package/src/utils/mammothApi.ts +16 -0
  118. package/src/utils/polyfills.ts +928 -0
  119. package/src/utils/regex.ts +14 -0
  120. package/src/utils/storage.ts +95 -0
  121. package/src/utils/workerBootstrap.ts +87 -0
  122. package/dist/api/index.js +0 -1553
  123. package/dist/contract.js +0 -5
  124. package/dist/index.js +0 -4492
  125. package/dist/types/index.js +0 -49
  126. package/dist/ui/index.css +0 -1876
  127. package/dist/ui/index.js +0 -22245
  128. package/dist/utils/index.js +0 -4296
  129. /package/{dist → src}/styles/ant-modal.css +0 -0
  130. /package/{dist → src}/styles/ant-spin.css +0 -0
  131. /package/{dist → src}/styles/ant-table.css +0 -0
  132. /package/{dist → src}/styles/index.css +0 -0
@@ -0,0 +1,382 @@
1
+ /**
2
+ * UIFileUpload - 文件上传组件
3
+ *
4
+ * 使用示例:
5
+ *
6
+ * 1. 基础用法:
7
+ * import { UIFileUpload } from './UIFileUpload';
8
+ * import type { UploadFile } from 'antd/es/upload/interface';
9
+ *
10
+ * const [fileList, setFileList] = useState<UploadFile[]>([]);
11
+ *
12
+ * <UIFileUpload
13
+ * fileList={fileList}
14
+ * onChange={setFileList}
15
+ * maxCount={5}
16
+ * accept=".doc,.docx,.pdf"
17
+ * />
18
+ *
19
+ * 2. 完整配置:
20
+ * <UIFileUpload
21
+ * fileList={fileList}
22
+ * onChange={setFileList}
23
+ * onFinish={async (newFileList) => {
24
+ * // 所有文件上传完成后的回调
25
+ * console.log('上传完成:', newFileList);
26
+ * }}
27
+ * cancel={(file) => {
28
+ * // 取消上传文件的回调
29
+ * console.log('取消上传:', file);
30
+ * }}
31
+ * maxCount={10}
32
+ * disabled={false}
33
+ * multiple={true}
34
+ * accept=".jpg,.jpeg,.png,.pdf,.doc,.docx"
35
+ * />
36
+ *
37
+ * 特点:
38
+ * - 支持文件大小限制(最大100MB)
39
+ * - 支持文件类型限制
40
+ * - 支持上传进度显示
41
+ * - 支持取消上传
42
+ * - 支持多文件上传
43
+ */
44
+
45
+
46
+ import React, { useEffect, useRef, useState } from 'react';
47
+ import { Upload, Button, Modal, Image, Space, message, Progress } from 'antd';
48
+ import {
49
+ UploadOutlined,
50
+ DownloadOutlined,
51
+ EyeOutlined,
52
+ FilePdfFilled,
53
+ FileExcelFilled,
54
+ FileWordFilled,
55
+ FileTextFilled,
56
+ FileUnknownFilled
57
+ } from '@ant-design/icons';
58
+ import type { UploadFile, UploadProps } from 'antd/es/upload/interface';
59
+ import { FileUploadHandler } from './upload';
60
+ import { getImageBase64ByFile } from './upload/image';
61
+
62
+ import * as febs from 'febs-browser';
63
+
64
+ // example:
65
+ // {/* 文件预览模态窗口 */}
66
+ // <UIFilePreviewModal
67
+ // visible={previewVisible}
68
+ // file={previewFile}
69
+ // onClose={() => setPreviewVisible(false)}
70
+ // onDownload={handleDownloadFile}
71
+ // />
72
+
73
+ // <UIFileList
74
+ // files={request.attachments}
75
+ // onPreview={onPreviewFile}
76
+ // onDownload={onDownloadFile}
77
+ // />
78
+
79
+ // <UIFileUpload
80
+ // fileList={[]}
81
+ // onChange={onFileListChange}
82
+ // cancel={(file: UploadFile) => {
83
+ // console.log('取消上传文件:', file);
84
+ // // 调用文件的 cancel 方法
85
+ // }}
86
+ // />
87
+
88
+ interface FileUploadProps {
89
+ fileList: UploadFile[];
90
+ onChange?: (fileList: UploadFile[]) => void;
91
+ onFinish?: (fileList: UploadFile[], serverData: any) => Promise<void>;
92
+ cancel?: (file: UploadFile) => void;
93
+ maxCount?: number;
94
+ disabled?: boolean;
95
+ btnText?: string;
96
+ multiple?: boolean;
97
+ /**
98
+ * 使用逗号隔开的文件类型:.doc,.docx
99
+ */
100
+ accept?: string;
101
+ }
102
+
103
+ export const UIFileUpload: React.FC<FileUploadProps> = ({
104
+ fileList,
105
+ onChange,
106
+ onFinish,
107
+ cancel,
108
+ btnText = '上传文件',
109
+ maxCount = 5,
110
+ disabled = false,
111
+ multiple = true,
112
+ accept = undefined,
113
+ }) => {
114
+ const [uploadingPercent, setUploadingPercent] = useState<number | undefined>(undefined);
115
+ const fileListRef = useRef<UploadFile[]>(fileList);
116
+
117
+ useEffect(() => {
118
+ fileListRef.current = fileList;
119
+ }, [fileList]);
120
+
121
+ const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
122
+ // 更新整个文件列表
123
+ onChange?.(newFileList);
124
+ };
125
+
126
+ const removeFileFromList = (targetFile: UploadFile) => {
127
+ onChange?.(fileListRef.current.filter(file => file.uid !== targetFile.uid));
128
+ };
129
+
130
+ const beforeUpload = (file: File) => {
131
+ const isLt100M = file.size / 1024 / 1024 < 100;
132
+ if (!isLt100M) {
133
+ message.error('文件大小不能超过100MB');
134
+ }
135
+ return isLt100M || Upload.LIST_IGNORE;
136
+ };
137
+
138
+ const customRequest = async (options: any) => {
139
+ const { file, onSuccess, onError, onProgress } = options;
140
+
141
+ try {
142
+ // 设置初始进度
143
+ file.status = 'uploading';
144
+ file.percent = 0;
145
+ onProgress({ percent: 0 });
146
+ setUploadingPercent(0);
147
+
148
+ febs.crypt.crc32_file(file as unknown as File, (crc32) => {
149
+ if (crc32 == 0) {
150
+ file.status = 'error';
151
+ onError('calc crc32 error');
152
+ return;
153
+ }
154
+
155
+ let abortHandle = FileUploadHandler.uploadSingle(file, crc32, (percent: number) => {
156
+ const nextPercent = Math.floor(percent * 100);
157
+ onProgress({ percent: nextPercent });
158
+ file.percent = nextPercent;
159
+ setUploadingPercent(nextPercent);
160
+ }, (err) => {
161
+ setUploadingPercent(undefined);
162
+ onError(err);
163
+ }, (data) => {
164
+ file.status = 'done';
165
+ file.percent = 100;
166
+ setUploadingPercent(undefined);
167
+ onSuccess("ok");
168
+ onFinish && onFinish([file], data).then(() => { }).catch(() => { });
169
+ });
170
+
171
+ // 添加取消上传的方法
172
+ file.cancel = () => {
173
+ abortHandle.abort(); // 调用上传取消方法
174
+
175
+ file.status = 'error';
176
+ file.percent = 0;
177
+ setUploadingPercent(undefined);
178
+ onError(new Error('上传已取消'));
179
+ removeFileFromList(file);
180
+ };
181
+ });
182
+
183
+ } catch (err) {
184
+ file.status = 'error';
185
+ setUploadingPercent(undefined);
186
+ onError(err);
187
+ }
188
+ };
189
+
190
+ const isUploading = uploadingPercent !== undefined && uploadingPercent < 100;
191
+
192
+ const itemRender = (_originNode: React.ReactElement, file: UploadFile, _fileList: Array<UploadFile>, _actions: {
193
+ download: () => void;
194
+ preview: () => void;
195
+ remove: () => void;
196
+ }) => {
197
+ // const fileUploading = file.status === 'uploading' && (file.percent || 0) < 100;
198
+
199
+ if (!isUploading) {
200
+ return null;
201
+ }
202
+
203
+ // 自定义上传列表项
204
+ return (
205
+ <div className="flex items-center justify-between w-full mt-2 rounded border border-blue-100 bg-blue-50 px-3 py-2">
206
+ <span className="mr-3 min-w-0 max-w-[120px] flex-1 truncate text-sm text-gray-700">{file.name}</span>
207
+ <Progress
208
+ className="w-[60px]"
209
+ percent={Math.round(file.percent || 0)}
210
+ size="small"
211
+ status="active"
212
+ showInfo
213
+ />
214
+ <Button
215
+ className="ml-3"
216
+ size="small"
217
+ onClick={() => {
218
+ if (cancel) {
219
+ cancel(file);
220
+ } else {
221
+ (file as any).cancel?.();
222
+ }
223
+ removeFileFromList(file);
224
+ }}
225
+ danger
226
+ >
227
+ 取消
228
+ </Button>
229
+ </div>
230
+ );
231
+ };
232
+
233
+ return (
234
+ <Upload
235
+ fileList={fileList}
236
+ onChange={handleChange}
237
+ beforeUpload={beforeUpload}
238
+ customRequest={customRequest}
239
+ multiple={multiple}
240
+ maxCount={maxCount}
241
+ disabled={disabled}
242
+ itemRender={itemRender}
243
+ accept={accept}
244
+ // 显示进度条
245
+ showUploadList={true}
246
+ >
247
+ {!isUploading && <Button icon={<UploadOutlined />} disabled={disabled || isUploading}
248
+ title={multiple ? `上传文件(最多支持${maxCount}个文件,单个文件不超过100MB)` : '上传文件(单个文件不超过100MB)'}
249
+ >{isUploading ? <span className="inline-block w-[85px]">上传中 {uploadingPercent}%</span> : btnText}</Button>}
250
+ {/* <span className="ml-2 text-gray-500">
251
+ {multiple
252
+ ? `(最多支持${maxCount}个文件,单个文件不超过100MB)`
253
+ : `(单个文件不超过100MB)`}
254
+ </span> */}
255
+ </Upload>
256
+ );
257
+ };
258
+
259
+ interface FileListProps {
260
+ files?: UploadFile[];
261
+ onPreview?: (file: UploadFile) => void;
262
+ onDownload?: (file: UploadFile) => void;
263
+ }
264
+
265
+ export const UIFileList: React.FC<FileListProps> = ({
266
+ files = [],
267
+ onPreview,
268
+ onDownload
269
+ }) => {
270
+ if (files.length === 0) return null;
271
+
272
+ return (
273
+ <div className="p-4 bg-gray-50 rounded">
274
+ {files.map((file, index) => (
275
+ <div key={file.uid || index} className="flex items-center mb-2">
276
+ {getFileIcon(file)}
277
+ <span className="mr-2">{file.name}</span>
278
+ <Space>
279
+ {onPreview && (
280
+ <Button
281
+ type="link"
282
+ size="small"
283
+ icon={<EyeOutlined />}
284
+ onClick={() => onPreview(file)}
285
+ >
286
+ 查看
287
+ </Button>
288
+ )}
289
+ {onDownload && (
290
+ <Button
291
+ type="link"
292
+ size="small"
293
+ icon={<DownloadOutlined />}
294
+ onClick={() => onDownload(file)}
295
+ >
296
+ 下载
297
+ </Button>
298
+ )}
299
+ </Space>
300
+ </div>
301
+ ))}
302
+ </div>
303
+ );
304
+ };
305
+
306
+ const getFileIcon = (file: UploadFile) => {
307
+ const fileType = file.type || '';
308
+ if (fileType.includes('pdf')) {
309
+ return <FilePdfFilled className="mr-2 text-red-500" />;
310
+ } else if (fileType.includes('excel') || fileType.includes('spreadsheet') || file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
311
+ return <FileExcelFilled className="mr-2 text-green-500" />;
312
+ } else if (fileType.includes('word') || fileType.includes('document') || file.name.endsWith('.docx') || file.name.endsWith('.doc')) {
313
+ return <FileWordFilled className="mr-2 text-blue-500" />;
314
+ } else if (fileType.includes('image')) {
315
+ return <FileTextFilled className="mr-2 text-purple-500" />;
316
+ } else {
317
+ return <FileUnknownFilled className="mr-2 text-gray-500" />;
318
+ }
319
+ };
320
+
321
+ interface FilePreviewModalProps {
322
+ visible: boolean;
323
+ file: UploadFile | null;
324
+ onClose: () => void;
325
+ onDownload?: (file: UploadFile) => void;
326
+ }
327
+
328
+ export const UIFilePreviewModal: React.FC<FilePreviewModalProps> = ({
329
+ visible,
330
+ file,
331
+ onClose,
332
+ onDownload
333
+ }) => {
334
+ if (!file) return null;
335
+
336
+ const [previewImage, setPreviewImage] = useState<string>('');
337
+
338
+ const previewTitle = file.name || '';
339
+ // const previewImage = file.url || (file.preview as string) || '';
340
+ const isImage = file.type?.startsWith('image/');
341
+
342
+ if (isImage) {
343
+ getImageBase64ByFile(file as unknown as File, (base64: string | null) => {
344
+ setPreviewImage(base64||'');
345
+ });
346
+ }
347
+
348
+ return (
349
+ <Modal
350
+ open={visible}
351
+ title={previewTitle}
352
+ footer={null}
353
+ onCancel={onClose}
354
+ width={800}
355
+ >
356
+ {isImage ? (
357
+ <Image
358
+ alt={previewTitle}
359
+ style={{ width: '100%' }}
360
+ src={previewImage}
361
+ />
362
+ ) : (
363
+ <div className="text-center p-8">
364
+ <div className="text-6xl mb-4">
365
+ {getFileIcon(file)}
366
+ </div>
367
+ <p className="mb-4">{previewTitle}</p>
368
+ <p className="text-gray-500 mb-4">该文件类型无法直接预览</p>
369
+ {onDownload && (
370
+ <Button
371
+ type="primary"
372
+ icon={<DownloadOutlined />}
373
+ onClick={() => onDownload(file)}
374
+ >
375
+ 下载文件查看
376
+ </Button>
377
+ )}
378
+ </div>
379
+ )}
380
+ </Modal>
381
+ );
382
+ };
@@ -0,0 +1,195 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import { Button, Modal, message } from 'antd';
3
+ import { UploadOutlined } from '@ant-design/icons';
4
+ import ReactCrop, { Crop } from 'react-image-crop';
5
+ import 'react-image-crop/dist/ReactCrop.css';
6
+ import { compressImage, getImageBase64ByFile } from './upload/image';
7
+
8
+ interface UIImageUploadProps {
9
+ onUpload: (file: File|string) => Promise<void>;
10
+ accept?: string;
11
+ maxSize?: number;
12
+ aspectRatio?: number;
13
+ className?: string;
14
+ useBase64?: boolean;
15
+ id?: string;
16
+ }
17
+
18
+ export const UIImageUpload: React.FC<UIImageUploadProps> = ({
19
+ onUpload,
20
+ accept = 'image/*',
21
+ maxSize = 5,
22
+ aspectRatio,
23
+ className = '',
24
+ useBase64 = false,
25
+ id,
26
+ }) => {
27
+ const [previewVisible, setPreviewVisible] = useState(false);
28
+ const [previewImage, setPreviewImage] = useState<string>('');
29
+ const [crop, setCrop] = useState<Crop>({
30
+ unit: '%',
31
+ width: 90,
32
+ height: 90,
33
+ x: 5,
34
+ y: 5,
35
+ });
36
+ const [imageRef, setImageRef] = useState<HTMLImageElement | null>(null);
37
+ const [originalFile, setOriginalFile] = useState<File | null>(null);
38
+ const fileInputRef = useRef<HTMLInputElement>(null);
39
+
40
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
41
+ const file = event.target.files?.[0];
42
+ if (!file) return;
43
+
44
+ if (file.size > maxSize * 1024 * 1024) {
45
+ message.error(`文件大小不能超过 ${maxSize}MB`);
46
+ return;
47
+ }
48
+
49
+ if (!file.type.startsWith('image/')) {
50
+ message.error('请上传图片文件');
51
+ return;
52
+ }
53
+
54
+ setOriginalFile(file);
55
+ const reader = new FileReader();
56
+ reader.onload = (e) => {
57
+ setPreviewImage(e.target?.result as string);
58
+ setPreviewVisible(true);
59
+ };
60
+ reader.readAsDataURL(file);
61
+ };
62
+
63
+ const handleCropComplete = async (croppedArea: Crop) => {
64
+ if (!imageRef || !originalFile) return;
65
+
66
+ const displayedWidth = imageRef.width;
67
+ const displayedHeight = imageRef.height;
68
+ const scaleX = imageRef.naturalWidth / displayedWidth;
69
+ const scaleY = imageRef.naturalHeight / displayedHeight;
70
+
71
+ const fatorW = croppedArea.unit == '%' ? displayedWidth / 100 : 1;
72
+ const fatorH = croppedArea.unit == '%' ? displayedHeight / 100 : 1;
73
+
74
+ const sourceX = croppedArea.x * scaleX * fatorW;
75
+ const sourceY = croppedArea.y * scaleY * fatorH;
76
+ const sourceWidth = croppedArea.width * scaleX * fatorW;
77
+ const sourceHeight = croppedArea.height * scaleY * fatorH;
78
+
79
+ try {
80
+ const canvas = document.createElement('canvas');
81
+
82
+ canvas.width = sourceWidth;
83
+ canvas.height = sourceHeight;
84
+
85
+ const ctx = canvas.getContext('2d');
86
+ if (!ctx) return;
87
+
88
+ ctx.drawImage(
89
+ imageRef,
90
+ sourceX,
91
+ sourceY,
92
+ sourceWidth,
93
+ sourceHeight,
94
+ 0,
95
+ 0,
96
+ sourceWidth,
97
+ sourceHeight
98
+ );
99
+
100
+ canvas.toBlob((blob) => {
101
+ if (!blob) return;
102
+ const croppedFile = new File([blob], originalFile.name, {
103
+ type: originalFile.type,
104
+ });
105
+
106
+ if (useBase64) {
107
+ getImageBase64ByFile(croppedFile, (base64: string | null) => {
108
+ compressImage(base64 as string, 0, "image/jpeg", (compressedBase64: string | null) => {
109
+ setPreviewVisible(false);
110
+ onUpload(compressedBase64 as string);
111
+ });
112
+ });
113
+ return;
114
+ } else {
115
+ setPreviewVisible(false);
116
+ onUpload(croppedFile);
117
+ }
118
+ }, originalFile.type);
119
+
120
+ setPreviewVisible(false);
121
+ setPreviewImage('');
122
+ setOriginalFile(null);
123
+ if (fileInputRef.current) {
124
+ fileInputRef.current.value = '';
125
+ }
126
+ } catch (error) {
127
+ message.error('图片处理失败');
128
+ }
129
+ };
130
+
131
+ return (
132
+ <div className={className}>
133
+ <input
134
+ type="file"
135
+ ref={fileInputRef}
136
+ accept={accept}
137
+ onChange={handleFileChange}
138
+ style={{ display: 'none' }}
139
+ />
140
+ <Button
141
+ id={id}
142
+ icon={<UploadOutlined />}
143
+ onClick={() => fileInputRef.current?.click()}
144
+ className="btn-primary"
145
+ >
146
+ 选择图片
147
+ </Button>
148
+
149
+ <Modal
150
+ title="裁剪图片"
151
+ open={previewVisible}
152
+ onCancel={() => {
153
+ setPreviewVisible(false);
154
+ setPreviewImage('');
155
+ setOriginalFile(null);
156
+ if (fileInputRef.current) {
157
+ fileInputRef.current.value = '';
158
+ }
159
+ }}
160
+ footer={[
161
+ <Button key="cancel" onClick={() => setPreviewVisible(false)} className="btn-outline">
162
+ 取消
163
+ </Button>,
164
+ <Button
165
+ key="confirm"
166
+ type="primary"
167
+ onClick={() => handleCropComplete(crop)}
168
+ className="btn-primary"
169
+ >
170
+ 确认
171
+ </Button>,
172
+ ]}
173
+ width={500}
174
+ >
175
+ <div className="flex justify-center">
176
+ {previewImage && (
177
+ <ReactCrop
178
+ crop={crop}
179
+ onChange={(c) => setCrop(c)}
180
+ aspect={aspectRatio}
181
+ className="max-w-full"
182
+ >
183
+ <img
184
+ src={previewImage}
185
+ alt="预览"
186
+ ref={(img) => setImageRef(img)}
187
+ className="max-w-full"
188
+ />
189
+ </ReactCrop>
190
+ )}
191
+ </div>
192
+ </Modal>
193
+ </div>
194
+ );
195
+ };