@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,472 @@
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 { ResponseCode } from '../../api/_base';
6
+ import ReactDOM from 'react-dom/client';
7
+ import { API } from '../../api/index';
8
+ import { Project } from '../../types/project/index';
9
+ import { projectCacheManager } from './projectCache';
10
+
11
+ /******
12
+ 使用示例:
13
+
14
+ <ProjectSelector
15
+ value={selectedRoleIds}
16
+ onChange={(projectIds, projects) => {
17
+ setSelectedRoleIds(projectIds);
18
+ }}`
19
+ placeholder="请选择项目"
20
+ multiple={false}
21
+ excludeProjectIds={[]}
22
+ />
23
+
24
+ ****/
25
+
26
+
27
+ const { Text } = Typography;
28
+
29
+ export interface ProjectSelectorProps extends Omit<SelectProps, 'options' | 'value' | 'onChange'> {
30
+ // 是否允许多选
31
+ multiple?: boolean;
32
+ // 需要排除的项目ID列表
33
+ excludeProjectIds?: string[];
34
+ // 是否允许清除选择
35
+ allowClear?: boolean;
36
+ // 请求超时时间(毫秒)
37
+ timeout?: number;
38
+ // 请求重试次数
39
+ retryCount?: number;
40
+ // 是否启用缓存
41
+ enableCache?: boolean;
42
+ // 缓存过期时间(毫秒)
43
+ cacheExpireTime?: number;
44
+ // 选中的项目ID数组
45
+ value?: string[];
46
+ // 选中项目变化时的回调
47
+ onChange?: (projectIds: string[], selectedProjects: Project[]) => void;
48
+ }
49
+
50
+ const ProjectSelector: React.FC<ProjectSelectorProps> = ({
51
+ multiple = false,
52
+ allowClear = true,
53
+ timeout = 10000,
54
+ retryCount = 3,
55
+ enableCache = true,
56
+ excludeProjectIds = [],
57
+ cacheExpireTime,
58
+ value,
59
+ onChange,
60
+ ...props
61
+ }) => {
62
+ const [loading, setLoading] = useState(false);
63
+ const [options, setOptions] = useState<Project[]>([]);
64
+ const [searchValue, setSearchValue] = useState('');
65
+ const [pageNum, setPageNum] = useState(1);
66
+ const [hasMore, setHasMore] = useState(true);
67
+ const [selectedProjects, setSelectedProjects] = useState<Project[]>([]);
68
+ const abortControllerRef = useRef<AbortController | null>(null);
69
+ const optionsRef = useRef<Project[]>([]);
70
+ const selectedProjectsRef = useRef<Project[]>([]);
71
+
72
+ // 更新 refs
73
+ useEffect(() => {
74
+ optionsRef.current = options;
75
+ }, [options]);
76
+
77
+ useEffect(() => {
78
+ selectedProjectsRef.current = selectedProjects;
79
+ }, [selectedProjects]);
80
+
81
+ // 根据选中的用户ID获取完整的用户信息
82
+ const getSelectedProjectsInfo = useCallback((projectIds: string[]) => {
83
+ return projectIds.map(id => {
84
+ // 先从已加载的选项中查找
85
+ const found = optionsRef.current.find(user => user.id === id);
86
+ if (found) return found;
87
+
88
+ // 如果选项中没有,则从已选中的用户中查找
89
+ return selectedProjectsRef.current.find(user => user.id === id);
90
+ }).filter((user): user is Project => user !== undefined);
91
+ }, []); // 移除 options 和 selectedProjects 依赖
92
+
93
+ // 处理选择变化
94
+ const handleChange = useCallback((newValue: string | string[] | null) => {
95
+ if (!newValue) {
96
+ setSelectedProjects([]);
97
+ onChange?.([], []);
98
+ return;
99
+ }
100
+
101
+ // 根据模式处理值
102
+ const projectIds = multiple
103
+ ? (Array.isArray(newValue) ? newValue : [newValue])
104
+ : (typeof newValue === 'string' ? [newValue] : []);
105
+
106
+ if (projectIds.length === 0) {
107
+ setSelectedProjects([]);
108
+ onChange?.([], []);
109
+ return;
110
+ }
111
+
112
+ const users = getSelectedProjectsInfo(projectIds);
113
+ setSelectedProjects(users);
114
+ onChange?.(projectIds, users);
115
+ }, [getSelectedProjectsInfo, onChange, multiple]);
116
+
117
+ // 当外部 value 变化时更新选中状态
118
+ useEffect(() => {
119
+ if (!value || value.length == 0) {
120
+ setSelectedProjects([]);
121
+ return;
122
+ }
123
+
124
+ // 根据模式处理值
125
+ const projectIds = multiple
126
+ ? (Array.isArray(value) ? value : [])
127
+ : (typeof value === 'string' ? [value] : []);
128
+
129
+ const users = getSelectedProjectsInfo(projectIds);
130
+ setSelectedProjects(users);
131
+ }, [value, getSelectedProjectsInfo, multiple]);
132
+
133
+ // 带重试的请求函数
134
+ const fetchWithRetry = useCallback(async (fn: () => Promise<any>, retries: number = retryCount): Promise<any> => {
135
+ try {
136
+ // 创建新的 AbortController
137
+ abortControllerRef.current = new AbortController();
138
+ const signal = abortControllerRef.current.signal;
139
+
140
+ // 设置超时
141
+ const timeoutPromise = new Promise((_, reject) => {
142
+ setTimeout(() => {
143
+ reject(new Error('请求超时'));
144
+ abortControllerRef.current?.abort();
145
+ }, timeout);
146
+ });
147
+
148
+ // 执行请求
149
+ const result = await Promise.race([
150
+ fn(),
151
+ timeoutPromise,
152
+ ]);
153
+
154
+ return result;
155
+ } catch (error) {
156
+ if (retries > 0 && !(error instanceof Error && error.name === 'AbortError')) {
157
+ // 如果不是取消请求,则重试
158
+ await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒后重试
159
+ return fetchWithRetry(fn, retries - 1);
160
+ }
161
+ throw error;
162
+ }
163
+ }, [timeout, retryCount]);
164
+
165
+ // 获项目列表
166
+ const fetchProjects = useCallback(async (search: string, append: boolean = false) => {
167
+ try {
168
+ setLoading(true);
169
+
170
+ // 生成缓存key
171
+ const cacheKey = `projects_${search}`;
172
+
173
+ // 检查缓存
174
+ if (enableCache && !search) {
175
+ const cachedData = projectCacheManager.get(cacheKey);
176
+ if (cachedData) {
177
+ setOptions(prev => append ? [...prev, ...cachedData] : cachedData);
178
+ setHasMore(false);
179
+ return;
180
+ }
181
+ }
182
+
183
+ const res = await fetchWithRetry(async () => {
184
+ const result = await API.project.project.listProjects({ pageNum, pageSize: 1000 });
185
+ if (result.code === ResponseCode.SUCCESS && result.data) {
186
+ return {
187
+ ...result,
188
+ data: result.data.list.map((admin) => ({
189
+ id: admin.id,
190
+ name: admin.name,
191
+ descs: admin.descs,
192
+ })),
193
+ };
194
+ }
195
+
196
+ return result;
197
+ });
198
+
199
+ if (res.code === ResponseCode.SUCCESS && res.data) {
200
+ const filteredUsers = res.data.filter((user: Project) => {
201
+ // 过滤排除的用户
202
+ if (excludeProjectIds.includes(user.id)) {
203
+ return false;
204
+ }
205
+ return true;
206
+ });
207
+
208
+ // 更新缓存
209
+ if (enableCache && !search) {
210
+ projectCacheManager.set(cacheKey, filteredUsers, cacheExpireTime);
211
+ }
212
+
213
+ setOptions(prev => append ? [...prev, ...filteredUsers] : filteredUsers);
214
+ setHasMore(false);
215
+ }
216
+ } catch (error) {
217
+ console.error('获取项目列表失败:', error);
218
+ message.error('获取项目列表失败,请稍后重试');
219
+ } finally {
220
+ setLoading(false);
221
+ }
222
+ }, [excludeProjectIds, fetchWithRetry, pageNum, enableCache, cacheExpireTime]);
223
+
224
+ // 组件卸载时取消未完成的请求
225
+ useEffect(() => {
226
+ return () => {
227
+ abortControllerRef.current?.abort();
228
+ };
229
+ }, []);
230
+
231
+ // 搜索防抖
232
+ const debouncedSearch = useMemo(
233
+ () => debounce((value: string) => {
234
+ setSearchValue(value);
235
+ setPageNum(1);
236
+ fetchProjects(value, false);
237
+ }, 300),
238
+ [fetchProjects]
239
+ );
240
+
241
+ // 滚动加载更多
242
+ const handlePopupScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
243
+ const { target } = e;
244
+ if (!target) return;
245
+
246
+ const { scrollTop, scrollHeight, clientHeight } = target as HTMLDivElement;
247
+ if (scrollHeight - scrollTop <= clientHeight * 1.5 && !loading && hasMore) {
248
+ const nextPage = pageNum + 1;
249
+ setPageNum(nextPage);
250
+ fetchProjects(searchValue, true);
251
+ }
252
+ }, [loading, hasMore, pageNum, searchValue, fetchProjects]);
253
+
254
+ // 初始加载
255
+ useEffect(() => {
256
+ fetchProjects('', false);
257
+ }, [fetchProjects]);
258
+
259
+ // 渲染用户信息卡片
260
+ const renderRoleInfoCard = (role: Project) => {
261
+ return (
262
+ <Card size="small" style={{ width: 280 }}>
263
+ <Space direction="vertical" size="small" style={{ width: '100%' }}>
264
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
265
+ <div>
266
+ <div style={{ fontWeight: 'bold' }}>{role.name}</div>
267
+ </div>
268
+ </div>
269
+ </Space>
270
+ </Card>
271
+ );
272
+ };
273
+
274
+ // 渲染选项
275
+ const renderOption = (user: Project) => {
276
+ const parts = [];
277
+ parts.push(
278
+ <span key="name" style={{
279
+ verticalAlign: 'middle',
280
+ lineHeight: '20px'
281
+ }}>
282
+ {user.name}
283
+ </span>
284
+ );
285
+
286
+ return (
287
+ <div style={{
288
+ display: 'flex',
289
+ alignItems: 'center',
290
+ minHeight: '24px'
291
+ }}>
292
+ {parts}
293
+ </div>
294
+ );
295
+ };
296
+
297
+ // 使用 useMemo 缓存选项数据
298
+ const selectOptions = useMemo(() =>
299
+ options.map(user => ({
300
+ key: user.id,
301
+ label: renderOption(user),
302
+ value: user.id,
303
+ user, // 将用户信息存储在选项中
304
+ })),
305
+ [options]
306
+ );
307
+
308
+ // 自定义标签渲染
309
+ const customTagRender = useCallback((props: any): ReactElement => {
310
+ const { value, closable, onClose } = props;
311
+ const user = options.find(u => u.id === value) || selectedProjects.find(u => u.id === value);
312
+
313
+ if (!user) {
314
+ return <div>{value}</div>;
315
+ }
316
+
317
+ return (
318
+ <Popover
319
+ content={renderRoleInfoCard(user)}
320
+ trigger="click"
321
+ placement="right"
322
+ >
323
+ <div style={{
324
+ display: 'flex',
325
+ alignItems: 'center',
326
+ cursor: 'pointer',
327
+ padding: '2px 8px',
328
+ background: 'rgb(var(--color-gray-100))',
329
+ borderRadius: 4,
330
+ margin: '2px 4px',
331
+ border: '1px solid rgb(var(--color-border))'
332
+ }}>
333
+ <span style={{ marginRight: 8 }}>{user.name}</span>
334
+ {closable && (
335
+ <span
336
+ onClick={onClose}
337
+ style={{
338
+ cursor: 'pointer',
339
+ color: 'rgb(var(--color-gray-400))',
340
+ fontSize: 12,
341
+ padding: '0 4px'
342
+ }}
343
+ >
344
+ ×
345
+ </span>
346
+ )}
347
+ </div>
348
+ </Popover>
349
+ );
350
+ }, [options, selectedProjects]);
351
+
352
+ // 自定义选中值的渲染
353
+ const customSingleValueRender = useCallback((value: string): ReactElement => {
354
+ const user = options.find(u => u.id === value) || selectedProjects.find(u => u.id === value);
355
+
356
+ if (!user) {
357
+ return <div>{value}</div>;
358
+ }
359
+
360
+ return (
361
+ <Popover
362
+ content={renderRoleInfoCard(user)}
363
+ trigger="click"
364
+ placement="right"
365
+ >
366
+ <div style={{
367
+ display: 'flex',
368
+ alignItems: 'center',
369
+ cursor: 'pointer',
370
+ height: '100%'
371
+ }}>
372
+ <span>{user.name}</span>
373
+ </div>
374
+ </Popover>
375
+ );
376
+ }, [options, selectedProjects]);
377
+
378
+ // 获取当前选中的用户
379
+ const getCurrentRole = useCallback((currentValue: string | string[] | null | undefined) => {
380
+ if (!currentValue) return null;
381
+ const userId = Array.isArray(currentValue) ? currentValue[0] : currentValue;
382
+ return options.find(u => u.id === userId) || selectedProjects.find(u => u.id === userId);
383
+ }, [options, selectedProjects]);
384
+
385
+ return (
386
+ <Select
387
+ {...props}
388
+ mode={multiple ? 'multiple' : undefined}
389
+ showSearch
390
+ loading={loading}
391
+ filterOption={false}
392
+ onSearch={debouncedSearch}
393
+ onPopupScroll={handlePopupScroll}
394
+ notFoundContent={loading ? <Spin size="small" /> : null}
395
+ allowClear={allowClear}
396
+ value={multiple ? value : (typeof value === 'string' ? value : value?.toString())}
397
+ onChange={handleChange}
398
+ options={selectOptions}
399
+ style={{
400
+ ...props.style,
401
+ minHeight: '32px'
402
+ }}
403
+ tagRender={multiple ? customTagRender : undefined}
404
+ fieldNames={{
405
+ label: 'label',
406
+ value: 'value',
407
+ }}
408
+ // 自定义选中值的渲染
409
+ popupRender={multiple ? undefined : (menu) => {
410
+ const currentUser = getCurrentRole(value);
411
+ if (!currentUser) return menu;
412
+
413
+ return (
414
+ <div>
415
+ <div style={{
416
+ padding: '8px 12px',
417
+ borderBottom: '1px solid rgb(var(--color-border))',
418
+ cursor: 'pointer'
419
+ }}>
420
+ {customSingleValueRender(currentUser.id)}
421
+ </div>
422
+ {menu}
423
+ </div>
424
+ );
425
+ }}
426
+ // 自定义选中值的显示
427
+ suffixIcon={multiple ? undefined : (
428
+ value ? (
429
+ <div
430
+ onClick={(e) => {
431
+ e.stopPropagation();
432
+ const currentUser = getCurrentRole(value);
433
+ if (currentUser) {
434
+ // 创建一个临时的 Popover 实例
435
+ const popover = document.createElement('div');
436
+ document.body.appendChild(popover);
437
+ const root = ReactDOM.createRoot(popover);
438
+ root.render(
439
+ <Popover
440
+ content={renderRoleInfoCard(currentUser)}
441
+ trigger="click"
442
+ placement="right"
443
+ open={true}
444
+ onOpenChange={(visible) => {
445
+ if (!visible) {
446
+ root.unmount();
447
+ document.body.removeChild(popover);
448
+ }
449
+ }}
450
+ >
451
+ <div style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%' }} />
452
+ </Popover>
453
+ );
454
+ }
455
+ }}
456
+ style={{
457
+ cursor: 'pointer',
458
+ padding: '0 8px',
459
+ height: '100%',
460
+ display: 'flex',
461
+ alignItems: 'center'
462
+ }}
463
+ >
464
+ <span className="ant-select-arrow" />
465
+ </div>
466
+ ) : <span className="ant-select-arrow" />
467
+ )}
468
+ />
469
+ );
470
+ };
471
+
472
+ export default ProjectSelector;
@@ -0,0 +1,40 @@
1
+ interface CacheItem {
2
+ data: any;
3
+ timestamp: number;
4
+ expireTime: number;
5
+ }
6
+
7
+ class CacheManager {
8
+ private cache: Map<string, CacheItem> = new Map();
9
+ private readonly defaultExpireTime = 5 * 60 * 1000;
10
+
11
+ set(key: string, data: any, expireTime: number = this.defaultExpireTime) {
12
+ this.cache.set(key, {
13
+ data,
14
+ timestamp: Date.now(),
15
+ expireTime,
16
+ });
17
+ }
18
+
19
+ get(key: string): any | null {
20
+ const item = this.cache.get(key);
21
+ if (!item) return null;
22
+
23
+ if (Date.now() - item.timestamp > item.expireTime) {
24
+ this.cache.delete(key);
25
+ return null;
26
+ }
27
+
28
+ return item.data;
29
+ }
30
+
31
+ clear() {
32
+ this.cache.clear();
33
+ }
34
+ }
35
+
36
+ export const projectCacheManager = new CacheManager();
37
+
38
+ export function clearProjectCache() {
39
+ projectCacheManager.clear();
40
+ }