@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,405 @@
1
+ import React, { memo, useCallback, useMemo, useState } from 'react';
2
+ import { useLongPress } from '../../hooks/useLongPress';
3
+ import { Table, Dropdown, MenuProps, TableProps, Tag } from 'antd';
4
+ import type { SortOrder } from 'antd/es/table/interface';
5
+ import { ProjectFile, FileSortBy } from './types';
6
+ import { formatDatetime, formatDisksize } from '../../utils/formatters';
7
+ import { uiFilePreview } from '../UIFilePreview/index';
8
+ import { FileContextMenu } from './FileContextMenu';
9
+ import { CheckOutlined, CloudOutlined, WarningOutlined, LinkOutlined } from '@ant-design/icons';
10
+ import { getDisplayName } from './utils/fileUtils';
11
+ import { sortTree } from './utils/fileTree';
12
+
13
+ interface FileTableViewProps {
14
+ dataSource: ProjectFile[];
15
+ expandedRowKeys: string[];
16
+ selectedFolderId: string | null;
17
+ dragOverId: string | null;
18
+ dragSourceId: string | null;
19
+ smallView: boolean;
20
+ isFolderExpanded: (folderId: string) => boolean;
21
+ onExpand: (expanded: boolean, record: ProjectFile) => void;
22
+ onPreview: (file: ProjectFile) => void;
23
+ onDownload: (file: ProjectFile) => void;
24
+ /** 下载文件夹回调(仅管理员/负责人提供,表格行右键菜单复用该回调) */
25
+ onDownloadFolder?: (file: ProjectFile) => void;
26
+ /** 复制链接回调(未提供时不显示「复制链接」) */
27
+ onCopyLink?: (file: ProjectFile) => void;
28
+ onRename: (file: ProjectFile) => void;
29
+ onEditDescs: (file: ProjectFile) => void;
30
+ onDelete: (fileId: string) => void;
31
+ onEdit?: (file: ProjectFile) => void;
32
+ /** 生成临时文件链接回调 */
33
+ onGenerateTempLink?: (file: ProjectFile) => void;
34
+ onMove: (sourceId: string, targetId: string | null) => void;
35
+ onDragStart: (fileId: string) => void;
36
+ onDragEnd: () => void;
37
+ onDragOver: (fileId: string) => void;
38
+ onDragLeave: (fileId: string) => void;
39
+ onDrop: (sourceId: string, targetId: string) => void;
40
+ onRowContextMenu: (event: React.MouseEvent, record: ProjectFile) => void;
41
+ /** 移动到其他项目回调 */
42
+ onMoveToProject?: (file: ProjectFile) => void;
43
+ /** 复制到其他项目回调 */
44
+ onCopyToProject?: (file: ProjectFile) => void;
45
+ /** 复制副本回调 */
46
+ onCopyToSelf?: (file: ProjectFile) => void;
47
+ /** 查看文件历史版本回调 */
48
+ onViewVersionHistory?: (file: ProjectFile) => void;
49
+ /** 跳转至合同回调 */
50
+ onJumpToContract?: (file: ProjectFile) => void;
51
+ /** 排序字段 */
52
+ sortBy: FileSortBy;
53
+ /** 排序顺序 */
54
+ sortOrder: 'asc' | 'desc';
55
+ /** 切换排序字段回调(如果是相同字段则切换顺序) */
56
+ onSortByChange: (by: FileSortBy) => void;
57
+ }
58
+
59
+ // antd 通过 sortDirections.indexOf(当前顺序) + 1 计算下一次排序;
60
+ // 只给两个方向时,第三次点击会进入“取消排序”并丢失 columnKey,导致只能排序一次。
61
+ // 末尾重复 ascend 可让顺序在“升序 → 降序 → 升序”之间无限循环。
62
+ const SORT_DIRECTIONS: SortOrder[] = ['ascend', 'descend', 'ascend'];
63
+
64
+ const NAME_COL_MIN_WIDTH = 120;
65
+ const NAME_COL_DEFAULT_WIDTH = 400;
66
+
67
+ interface ResizableTitleProps extends React.HTMLAttributes<HTMLTableCellElement> {
68
+ width?: number;
69
+ onResizeStart?: (e: React.MouseEvent) => void;
70
+ }
71
+
72
+ // 可拖拽调宽的表头单元格:仅有 onResizeStart 的列(名称列)渲染拖拽手柄
73
+ const ResizableTitle: React.FC<ResizableTitleProps> = (props) => {
74
+ const { width, onResizeStart, style, children, ...restProps } = props;
75
+ if (!width || !onResizeStart) {
76
+ return <th style={style} {...restProps}>{children}</th>;
77
+ }
78
+ return (
79
+ <th style={{ ...style, position: 'relative' }} {...restProps}>
80
+ {children}
81
+ <span
82
+ className="file-table-col-resizer"
83
+ onMouseDown={onResizeStart}
84
+ onClick={(e) => e.stopPropagation()}
85
+ onDoubleClick={(e) => e.stopPropagation()}
86
+ />
87
+ </th>
88
+ );
89
+ };
90
+
91
+ const FileTableViewComponent: React.FC<FileTableViewProps> = ({
92
+ dataSource,
93
+ expandedRowKeys,
94
+ selectedFolderId,
95
+ dragOverId,
96
+ dragSourceId,
97
+ smallView,
98
+ isFolderExpanded,
99
+ onExpand,
100
+ onPreview,
101
+ onDownload,
102
+ onCopyLink,
103
+ onRename,
104
+ onEditDescs,
105
+ onDelete,
106
+ onEdit,
107
+ onGenerateTempLink,
108
+ onMove,
109
+ onDragStart,
110
+ onDragEnd,
111
+ onDragOver,
112
+ onDragLeave,
113
+ onDrop,
114
+ onRowContextMenu,
115
+ onMoveToProject,
116
+ onCopyToProject,
117
+ onCopyToSelf,
118
+ onViewVersionHistory,
119
+ onJumpToContract,
120
+ sortBy,
121
+ sortOrder,
122
+ onSortByChange,
123
+ }) => {
124
+ const { handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel, wasLongPressed, resetLongPress } = useLongPress<ProjectFile>(
125
+ (record, e) => {
126
+ const touch = e.touches[0];
127
+ const mockEvent = {
128
+ preventDefault: () => {},
129
+ stopPropagation: () => {},
130
+ clientX: touch.clientX,
131
+ clientY: touch.clientY,
132
+ } as unknown as React.MouseEvent;
133
+ onRowContextMenu(mockEvent, record);
134
+ },
135
+ { delay: 500, smallView }
136
+ );
137
+
138
+ // 排序后的数据
139
+ const sortedDataSource = useMemo(() => {
140
+ return sortTree(dataSource, sortBy, sortOrder);
141
+ }, [dataSource, sortBy, sortOrder]);
142
+
143
+ // 名称列宽度(表头右侧手柄拖拽调节)
144
+ const [nameColWidth, setNameColWidth] = useState(NAME_COL_DEFAULT_WIDTH);
145
+
146
+ const handleNameColResizeStart = useCallback((e: React.MouseEvent) => {
147
+ e.preventDefault();
148
+ e.stopPropagation();
149
+ const startX = e.clientX;
150
+ const startWidth = nameColWidth;
151
+ const handleMove = (ev: MouseEvent) => {
152
+ setNameColWidth(Math.max(NAME_COL_MIN_WIDTH, startWidth + ev.clientX - startX));
153
+ };
154
+ const handleUp = () => {
155
+ document.removeEventListener('mousemove', handleMove);
156
+ document.removeEventListener('mouseup', handleUp);
157
+ document.body.style.cursor = '';
158
+ document.body.style.userSelect = '';
159
+ };
160
+ document.addEventListener('mousemove', handleMove);
161
+ document.addEventListener('mouseup', handleUp);
162
+ document.body.style.cursor = 'col-resize';
163
+ document.body.style.userSelect = 'none';
164
+ }, [nameColWidth]);
165
+
166
+ const handleTableChange: TableProps<ProjectFile>['onChange'] = (_pagination, _filters, sorter) => {
167
+ const active = Array.isArray(sorter) ? sorter[0] : sorter;
168
+ const by = active?.columnKey as FileSortBy | undefined;
169
+ if (by !== 'fileName' && by !== 'fileSize' && by !== 'uploadTime') return;
170
+ // 相同字段由上层切换升/降序:antd 第三次点击会给出“取消排序”,这里忽略 order,保证升序↔降序循环
171
+ onSortByChange(by);
172
+ };
173
+
174
+ // 表头排序箭头状态:仅当前排序字段显示箭头
175
+ const columnSortOrder = (by: FileSortBy): 'ascend' | 'descend' | null =>
176
+ sortBy === by ? (sortOrder === 'asc' ? 'ascend' : 'descend') : null;
177
+
178
+ const handleRowClick = (record: ProjectFile) => () => {
179
+ if (!wasLongPressed(record)) {
180
+ if (record.fileType === 'folder') {
181
+ onExpand(!isFolderExpanded(record.id), record);
182
+ }
183
+ }
184
+ resetLongPress(record);
185
+ };
186
+ const indentSize = smallView ? 12 : 16;
187
+
188
+ const columnsBase = [
189
+ {
190
+ title: '名称',
191
+ dataIndex: 'fileName',
192
+ key: 'fileName',
193
+ width: nameColWidth,
194
+ onHeaderCell: () => ({
195
+ width: nameColWidth,
196
+ onResizeStart: handleNameColResizeStart,
197
+ }),
198
+ sorter: true,
199
+ sortDirections: SORT_DIRECTIONS,
200
+ sortOrder: columnSortOrder('fileName'),
201
+ render: (fileName: string, record: ProjectFile) => (
202
+ <div
203
+ style={{ paddingLeft: `${record.level ? record.level * indentSize + 8 : 8}px`, minWidth: 0, opacity: (record.isContractSubfolder || record.isContractLink) && record.contractSigned === false ? 0.55 : undefined }}
204
+ onContextMenu={(e) => {
205
+ e.preventDefault();
206
+ e.stopPropagation();
207
+ onRowContextMenu(e, record);
208
+ }}
209
+ >
210
+ <div className='flex items-center overflow-hidden'>
211
+ {uiFilePreview.getFileIcon({
212
+ fileName,
213
+ fileType: record.fileType,
214
+ isExpanded: isFolderExpanded(record.id),
215
+ isSecure: record.isSecure,
216
+ isVirtualContractFolder: record.isVirtualContractFolder,
217
+ isContractSubfolder: record.isContractSubfolder,
218
+ isContractTagFolder: record.isContractTagFolder,
219
+ contractSigned: record.contractSigned,
220
+ })}
221
+ {record.isContractLink && (
222
+ <LinkOutlined className='ml-1 text-green-600' style={{ fontSize: 12 }} />
223
+ )}
224
+ <span
225
+ className={`truncate ml-1 flex-1 ${record.isContractLink ? 'hover:underline cursor-pointer' : ''}`}
226
+ title={record.isContractLink ? `合同附件${record.descs ? `(${record.descs})` : ''}` : getDisplayName(fileName, { isVirtualContractFolder: record.isVirtualContractFolder, isContractSubfolder: record.isContractSubfolder, descs: record.descs })}
227
+ >
228
+ {getDisplayName(fileName, { isVirtualContractFolder: record.isVirtualContractFolder, isContractSubfolder: record.isContractSubfolder, descs: record.descs })}
229
+ </span>
230
+ {record.isContractSubfolder && record.contractSigned === false && (
231
+ <Tag
232
+ className="ml-1"
233
+ style={{ marginInlineStart: 4 }}
234
+ title="该合同尚未签约,签约后盖章文件会显示在此文件夹中"
235
+ >
236
+ 未签约
237
+ </Tag>
238
+ )}
239
+ {record.isLocalOnly && (
240
+ <Tag
241
+ icon={<CloudOutlined />}
242
+ color="orange"
243
+ className="ml-1"
244
+ style={{ marginInlineStart: 4 }}
245
+ title="仅保存在本地,尚未上传到服务器"
246
+ >
247
+ 本地
248
+ </Tag>
249
+ )}
250
+ {!record.isLocalOnly && record.hasUnsyncedChanges && (
251
+ <Tag
252
+ icon={<WarningOutlined />}
253
+ color="warning"
254
+ className="ml-1"
255
+ style={{ marginInlineStart: 4 }}
256
+ title="本地有未同步的修改,重新上传后可与服务器一致"
257
+ >
258
+ 未同步
259
+ </Tag>
260
+ )}
261
+ {record.hasLocalCache && (
262
+ <span className="ml-1 flex h-3 w-3 items-center justify-center rounded-full bg-green-600 text-white" title="已缓存到本地">
263
+ <CheckOutlined className="w-2 h-2" />
264
+ </span>
265
+ )}
266
+ </div>
267
+ </div>
268
+ ),
269
+ },
270
+ {
271
+ title: '大小',
272
+ dataIndex: 'fileSize',
273
+ key: 'fileSize',
274
+ width: 85,
275
+ className: 'font-thin',
276
+ sorter: true,
277
+ sortDirections: SORT_DIRECTIONS,
278
+ sortOrder: columnSortOrder('fileSize'),
279
+ render: (size: number) => (size == 0 ? '' : formatDisksize(size)),
280
+ },
281
+ ];
282
+
283
+ const columnsDesc =
284
+ {
285
+ title: '备注',
286
+ dataIndex: 'descs',
287
+ key: 'descs',
288
+ className: 'font-thin',
289
+ ellipsis: true,
290
+ render: (descs: string) => descs || '',
291
+ };
292
+
293
+ const columnsTablet = [
294
+ {
295
+ title: '上传人',
296
+ dataIndex: 'uploaderName',
297
+ key: 'uploaderName',
298
+ width: 80,
299
+ className: 'font-thin',
300
+ },
301
+ {
302
+ title: '上传时间',
303
+ dataIndex: 'uploadTime',
304
+ key: 'uploadTime',
305
+ width: 140,
306
+ className: 'font-thin',
307
+ sorter: true,
308
+ sortDirections: SORT_DIRECTIONS,
309
+ sortOrder: columnSortOrder('uploadTime'),
310
+ render: (date: Date) => formatDatetime(date),
311
+ },
312
+ ];
313
+
314
+
315
+ const columns = smallView
316
+ ? columnsBase
317
+ : columnsBase.concat(columnsTablet as any);
318
+
319
+ columns.push(columnsDesc as any);
320
+
321
+ const getRowClassName = (record: ProjectFile) => {
322
+ if (record.fileType === 'folder' && record.id === selectedFolderId) {
323
+ return 'file-table-row-highlight';
324
+ }
325
+ if (record.fileType === 'folder' && dragOverId === record.id) {
326
+ return 'file-table-row-highlight';
327
+ }
328
+ return '';
329
+ };
330
+
331
+ return (
332
+ <div>
333
+ <Table
334
+ className='file-manager-table'
335
+ scroll={{ x: 800 }}
336
+ components={{ header: { cell: ResizableTitle } }}
337
+ columns={columns}
338
+ dataSource={sortedDataSource}
339
+ loading={false}
340
+ rowKey="id"
341
+ pagination={false}
342
+ size="small"
343
+ onChange={handleTableChange}
344
+ expandable={{
345
+ expandedRowKeys,
346
+ onExpand,
347
+ rowExpandable: (record: ProjectFile) => record.fileType === 'folder',
348
+ }}
349
+ expandIcon={() => null}
350
+ onRow={(record: ProjectFile) => ({
351
+ style: { cursor: 'pointer' },
352
+ title: record.isSecure ? '仅管理员可见' : getDisplayName(record.fileName, { isVirtualContractFolder: record.isVirtualContractFolder, isContractSubfolder: record.isContractSubfolder, descs: record.descs }),
353
+ draggable: !record.isContractLink && !record.isVirtualContractFolder && !record.isContractSubfolder && !record.isContractTagFolder,
354
+ onDragStart: (event) => {
355
+ if (record.isContractLink || record.isVirtualContractFolder || record.isContractSubfolder || record.isContractTagFolder) {
356
+ event.preventDefault();
357
+ return;
358
+ }
359
+ event.dataTransfer.setData('text/plain', record.id);
360
+ onDragStart(record.id);
361
+ },
362
+ onDragEnd: () => {
363
+ onDragEnd();
364
+ },
365
+ onDragOver: (event) => {
366
+ if (record.fileType === 'folder') {
367
+ event.preventDefault();
368
+ onDragOver(record.id);
369
+ }
370
+ },
371
+ onDragLeave: () => {
372
+ if (dragOverId === record.id) {
373
+ onDragLeave(record.id);
374
+ }
375
+ },
376
+ onDrop: (event) => {
377
+ if (record.fileType === 'folder') {
378
+ event.preventDefault();
379
+ const sourceId = event.dataTransfer.getData('text/plain');
380
+ if (sourceId) {
381
+ onDrop(sourceId, record.id);
382
+ }
383
+ }
384
+ },
385
+ onDoubleClick: () => {
386
+ if (record.fileType !== 'folder') {
387
+ onPreview(record);
388
+ }
389
+ },
390
+ onClick: handleRowClick(record),
391
+ onTouchStart: handleTouchStart(record),
392
+ onTouchMove: handleTouchMove(record),
393
+ onTouchEnd: handleTouchEnd(record),
394
+ onTouchCancel: handleTouchCancel(record),
395
+ onContextMenu: (event) => {
396
+ onRowContextMenu(event as any, record);
397
+ },
398
+ })}
399
+ rowClassName={getRowClassName}
400
+ />
401
+ </div>
402
+ );
403
+ };
404
+
405
+ export const FileTableView = memo(FileTableViewComponent);
@@ -0,0 +1,119 @@
1
+ import React, { useState, memo } from 'react';
2
+ import { Button, Dropdown, Divider, UploadFile } from 'antd';
3
+ import { FolderAddOutlined, FileMarkdownFilled, FileExcelFilled, FileWordFilled, CaretDownOutlined, FileTextFilled } from '@ant-design/icons';
4
+ import { UIFileUpload } from '../UIFileUpload';
5
+ import { UITableGridViewToggle } from '../UIViewToggle';
6
+ import { Space } from 'antd';
7
+ import { ProjectRole } from './types';
8
+
9
+ interface FileToolbarProps {
10
+ currentUserRole: ProjectRole | null;
11
+ viewMode: 'table' | 'grid';
12
+ onToggleViewMode: (mode: 'table' | 'grid') => void;
13
+ onCreateFolder: () => void;
14
+ onCreateMarkdown: () => void;
15
+ onCreateRdoc: () => void;
16
+ onCreateExcel: () => void;
17
+ onCreateWord: () => void;
18
+ onUploadFiles: (fileList: UploadFile[], serverData: any) => Promise<void> | void;
19
+ }
20
+
21
+ const FileToolbarComponent: React.FC<FileToolbarProps> = ({
22
+ currentUserRole,
23
+ viewMode,
24
+ onToggleViewMode,
25
+ onCreateFolder,
26
+ onCreateMarkdown,
27
+ onCreateRdoc,
28
+ onCreateExcel,
29
+ onCreateWord,
30
+ onUploadFiles,
31
+ }) => {
32
+ const [uploadFileList, setUploadFileList] = useState<UploadFile[]>([]);
33
+
34
+ // 项目所有者/管理员:可新建文件夹、文档并上传文件
35
+ const canManage = currentUserRole === ProjectRole.MANAGER || currentUserRole === ProjectRole.OWNER;
36
+ // 后端策略调整:普通成员也可以上传文件;仅查看者等其他角色保持只读
37
+ const canUpload = canManage || currentUserRole === ProjectRole.MEMBER;
38
+
39
+ return (
40
+ <Space>
41
+ {canManage && (
42
+ <Dropdown
43
+ menu={{
44
+ items: [
45
+ {
46
+ key: '1',
47
+ label: (
48
+ <>
49
+ <FolderAddOutlined className="text-blue-400 mr-1" /> 新建文件夹
50
+ </>
51
+ ),
52
+ onClick: onCreateFolder,
53
+ },
54
+ {
55
+ key: '6',
56
+ label: (
57
+ <>
58
+ <FileTextFilled className="text-yellow-600 mr-1" /> 新建文档
59
+ </>
60
+ ),
61
+ onClick: onCreateRdoc,
62
+ },
63
+ {
64
+ key: '2',
65
+ label: (
66
+ <>
67
+ <FileMarkdownFilled className="text-gray-800 mr-1" /> 新建Markdown
68
+ </>
69
+ ),
70
+ onClick: onCreateMarkdown,
71
+ },
72
+ {
73
+ key: '3',
74
+ label: (
75
+ <>
76
+ <FileWordFilled className="text-blue-600 mr-1" /> 新建Word
77
+ </>
78
+ ),
79
+ onClick: onCreateWord,
80
+ },
81
+ {
82
+ key: '5',
83
+ label: (
84
+ <>
85
+ <FileExcelFilled className="text-green-600 mr-1" /> 新建Excel
86
+ </>
87
+ ),
88
+ onClick: onCreateExcel,
89
+ },
90
+ ],
91
+ }}
92
+ placement="bottom"
93
+ >
94
+ <Button type="text">
95
+ 新建 <CaretDownOutlined />
96
+ </Button>
97
+ </Dropdown>
98
+ )}
99
+ {canUpload && (
100
+ <UIFileUpload
101
+ fileList={uploadFileList}
102
+ onFinish={async (fileList, serverData) => {
103
+ await onUploadFiles(fileList, serverData);
104
+ setUploadFileList([]);
105
+ }}
106
+ onChange={setUploadFileList}
107
+ multiple={false}
108
+ />
109
+ )}
110
+ <Divider type="vertical" />
111
+ <UITableGridViewToggle
112
+ viewMode={viewMode}
113
+ onToggle={onToggleViewMode}
114
+ />
115
+ </Space>
116
+ );
117
+ };
118
+
119
+ export const FileToolbar = memo(FileToolbarComponent);
@@ -0,0 +1,175 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { Modal, Table, Spin, Empty, Button, Space, message } from 'antd';
3
+ import { HistoryOutlined, DownloadOutlined, EyeOutlined, FileOutlined } from '@ant-design/icons';
4
+ import { API } from '../../api/index';
5
+ import { ResponseCode } from '../../api/_base';
6
+ import { uiFilePreview, UIFilePreview } from '../UIFilePreview/index';
7
+ import { ProjectFile } from './types';
8
+ import { formatDatetime, formatDisksize } from '../../utils/formatters';
9
+ import { ColumnsType } from 'antd/es/table';
10
+
11
+ interface FileVersion {
12
+ id: string;
13
+ fileId: string;
14
+ fileSize: number;
15
+ fileResourcePath: string;
16
+ uploadTime: Date;
17
+ uploaderName: string;
18
+ uploadBy: string;
19
+ }
20
+
21
+ interface FileVersionHistoryModalProps {
22
+ visible: boolean;
23
+ file: ProjectFile | null;
24
+ onClose: () => void;
25
+ }
26
+
27
+ export const FileVersionHistoryModal: React.FC<FileVersionHistoryModalProps> = ({
28
+ visible,
29
+ file,
30
+ onClose,
31
+ }) => {
32
+ const [loading, setLoading] = useState(false);
33
+ const [versions, setVersions] = useState<FileVersion[]>([]);
34
+ const [previewUrl, setPreviewUrl] = useState<string | null>(null);
35
+ const [previewVisible, setPreviewVisible] = useState(false);
36
+
37
+ const loadVersions = useCallback(async () => {
38
+ if (!file) return;
39
+
40
+ setLoading(true);
41
+ try {
42
+ const res = await API.project.fileversion.listVersions({ fileId: file.id });
43
+ if (res.code === ResponseCode.SUCCESS && res.data) {
44
+ setVersions(res.data);
45
+ } else {
46
+ message.error('获取文件历史版本失败');
47
+ }
48
+ } catch (error) {
49
+ message.error('获取文件历史版本失败');
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ }, [file]);
54
+
55
+ useEffect(() => {
56
+ if (visible && file) {
57
+ loadVersions();
58
+ } else {
59
+ setVersions([]);
60
+ }
61
+ }, [visible, file, loadVersions]);
62
+
63
+ const handlePreview = (version: FileVersion) => {
64
+ setPreviewUrl(version.fileResourcePath);
65
+ setPreviewVisible(true);
66
+ };
67
+
68
+ const handleDownload = (version: FileVersion) => {
69
+ uiFilePreview.downloadFile(version.fileResourcePath, undefined, version.fileId)
70
+ .catch(() => {
71
+ // downloadFile 内部已统一提示下载失败
72
+ });
73
+ };
74
+
75
+ const columns: ColumnsType<FileVersion> = [
76
+ {
77
+ title: '版本',
78
+ key: 'version',
79
+ width: 80,
80
+ render: (_: any, __: any, index: number) => (
81
+ <span className="text-blue-600">v{versions.length - index}</span>
82
+ ),
83
+ },
84
+ {
85
+ title: '文件大小',
86
+ dataIndex: 'fileSize',
87
+ key: 'fileSize',
88
+ width: 100,
89
+ render: (size: number) => formatDisksize(size),
90
+ },
91
+ {
92
+ title: '上传时间',
93
+ dataIndex: 'uploadTime',
94
+ key: 'uploadTime',
95
+ width: 160,
96
+ render: (time: Date) => formatDatetime(time),
97
+ },
98
+ {
99
+ title: '上传人',
100
+ dataIndex: 'uploaderName',
101
+ key: 'uploaderName',
102
+ width: 100,
103
+ },
104
+ {
105
+ title: '操作',
106
+ key: 'action',
107
+ width: 120,
108
+ render: (_: any, record: FileVersion) => (
109
+ <Space>
110
+ <Button
111
+ type="text"
112
+ icon={<EyeOutlined />}
113
+ onClick={() => handlePreview(record)}
114
+ title="预览"
115
+ />
116
+ <Button
117
+ type="text"
118
+ icon={<DownloadOutlined />}
119
+ onClick={() => handleDownload(record)}
120
+ title="下载"
121
+ />
122
+ </Space>
123
+ ),
124
+ },
125
+ ];
126
+
127
+ return (
128
+ <>
129
+ <Modal
130
+ title={
131
+ <div className="flex items-center">
132
+ <HistoryOutlined className="mr-2" />
133
+ <span>文件历史版本 - {file?.fileName}</span>
134
+ </div>
135
+ }
136
+ open={visible}
137
+ onCancel={onClose}
138
+ footer={null}
139
+ width={720}
140
+ destroyOnHidden
141
+ >
142
+ <div className="py-4">
143
+ {loading ? (
144
+ <div className="flex justify-center items-center py-12">
145
+ <Spin />
146
+ </div>
147
+ ) : versions.length === 0 ? (
148
+ <Empty description="暂无历史版本" className="py-12" />
149
+ ) : (
150
+ <Table
151
+ columns={columns}
152
+ dataSource={versions}
153
+ rowKey="id"
154
+ pagination={false}
155
+ scroll={{ y: 400 }}
156
+ size="middle"
157
+ />
158
+ )}
159
+ </div>
160
+ </Modal>
161
+
162
+ {/* 文件预览 */}
163
+ <UIFilePreview
164
+ fileUrl={previewUrl}
165
+ fileName={file?.fileName}
166
+ visible={previewVisible}
167
+ onClose={() => setPreviewVisible(false)}
168
+ editable={false}
169
+ uploadPercent={undefined}
170
+ />
171
+ </>
172
+ );
173
+ };
174
+
175
+ export default FileVersionHistoryModal;