@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,583 @@
1
+ import React, { useEffect, useState, useCallback, useRef, useMemo, ReactElement } from 'react';
2
+ import { Select, Avatar, Spin, message, Popover, Card, Typography, Space } from 'antd';
3
+ import { debounce } from 'lodash-es';
4
+ import type { SelectProps } from 'antd/es/select';
5
+ import ReactDOM from 'react-dom/client';
6
+ import { CacheUserInfo, UserInfo } from '../../utils/CacheUserInfo';
7
+
8
+ /******
9
+ 使用示例:
10
+
11
+ <UserSelector
12
+ value={selectedUserIds}
13
+ onChange={(userIds, users) => {
14
+ setSelectedUserIds(userIds);
15
+ }}
16
+ placeholder="请选择用户"
17
+ multiple={false}
18
+ userType="all"
19
+ excludeUserIds={[]}
20
+ showAvatar={true}
21
+ showUsername={true}
22
+ showNickname={false}
23
+ showPhone={false}
24
+ />
25
+
26
+ ****/
27
+
28
+ const { Text } = Typography;
29
+
30
+ export interface UserSelectorProps extends Omit<SelectProps, 'options' | 'value' | 'onChange'> {
31
+ // 是否允许多选
32
+ multiple?: boolean;
33
+ // 用户类型过滤:'admin' | 'user' | 'all'
34
+ userType?: 'admin' | 'user' | 'all';
35
+ // 需要排除的用户ID列表
36
+ excludeUserIds?: string[];
37
+ // 是否显示头像
38
+ showAvatar?: boolean;
39
+ // 是否显示用户名
40
+ showUsername?: boolean;
41
+ // 是否显示昵称
42
+ showNickname?: boolean;
43
+ // 是否显示手机号
44
+ showPhone?: boolean;
45
+ // 自定义搜索字段
46
+ searchFields?: ('username' | 'nickname' | 'phone')[];
47
+ // 每页加载数量
48
+ pageSize?: number;
49
+ // 是否允许清除选择
50
+ allowClear?: boolean;
51
+ // 选中的用户ID数组
52
+ value?: string[];
53
+ // 初始已选用户信息(用于回显头像/昵称,避免用户列表异步加载完成前显示为 ID)
54
+ initialUsers?: UserInfo[];
55
+ // 选中用户变化时的回调
56
+ onChange?: (userIds: string[], selectedUsers: UserInfo[]) => void;
57
+ }
58
+
59
+ const UserSelector: React.FC<UserSelectorProps> = ({
60
+ multiple = false,
61
+ userType = 'all',
62
+ excludeUserIds = [],
63
+ showAvatar = true,
64
+ showUsername = true,
65
+ showNickname = true,
66
+ showPhone = true,
67
+ searchFields = ['username', 'nickname', 'phone'],
68
+ pageSize = 10,
69
+ allowClear = true,
70
+ value,
71
+ initialUsers,
72
+ onChange,
73
+ ...props
74
+ }) => {
75
+ const [loading, setLoading] = useState(false);
76
+ const [options, setOptions] = useState<UserInfo[]>([]);
77
+ const [searchValue, setSearchValue] = useState('');
78
+ const [pageNum, setPageNum] = useState(1);
79
+ const [hasMore, setHasMore] = useState(true);
80
+ const [selectedUsers, setSelectedUsers] = useState<UserInfo[]>([]);
81
+ const optionsRef = useRef<UserInfo[]>([]);
82
+ const selectedUsersRef = useRef<UserInfo[]>([]);
83
+ const initialUsersRef = useRef<UserInfo[]>([]);
84
+
85
+ // 按 ID 查找用户:选项 → 已选 → 外部初始用户
86
+ const findUserById = useCallback((id: string): UserInfo | undefined => {
87
+ return optionsRef.current.find(user => user.id === id)
88
+ || selectedUsersRef.current.find(user => user.id === id)
89
+ || (initialUsersRef.current || []).find(user => user.id === id);
90
+ }, []);
91
+
92
+ useEffect(() => {
93
+ initialUsersRef.current = initialUsers || [];
94
+ }, [initialUsers]);
95
+
96
+ // 获取人员列表
97
+ const fetchUsers = useCallback(async (search: string|null) => {
98
+ try {
99
+ setLoading(true);
100
+
101
+ // 使用 getUserInfos 获取用户信息
102
+ const userInfosMap = await CacheUserInfo.getUserInfos({ keywords: (search ? search : ''), forceRefresh: false });
103
+
104
+ // 将 Map 转换为数组
105
+ let usersArray = Array.from(userInfosMap.values());
106
+
107
+ // 过滤管理员
108
+ usersArray = usersArray.filter(user => user.username !== 'admin');
109
+
110
+ // 根据用户类型过滤
111
+ if (userType === 'admin') {
112
+ usersArray = usersArray.filter(user => user.isAdmin);
113
+ } else if (userType === 'user') {
114
+ usersArray = usersArray.filter(user => !user.isAdmin);
115
+ }
116
+
117
+ // 搜索过滤
118
+ if (search) {
119
+ usersArray = usersArray.filter(user =>
120
+ searchFields.some(field => {
121
+ const value = user[field];
122
+ return value && value.toLowerCase().includes(search.toLowerCase());
123
+ })
124
+ );
125
+ }
126
+
127
+ // 排除用户
128
+ if (excludeUserIds.length > 0) {
129
+ usersArray = usersArray.filter(user => !excludeUserIds.includes(user.id));
130
+ }
131
+
132
+ // 按用户名排序
133
+ usersArray.sort((a, b) => (a.username || '').localeCompare(b.username || ''));
134
+
135
+ // 转换数据格式以匹配 UserInfo 接口
136
+ const transformedUsers: UserInfo[] = usersArray;
137
+
138
+ setOptions(prev => [...prev, ...transformedUsers]);
139
+ setHasMore(false); // getUserInfos 不支持分页,暂时设为 false
140
+ } catch (error) {
141
+ console.error('获取人员列表失败:', error);
142
+ message.error('获取人员列表失败,请稍后重试');
143
+ } finally {
144
+ setLoading(false);
145
+ }
146
+ }, [userType, excludeUserIds, searchFields]);
147
+
148
+
149
+ // 初始加载
150
+ useEffect(() => {
151
+ fetchUsers(null);
152
+ }, [fetchUsers]);
153
+
154
+ // 更新 refs
155
+ useEffect(() => {
156
+ optionsRef.current = options;
157
+ }, [options]);
158
+
159
+ useEffect(() => {
160
+ selectedUsersRef.current = selectedUsers;
161
+ }, [selectedUsers]);
162
+
163
+ // 处理选择变化
164
+ const handleChange = useCallback((newValue: string[] | null) => {
165
+ if (!newValue || newValue.length == 0) {
166
+ setSelectedUsers([]);
167
+ onChange?.([], []);
168
+ return;
169
+ }
170
+
171
+ // 根据模式处理值
172
+ const userIds = Array.isArray(newValue) ? newValue : [newValue];
173
+ let users = [] as UserInfo[];
174
+ for (let i = 0; i < userIds.length; i++) {
175
+ const id = userIds[i];
176
+ const found = findUserById(id);
177
+ if (found) {
178
+ users.push(found);
179
+ } else {
180
+ users.push({
181
+ id,
182
+ username: id,
183
+ nickname: '',
184
+ phone: '',
185
+ avatar: { avatar_small: '' }
186
+ } as UserInfo);
187
+ } // if..else.
188
+ } // for.
189
+
190
+ if (selectedUsers.length == users.length) {
191
+ let i = 0;
192
+ for (i = 0; i < users.length; i++) {
193
+ if (selectedUsers.find((value) => { return value.id == users[i].id ? value : null; })) {
194
+ break;
195
+ }
196
+ }
197
+ if (i < users.length) {
198
+ return;
199
+ }
200
+ } // if.
201
+
202
+ setSelectedUsers(users.slice());
203
+
204
+ onChange?.(userIds, users);
205
+ }, [onChange, selectedUsers]);
206
+
207
+ // 当外部 value 变化时更新选中状态
208
+ useEffect(() => {
209
+ // setTimeout(() => {
210
+ // console.log('外部 value 变化:', value, selectedUsers);
211
+ if (!value || value.length == 0) {
212
+ if (!selectedUsers || selectedUsers.length == 0) return;
213
+ setSelectedUsers([]);
214
+ return;
215
+ }
216
+
217
+ value.sort();
218
+ let selected = [] as string[];
219
+ if (selectedUsers) {
220
+ selected = selectedUsers.map(user => user.id);
221
+ selected.sort();
222
+ }
223
+ if (value.join(',') == selected.join(',')) return;
224
+
225
+ // 根据模式处理值
226
+ handleChange(value);
227
+ // }, 100);
228
+
229
+ }, [value, multiple, handleChange, selectedUsers]);
230
+
231
+ // 当 options 更新时,更新 selectedUsers 中的用户信息
232
+ useEffect(() => {
233
+ if (!value || value.length == 0) return;
234
+
235
+ const userIds = value;
236
+
237
+ const users = userIds.map(id => {
238
+ const found = findUserById(id);
239
+ if (found) return found;
240
+
241
+ return {
242
+ id,
243
+ username: id,
244
+ nickname: '',
245
+ phone: '',
246
+ type: null,
247
+ } as any;
248
+ }).filter((user): user is UserInfo => user !== undefined);
249
+
250
+ // 只在有变化时更新
251
+ setSelectedUsers(prev => {
252
+ if (prev.length !== users.length || prev.some((u, i) => u.id !== users[i]?.id || u.username !== users[i]?.username)) {
253
+ return users;
254
+ }
255
+ return prev;
256
+ });
257
+ }, [options, value, initialUsers, findUserById]);
258
+
259
+ // 搜索防抖
260
+ const debouncedSearch = useMemo(
261
+ () => debounce((value: string) => {
262
+ setSearchValue(value);
263
+ setPageNum(1);
264
+ fetchUsers(value);
265
+ }, 300),
266
+ [fetchUsers]
267
+ );
268
+
269
+ // 滚动加载更多
270
+ const handlePopupScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
271
+ const { target } = e;
272
+ if (!target) return;
273
+
274
+ const { scrollTop, scrollHeight, clientHeight } = target as HTMLDivElement;
275
+ if (scrollHeight - scrollTop <= clientHeight * 1.5 && !loading && hasMore) {
276
+ const nextPage = pageNum + 1;
277
+ setPageNum(nextPage);
278
+ fetchUsers(searchValue);
279
+ }
280
+ }, [loading, hasMore, pageNum, searchValue, fetchUsers]);
281
+
282
+ // 渲染用户信息卡片
283
+ const renderUserInfoCard = (user: UserInfo) => {
284
+ return (
285
+ <Card size="small" style={{ width: 280 }}>
286
+ <Space direction="vertical" size="small" style={{ width: '100%' }}>
287
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
288
+ <Avatar
289
+ src={user?.avatar?.avatar_small}
290
+ size={40}
291
+ >
292
+ {user.nickname?.[0] || user.username?.[0]}
293
+ </Avatar>
294
+ <div>
295
+ <div style={{ fontWeight: 'bold' }}>{user.nickname || user.username}</div>
296
+ {user.title && (
297
+ <Text type="secondary" style={{ fontSize: 12 }}>{user.title}</Text>
298
+ )}
299
+ </div>
300
+ </div>
301
+ {user.phone && (
302
+ <div>
303
+ <Text type="secondary" style={{ fontSize: 12 }}>手机号:</Text>
304
+ <Text style={{ fontSize: 12 }}>{user.phone}</Text>
305
+ </div>
306
+ )}
307
+ {user.departmentInfo?.department_names && (
308
+ <div>
309
+ <Text type="secondary" style={{ fontSize: 12 }}>所属部门:</Text>
310
+ <Text style={{ fontSize: 12 }}>
311
+ {user.departmentInfo?.department_names.join('、')}
312
+ </Text>
313
+ </div>
314
+ )}
315
+ </Space>
316
+ </Card>
317
+ );
318
+ };
319
+
320
+ // 渲染选项
321
+ const renderOption = useMemo(() => (user: UserInfo) => {
322
+ const parts = [];
323
+ if (showAvatar) {
324
+ parts.push(
325
+ <Avatar
326
+ key="avatar"
327
+ src={user?.avatar?.avatar_small}
328
+ size={20}
329
+ style={{
330
+ marginRight: 8,
331
+ verticalAlign: 'middle',
332
+ flexShrink: 0
333
+ }}
334
+ >
335
+ {user.nickname?.[0] || user.username?.[0]}
336
+ </Avatar>
337
+ );
338
+ }
339
+ if (showNickname && user.nickname) {
340
+ parts.push(
341
+ <span key="nickname" style={{
342
+ verticalAlign: 'middle',
343
+ lineHeight: '20px'
344
+ }}>
345
+ {user.nickname}
346
+ </span>
347
+ );
348
+ }
349
+ if (showUsername) {
350
+ parts.push(
351
+ <span key="username" style={{
352
+ marginLeft: 8,
353
+ color: 'rgb(var(--color-gray-400))',
354
+ verticalAlign: 'middle',
355
+ lineHeight: '20px'
356
+ }}>
357
+ ({user.username})
358
+ </span>
359
+ );
360
+ }
361
+ if (showPhone && user.phone) {
362
+ parts.push(
363
+ <span key="phone" style={{
364
+ marginLeft: 8,
365
+ color: 'rgb(var(--color-gray-400))',
366
+ verticalAlign: 'middle',
367
+ lineHeight: '20px'
368
+ }}>
369
+ {user.phone}
370
+ </span>
371
+ );
372
+ }
373
+ return (
374
+ <div style={{
375
+ display: 'flex',
376
+ alignItems: 'center',
377
+ minHeight: '24px'
378
+ }}>
379
+ {parts}
380
+ </div>
381
+ );
382
+ }, [showAvatar, showUsername, showNickname, showPhone]);
383
+
384
+ // 使用 useMemo 缓存选项数据
385
+ const selectOptions = useMemo(() => {
386
+ const allUsers = [...options, ...selectedUsers];
387
+ const uniqueUsers = allUsers.filter((user, index, self) =>
388
+ self.findIndex(u => u.id === user.id) === index
389
+ );
390
+ return uniqueUsers.map(user => ({
391
+ label: renderOption(user),
392
+ value: user.id,
393
+ user, // 将用户信息存储在选项中
394
+ }));
395
+ },
396
+ [options, selectedUsers, renderOption]
397
+ );
398
+
399
+ // 自定义标签渲染
400
+ const customTagRender = useCallback((props: any): ReactElement => {
401
+ const { value, closable, onClose } = props;
402
+ const user = options.find(u => u.id === value)
403
+ || selectedUsers.find(u => u.id === value)
404
+ || (initialUsers || []).find(u => u.id === value);
405
+
406
+ if (!user) {
407
+ return <div>{value}</div>;
408
+ }
409
+
410
+ return (
411
+ <Popover
412
+ content={renderUserInfoCard(user)}
413
+ trigger="click"
414
+ placement="right"
415
+ >
416
+ <div style={{
417
+ display: 'flex',
418
+ alignItems: 'center',
419
+ cursor: 'pointer',
420
+ padding: '2px 8px',
421
+ background: 'rgb(var(--color-gray-100))',
422
+ borderRadius: 4,
423
+ margin: '2px 4px',
424
+ border: '1px solid rgb(var(--color-border))'
425
+ }}>
426
+ <Avatar
427
+ src={user?.avatar?.avatar_small}
428
+ size={20}
429
+ style={{ marginRight: 8 }}
430
+ >
431
+ {user.nickname?.[0] || user.username?.[0]}
432
+ </Avatar>
433
+ <span style={{ marginRight: 8 }}>{user.nickname || user.username}</span>
434
+ {closable && (
435
+ <span
436
+ onClick={onClose}
437
+ style={{
438
+ cursor: 'pointer',
439
+ color: 'rgb(var(--color-gray-400))',
440
+ fontSize: 12,
441
+ padding: '0 4px'
442
+ }}
443
+ >
444
+ ×
445
+ </span>
446
+ )}
447
+ </div>
448
+ </Popover>
449
+ );
450
+ }, [options, selectedUsers, initialUsers]);
451
+
452
+ // 自定义选中值的渲染
453
+ const customSingleValueRender = useCallback((value: string): ReactElement => {
454
+ const user = options.find(u => u.id === value)
455
+ || selectedUsers.find(u => u.id === value)
456
+ || (initialUsers || []).find(u => u.id === value);
457
+
458
+ if (!user) {
459
+ return <div>{value}</div>;
460
+ }
461
+
462
+ return (
463
+ <Popover
464
+ content={renderUserInfoCard(user)}
465
+ trigger="click"
466
+ placement="right"
467
+ >
468
+ <div style={{
469
+ display: 'flex',
470
+ alignItems: 'center',
471
+ cursor: 'pointer',
472
+ height: '100%'
473
+ }}>
474
+ <Avatar
475
+ src={user?.avatar?.avatar_small}
476
+ size={20}
477
+ style={{ marginRight: 8 }}
478
+ >
479
+ {user.nickname?.[0] || user.username?.[0]}
480
+ </Avatar>
481
+ <span>{user.nickname || user.username}</span>
482
+ </div>
483
+ </Popover>
484
+ );
485
+ }, [options, selectedUsers, initialUsers]);
486
+
487
+ // 获取当前选中的用户
488
+ const getCurrentUser = useCallback((currentValue:string[] | null | undefined) => {
489
+ if (!currentValue || currentValue.length == 0) return null;
490
+ const userId = currentValue[0];
491
+ return options.find(u => u.id === userId)
492
+ || selectedUsers.find(u => u.id === userId)
493
+ || (initialUsers || []).find(u => u.id === userId);
494
+ }, [options, selectedUsers, initialUsers]);
495
+
496
+ return (
497
+ <Select
498
+ {...props}
499
+ mode={multiple ? 'multiple' : undefined}
500
+ showSearch
501
+ loading={loading}
502
+ filterOption={false}
503
+ onSearch={debouncedSearch}
504
+ onPopupScroll={handlePopupScroll}
505
+ notFoundContent={loading ? <Spin size="small" /> : null}
506
+ allowClear={allowClear}
507
+ value={value}
508
+ onChange={handleChange}
509
+ options={selectOptions}
510
+ style={{
511
+ ...props.style,
512
+ minHeight: '32px'
513
+ }}
514
+ tagRender={multiple ? customTagRender : undefined}
515
+ fieldNames={{
516
+ label: 'label',
517
+ value: 'value',
518
+ }}
519
+ // 自定义选中值的渲染
520
+ popupRender={multiple ? undefined : (menu) => {
521
+ const currentUser = getCurrentUser(value);
522
+ if (!currentUser) return menu;
523
+
524
+ return (
525
+ <div>
526
+ <div style={{
527
+ padding: '8px 12px',
528
+ borderBottom: '1px solid rgb(var(--color-border))',
529
+ cursor: 'pointer'
530
+ }}>
531
+ {customSingleValueRender(currentUser.id)}
532
+ </div>
533
+ {menu}
534
+ </div>
535
+ );
536
+ }}
537
+ // 自定义选中值的显示
538
+ suffixIcon={multiple ? undefined : (
539
+ value ? (
540
+ <div
541
+ onClick={(e) => {
542
+ e.stopPropagation();
543
+ const currentUser = getCurrentUser(value);
544
+ if (currentUser) {
545
+ // 创建一个临时的 Popover 实例
546
+ const popover = document.createElement('div');
547
+ document.body.appendChild(popover);
548
+ const root = ReactDOM.createRoot(popover);
549
+ root.render(
550
+ <Popover
551
+ content={renderUserInfoCard(currentUser)}
552
+ trigger="click"
553
+ placement="right"
554
+ open={true}
555
+ onOpenChange={(visible) => {
556
+ if (!visible) {
557
+ root.unmount();
558
+ document.body.removeChild(popover);
559
+ }
560
+ }}
561
+ >
562
+ <div style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%' }} />
563
+ </Popover>
564
+ );
565
+ }
566
+ }}
567
+ style={{
568
+ cursor: 'pointer',
569
+ padding: '0 8px',
570
+ height: '100%',
571
+ display: 'flex',
572
+ alignItems: 'center'
573
+ }}
574
+ >
575
+ <span className="ant-select-arrow" />
576
+ </div>
577
+ ) : <span className="ant-select-arrow" />
578
+ )}
579
+ />
580
+ );
581
+ };
582
+
583
+ export default UserSelector;
@@ -0,0 +1,84 @@
1
+ // UI 公共组件桶导出
2
+ export * from './UIAlert';
3
+ export * from './UIAlertExample';
4
+ export * from './UIConfirm';
5
+ export * from './UIConfirmExample';
6
+ export * from './UIDatePicker';
7
+ export * from './UIDateRangePicker';
8
+ export * from './UIImageUpload';
9
+ export * from './UILoading';
10
+ export * from './UILoadingExample';
11
+ export * from './UISelect';
12
+ export * from './UIToast';
13
+ export * from './UIToastExample';
14
+ export * from './UIToastSingle';
15
+ export * from './UIViewToggle';
16
+ // 带内部状态/缓存的选择器
17
+ export * from './CommentTimeline';
18
+ export * from './ProjectSelector';
19
+ export * from './RoleSelector';
20
+ export * from './UserSelector';
21
+ // 重量级组件(文件管理、预览、发票、上传)
22
+ export * from './FileManager';
23
+ export * from './FileManager/types';
24
+ export * from './UIFilePreview';
25
+ export * from './InvoiceRecordsList';
26
+ export * from './MyFileSelectorModal';
27
+ export * from './UIFileUpload';
28
+
29
+ // 仅以 default 方式导出的组件,这里再以具名形式暴露,供模块统一从包入口导入
30
+ export { default as uiAlert } from './UIAlert';
31
+ export { default as uiConfirm } from './UIConfirm';
32
+ export { default as uiToast } from './UIToast';
33
+ export { default as UserSelector } from './UserSelector';
34
+ export { default as ProjectSelector } from './ProjectSelector';
35
+ export { default as RoleSelector } from './RoleSelector';
36
+
37
+ // 文件上传能力
38
+ export { FileUploadHandler } from './upload';
39
+ export { compressImage } from './upload/image';
40
+
41
+ // 文件预览工具函数
42
+ export {
43
+ fetchFileBuffer,
44
+ downloadFile,
45
+ downloadBlob,
46
+ getFileUrl,
47
+ getFilePreviewType,
48
+ getFileIcon,
49
+ ARCHIVE_FILE_EXTENSIONS,
50
+ CAD_FILE_EXTENSIONS,
51
+ } from './UIFilePreview/previewUtils';
52
+ export { default as MarkdownEditor } from './UIFilePreview/MarkdownEditor';
53
+
54
+ // 文件树 / 文件工具
55
+ export {
56
+ buildFileTree,
57
+ sortFiles,
58
+ sortTree,
59
+ getDescendantIds,
60
+ getDescendantIdsOptimized,
61
+ getFileDescendants,
62
+ } from './FileManager/utils/fileTree';
63
+ export {
64
+ formatFileSize,
65
+ isEditableFileType,
66
+ getDisplayName,
67
+ CONTRACTS_VIRTUAL_FOLDER_ID,
68
+ NOTES_ROOT_FOLDER_NAME,
69
+ isNotesRootName,
70
+ } from './FileManager/utils/fileUtils';
71
+ export {
72
+ getMimeType,
73
+ sanitizeFileName,
74
+ stripFileExtension,
75
+ calculateCRC32,
76
+ uploadFileBlob,
77
+ uploadMarkdownContent,
78
+ uploadFileDelta,
79
+ uploadMarkdownDelta,
80
+ extractFilesFromDrop,
81
+ } from './FileManager/utils/fileUpload';
82
+
83
+ // 角色缓存
84
+ export { clearRoleCache } from './RoleSelector/roleCache';