@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,535 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { message } from 'antd';
3
+ import { PptxViewer } from '@file-viewer/pptx';
4
+ import { withPolyfillWorkerBootstrap } from '../../../utils/workerBootstrap';
5
+ import { exportPptxPdf } from './pptExportPdf';
6
+ import { ExportDropdown } from '../ExportDropdown';
7
+ import { downloadBlob } from '../previewUtils';
8
+
9
+ export interface PptPreviewProps {
10
+ file: File | null;
11
+ }
12
+
13
+ export const PptPreview: React.FC<PptPreviewProps> = ({ file }) => {
14
+ const [isLoading, setIsLoading] = useState(true);
15
+ const [hasError, setHasError] = useState(false);
16
+ const [reloadTick, setReloadTick] = useState(0);
17
+ const [exporting, setExporting] = useState(false);
18
+ // 大文件加载反馈:
19
+ // - stage='parsing':worker 正在解压/解析 PPTX(大文件此阶段可能持续很久且此前只有转圈)
20
+ // - parsePercent:worker 上报的解析进度(0-100),parsedSlides:已解析幻灯片序号
21
+ // - stage='rendering':解析完成后等待首页幻灯片渲染出来
22
+ const [stage, setStage] = useState<'parsing' | 'rendering'>('parsing');
23
+ const [parsePercent, setParsePercent] = useState(0);
24
+ const [parsedSlides, setParsedSlides] = useState(0);
25
+ const [renderedSlides, setRenderedSlides] = useState(0);
26
+ const [totalSlides, setTotalSlides] = useState(0);
27
+
28
+ // 离屏重新以 1:1 尺寸渲染全部幻灯片后逐页光栅化,直接下载 PDF(无打印对话框)。
29
+ const handleExportPdf = useCallback(async () => {
30
+ if (!file) {
31
+ message.warning('文档尚未加载完成,请稍后再试');
32
+ return;
33
+ }
34
+ setExporting(true);
35
+ message.loading({ key: 'ppt-export-pdf', content: '正在准备演示文稿…', duration: 0 });
36
+ try {
37
+ await exportPptxPdf({
38
+ file,
39
+ fileName: file.name,
40
+ onProgress: (page, total) => {
41
+ message.loading({ key: 'ppt-export-pdf', content: `正在导出第 ${page} / ${total} 页…`, duration: 0 });
42
+ },
43
+ });
44
+ } catch (error) {
45
+ console.error('PPT 导出 PDF 失败:', error);
46
+ message.error(`导出 PDF 失败:${(error as Error)?.message || '请重试'}`);
47
+ } finally {
48
+ message.destroy('ppt-export-pdf');
49
+ setExporting(false);
50
+ }
51
+ }, [file]);
52
+
53
+ // 下载原始文件:只读预览拿到的 File 就是原始文件
54
+ const handleDownload = useCallback(() => {
55
+ if (!file) return;
56
+ downloadBlob(file, file.name);
57
+ }, [file]);
58
+
59
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
60
+ const targetRef = useRef<HTMLDivElement>(null);
61
+ const viewerRef = useRef<PptxViewer | null>(null);
62
+ const slideSizeRef = useRef<{ width?: number; height?: number } | null>(null);
63
+ const fittedRef = useRef(false);
64
+
65
+ /**
66
+ * 单页显示:只让"视觉中心最接近容器中心"的 slide 可见,
67
+ * 其余 slide 一律 opacity:0(由 CSS 控制)。滚动/键盘翻页时随滚动实时切换,
68
+ * 配合 CSS opacity 过渡形成翻页淡入淡出,任何窗口比例下都只看到一页。
69
+ * 注意:导出 PDF 使用独立离屏渲染器,其 slide 不在 .ppt-scroll-container 内,
70
+ * 不受这里的 class 影响。
71
+ */
72
+ const updateActiveSlide = useCallback(() => {
73
+ const container = scrollContainerRef.current;
74
+ if (!container) return;
75
+ const slides = Array.from(
76
+ container.querySelectorAll<HTMLElement>('.flyfish-pptx-content .slide'),
77
+ );
78
+ if (slides.length === 0) return;
79
+
80
+ const viewportCenter =
81
+ container.getBoundingClientRect().top + container.clientHeight / 2;
82
+
83
+ let activeIndex = 0;
84
+ let closestDist = Number.POSITIVE_INFINITY;
85
+ slides.forEach((slide, i) => {
86
+ const r = slide.getBoundingClientRect();
87
+ const d = Math.abs(r.top + r.height / 2 - viewportCenter);
88
+ if (d < closestDist) {
89
+ closestDist = d;
90
+ activeIndex = i;
91
+ }
92
+ });
93
+
94
+ slides.forEach((slide, i) => {
95
+ slide.classList.toggle('ppt-slide-active', i === activeIndex);
96
+ });
97
+ }, []);
98
+
99
+ /**
100
+ * 让幻灯片完整显示在可视区域内(contain 模式),并给每个 slide 加间距
101
+ * 让它们一页一页排列:每页视觉高度 = 100dvh,slide 在页内居中。
102
+ */
103
+ const layoutSlides = useCallback(async () => {
104
+ const viewer = viewerRef.current;
105
+ const container = scrollContainerRef.current;
106
+ const size = slideSizeRef.current;
107
+ if (!viewer || !container || !size) return;
108
+
109
+ const slideW = size.width ?? 1;
110
+ const slideH = size.height ?? 1;
111
+
112
+ // contain 模式缩放比
113
+ const scaleW = container.clientWidth / slideW;
114
+ const scaleH = container.clientHeight / slideH;
115
+ const scale = Math.min(scaleW, scaleH);
116
+ await viewer.setZoom(scale * 100);
117
+
118
+ // 每页的"布局高度"(content 坐标系里的像素)= 100dvh / scale
119
+ // 这样乘以 transform:scale 后视觉高度正好是 100dvh
120
+ const layoutPageHeight = container.clientHeight / scale;
121
+
122
+ // 等 viewer resize() 完成,再设 margin
123
+ requestAnimationFrame(() => {
124
+ const slides = Array.from(container.querySelectorAll('.flyfish-pptx-content .slide')) as HTMLElement[];
125
+ slides.forEach((slide) => {
126
+ const gap = Math.max(0, layoutPageHeight - slideH);
127
+ // 居中:前面加半个 gap,后面加半个 gap
128
+ slide.style.marginTop = `${gap / 2}px`;
129
+ slide.style.marginBottom = `${gap / 2}px`;
130
+ slide.style.marginLeft = 'auto';
131
+ slide.style.marginRight = 'auto';
132
+ });
133
+ // 间距生效后重新判定当前可见页
134
+ updateActiveSlide();
135
+ });
136
+ }, [updateActiveSlide]);
137
+
138
+ useEffect(() => {
139
+ let disposed = false;
140
+
141
+ const mount = async () => {
142
+ if (!file || !targetRef.current || !scrollContainerRef.current) return;
143
+
144
+ setIsLoading(true);
145
+ setHasError(false);
146
+ setStage('parsing');
147
+ setParsePercent(0);
148
+ setParsedSlides(0);
149
+ setRenderedSlides(0);
150
+ setTotalSlides(0);
151
+ fittedRef.current = false;
152
+ slideSizeRef.current = null;
153
+ scrollContainerRef.current.scrollTop = 0;
154
+
155
+ try {
156
+ const buffer = await file.arrayBuffer();
157
+ if (disposed) return;
158
+
159
+ const viewer = await PptxViewer.open(buffer, targetRef.current, {
160
+ fitMode: 'none',
161
+ workerType: 'classic',
162
+ workerUrl: withPolyfillWorkerBootstrap('/vendor/pptx/pptx.worker.js'),
163
+ lazySlides: false,
164
+ listOptions: undefined,
165
+ // worker 每解析完一页幻灯片上报一次:progress 为 0-100,
166
+ // message 形如 { type:'progress-update', slide_num, data }
167
+ onProgress: (progress: number, message: unknown) => {
168
+ if (disposed) return;
169
+ setParsePercent(Math.min(100, Math.max(0, Math.floor(progress))));
170
+ const slideNum = (message as { slide_num?: number } | null)?.slide_num;
171
+ if (typeof slideNum === 'number') {
172
+ setParsedSlides(slideNum);
173
+ }
174
+ },
175
+ onSlideSize: (size: { width?: number; height?: number }) => {
176
+ slideSizeRef.current = size;
177
+ },
178
+ onSlideRendered: (slideIndex: number) => {
179
+ if (disposed) return;
180
+ setRenderedSlides((count) => Math.max(count, slideIndex + 1));
181
+ setIsLoading(false);
182
+ // 幻灯片是流式逐页追加的:新页默认 opacity:0,
183
+ // 每次追加后都重算激活页,保证首页立即可见、其余页隐藏
184
+ requestAnimationFrame(() => updateActiveSlide());
185
+ if (!fittedRef.current) {
186
+ fittedRef.current = true;
187
+ void layoutSlides();
188
+ }
189
+ },
190
+ onError: (error) => {
191
+ if (disposed) return;
192
+ console.error('[ppt-preview] render error:', error);
193
+ setHasError(true);
194
+ setIsLoading(false);
195
+ },
196
+ });
197
+
198
+ if (disposed) {
199
+ viewer.destroy();
200
+ return;
201
+ }
202
+ viewerRef.current = viewer;
203
+ // open 在 worker 发送 Done(全部解析完成)后才 resolve,
204
+ // 此时可拿到总页数;若首页尚未渲染(极端大文件后处理慢),显示渲染阶段进度
205
+ setTotalSlides(viewer.slideCount || 0);
206
+ setStage('rendering');
207
+ } catch (error) {
208
+ if (disposed) return;
209
+ console.error('[ppt-preview] mount failed:', error);
210
+ setHasError(true);
211
+ setIsLoading(false);
212
+ }
213
+ };
214
+
215
+ void mount();
216
+
217
+ return () => {
218
+ disposed = true;
219
+ viewerRef.current?.destroy();
220
+ viewerRef.current = null;
221
+ };
222
+ }, [file, reloadTick, layoutSlides, updateActiveSlide]);
223
+
224
+ // 窗口尺寸变化时重新适配
225
+ useEffect(() => {
226
+ let frame = 0;
227
+ const handleResize = () => {
228
+ cancelAnimationFrame(frame);
229
+ frame = requestAnimationFrame(() => {
230
+ void layoutSlides();
231
+ });
232
+ };
233
+ window.addEventListener('resize', handleResize);
234
+ return () => {
235
+ cancelAnimationFrame(frame);
236
+ window.removeEventListener('resize', handleResize);
237
+ };
238
+ }, [layoutSlides]);
239
+
240
+ // 滚动过程中逐帧切换"当前页"显隐(与滚动结束后的对齐 snap 相互独立:
241
+ // snap 有 150ms 防抖,显隐必须实时,否则滚动途中仍会看到相邻页)
242
+ useEffect(() => {
243
+ const container = scrollContainerRef.current;
244
+ if (!container) return;
245
+
246
+ let frame = 0;
247
+ const handleScroll = () => {
248
+ if (frame) return;
249
+ frame = requestAnimationFrame(() => {
250
+ frame = 0;
251
+ updateActiveSlide();
252
+ });
253
+ };
254
+
255
+ container.addEventListener('scroll', handleScroll, { passive: true });
256
+ return () => {
257
+ if (frame) cancelAnimationFrame(frame);
258
+ container.removeEventListener('scroll', handleScroll);
259
+ };
260
+ }, [updateActiveSlide]);
261
+
262
+ // 滚动结束后自动对齐到最近的 slide(与方向键同算法,视觉坐标零换算)
263
+ useEffect(() => {
264
+ const container = scrollContainerRef.current;
265
+ if (!container) return;
266
+
267
+ let scrollTimer: number;
268
+ let snapping = false;
269
+
270
+ const snap = () => {
271
+ if (snapping) return;
272
+ const slides = Array.from(container.querySelectorAll('.flyfish-pptx-content .slide')) as HTMLElement[];
273
+ if (slides.length === 0) return;
274
+
275
+ const containerRect = container.getBoundingClientRect();
276
+ const viewportCenter = containerRect.top + container.clientHeight / 2;
277
+
278
+ // 找视觉中心最接近容器中心的 slide
279
+ let closest = slides[0];
280
+ let closestDist = Number.POSITIVE_INFINITY;
281
+ for (const slide of slides) {
282
+ const r = slide.getBoundingClientRect();
283
+ const d = Math.abs(r.top + r.height / 2 - viewportCenter);
284
+ if (d < closestDist) { closestDist = d; closest = slide; }
285
+ }
286
+
287
+ const slideRect = closest.getBoundingClientRect();
288
+ const scrollDelta = slideRect.top + slideRect.height / 2 - viewportCenter;
289
+ if (Math.abs(scrollDelta) < 3) return;
290
+
291
+ snapping = true;
292
+ container.scrollTo({
293
+ top: Math.max(0, container.scrollTop + scrollDelta),
294
+ behavior: 'smooth',
295
+ });
296
+ window.setTimeout(() => { snapping = false; }, 500);
297
+ };
298
+
299
+ const handleScroll = () => {
300
+ clearTimeout(scrollTimer);
301
+ scrollTimer = window.setTimeout(snap, 150);
302
+ };
303
+
304
+ container.addEventListener('scroll', handleScroll, { passive: true });
305
+ return () => {
306
+ clearTimeout(scrollTimer);
307
+ container.removeEventListener('scroll', handleScroll);
308
+ };
309
+ }, []);
310
+
311
+ // 方向键翻页:↑← 上一页,↓→/空格 下一页,Home 首页,End 末页
312
+ useEffect(() => {
313
+ const container = scrollContainerRef.current;
314
+ if (!container) return;
315
+
316
+ const goToIndex = (delta: number) => {
317
+ const slides = Array.from(container.querySelectorAll('.flyfish-pptx-content .slide')) as HTMLElement[];
318
+ if (slides.length === 0) return;
319
+
320
+ const containerRect = container.getBoundingClientRect();
321
+ const viewportVisualCenter = containerRect.top + container.clientHeight / 2;
322
+
323
+ // 当前 slide:视觉中心最接近容器中心的
324
+ let currentIndex = 0;
325
+ let closestDist = Number.POSITIVE_INFINITY;
326
+ slides.forEach((slide, i) => {
327
+ const r = slide.getBoundingClientRect();
328
+ const d = Math.abs(r.top + r.height / 2 - viewportVisualCenter);
329
+ if (d < closestDist) { closestDist = d; currentIndex = i; }
330
+ });
331
+
332
+ const idx = Math.max(0, Math.min(slides.length - 1, currentIndex + delta));
333
+ if (idx === currentIndex) return;
334
+
335
+ // 让目标 slide 的视觉中心对齐容器中心
336
+ // scrollTop 和 getBoundingClientRect 都在视口坐标系,直接对应,
337
+ // scrollTop 增加 delta → slide visual top 减少 delta,零换算!
338
+ const slideRect = slides[idx].getBoundingClientRect();
339
+ const slideCenter = slideRect.top + slideRect.height / 2;
340
+ const scrollDelta = slideCenter - viewportVisualCenter;
341
+
342
+ container.scrollTo({
343
+ top: Math.max(0, container.scrollTop + scrollDelta),
344
+ behavior: 'smooth',
345
+ });
346
+ };
347
+
348
+ const handleKeyDown = (e: KeyboardEvent) => {
349
+ if (['ArrowUp', 'ArrowLeft'].includes(e.key)) {
350
+ e.preventDefault();
351
+ goToIndex(-1);
352
+ } else if (['ArrowDown', 'ArrowRight', ' '].includes(e.key)) {
353
+ e.preventDefault();
354
+ goToIndex(1);
355
+ } else if (e.key === 'Home') {
356
+ e.preventDefault();
357
+ goToIndex(-9999);
358
+ } else if (e.key === 'End') {
359
+ e.preventDefault();
360
+ goToIndex(9999);
361
+ }
362
+ };
363
+
364
+ window.addEventListener('keydown', handleKeyDown);
365
+ return () => window.removeEventListener('keydown', handleKeyDown);
366
+ }, []);
367
+
368
+ return (
369
+ <div className="ui-file-preview-ppt" data-global-context-menu-ignore="true">
370
+ <style>
371
+ {`.ui-file-preview-ppt {
372
+ position: relative;
373
+ width: 100%;
374
+ height: 100dvh;
375
+ background: #000;
376
+ color: #000;
377
+ color-scheme: light;
378
+ overflow: hidden;
379
+ }
380
+ .ppt-export-pdf-btn {
381
+ position: absolute;
382
+ top: 7px;
383
+ right: 16px;
384
+ z-index: 50;
385
+ }
386
+ .ppt-scroll-container {
387
+ width: 100%;
388
+ height: 100dvh;
389
+ overflow-y: auto;
390
+ overflow-x: hidden;
391
+ scrollbar-width: none;
392
+ }
393
+ .ppt-scroll-container::-webkit-scrollbar {
394
+ display: none;
395
+ }
396
+ /* 覆盖 viewer 默认的 .slide margin-bottom: 50px,
397
+ 由 layoutSlides() 精确计算间距并 inline 设置。
398
+ 单页显示:默认全部透明,仅当前页(视觉中心所在页)加
399
+ .ppt-slide-active 后不透明,保证任何窗口比例下窗口内只出现一页;
400
+ 过渡时间要短,翻页时快速淡入淡出而不拖泥带水。
401
+ (离屏导出 PDF 的 viewer 不在 .ppt-scroll-container 内,不受影响) */
402
+ .ppt-scroll-container .flyfish-pptx-content .slide {
403
+ box-shadow: none !important;
404
+ opacity: 0;
405
+ transition: opacity 0.18s ease;
406
+ }
407
+ .ppt-scroll-container .flyfish-pptx-content .slide.ppt-slide-active {
408
+ opacity: 1;
409
+ }
410
+ .ppt-loading-overlay {
411
+ position: absolute;
412
+ top: 0;
413
+ left: 0;
414
+ right: 0;
415
+ bottom: 0;
416
+ background: #000;
417
+ display: flex;
418
+ flex-direction: column;
419
+ align-items: center;
420
+ justify-content: center;
421
+ z-index: 100;
422
+ }
423
+ .ppt-loading-spinner {
424
+ width: 40px;
425
+ height: 40px;
426
+ border: 3px solid #333;
427
+ border-top: 3px solid #5292f7;
428
+ border-radius: 50%;
429
+ animation: ppt-spin 1s linear infinite;
430
+ }
431
+ .ppt-loading-text {
432
+ margin-top: 16px;
433
+ color: #ccc;
434
+ font-size: 14px;
435
+ }
436
+ .ppt-loading-progress {
437
+ margin-top: 20px;
438
+ width: 220px;
439
+ height: 6px;
440
+ background: #2a2a2a;
441
+ border-radius: 3px;
442
+ overflow: hidden;
443
+ }
444
+ .ppt-loading-progress-bar {
445
+ height: 100%;
446
+ background: linear-gradient(90deg, #5292f7, #67b8ff);
447
+ border-radius: 3px;
448
+ transition: width 0.3s ease;
449
+ }
450
+ .ppt-loading-percent {
451
+ margin-top: 8px;
452
+ color: #888;
453
+ font-size: 12px;
454
+ min-height: 16px;
455
+ }
456
+ .ppt-loading-error-icon {
457
+ font-size: 48px;
458
+ color: #f56c6c;
459
+ }
460
+ .ppt-loading-error-text {
461
+ margin-top: 16px;
462
+ color: #f56c6c;
463
+ font-size: 14px;
464
+ }
465
+ .ppt-loading-retry-btn {
466
+ margin-top: 20px;
467
+ padding: 8px 24px;
468
+ background: #5292f7;
469
+ color: #fff;
470
+ border: none;
471
+ border-radius: 4px;
472
+ cursor: pointer;
473
+ font-size: 14px;
474
+ }
475
+ .ppt-loading-retry-btn:hover {
476
+ background: #67b8ff;
477
+ }
478
+ @keyframes ppt-spin {
479
+ 0% { transform: rotate(0deg); }
480
+ 100% { transform: rotate(360deg); }
481
+ }`}
482
+ </style>
483
+ {!isLoading && !hasError && file && (
484
+ <ExportDropdown
485
+ onExportPdf={handleExportPdf}
486
+ onDownload={handleDownload}
487
+ exportingPdf={exporting}
488
+ buttonProps={{ className: 'vendorsys-export-trigger-on-dark ppt-export-pdf-btn mr-[30px] bg-transparent border-none' }}
489
+ />
490
+ )}
491
+ {file && isLoading && !hasError && (
492
+ <div className="ppt-loading-overlay">
493
+ <div className="ppt-loading-spinner"></div>
494
+ <div className="ppt-loading-text">加载中...</div>
495
+ <div className="ppt-loading-progress">
496
+ <div
497
+ className="ppt-loading-progress-bar"
498
+ style={{ width: `${stage === 'rendering' ? 100 : parsePercent}%` }}
499
+ />
500
+ </div>
501
+ <div className="ppt-loading-percent">
502
+ {stage === 'rendering'
503
+ ? totalSlides > 0
504
+ ? `正在渲染幻灯片 第 ${Math.max(renderedSlides, 1)} / ${totalSlides} 页`
505
+ : '正在渲染幻灯片…'
506
+ : parsePercent >= 100
507
+ ? '正在处理幻灯片资源…'
508
+ : parsePercent > 0
509
+ ? `正在解析演示文稿 ${parsePercent}%${parsedSlides > 0 ? ` · 已解析第 ${parsedSlides} 页` : ''}`
510
+ : '正在读取文件…'}
511
+ </div>
512
+ </div>
513
+ )}
514
+ {file && hasError && (
515
+ <div className="ppt-loading-overlay">
516
+ <div className="ppt-loading-error-icon">✕</div>
517
+ <div className="ppt-loading-error-text">文档加载失败</div>
518
+ <button
519
+ className="ppt-loading-retry-btn"
520
+ onClick={() => {
521
+ setHasError(false);
522
+ setIsLoading(true);
523
+ setReloadTick((v) => v + 1);
524
+ }}
525
+ >
526
+ 重新加载
527
+ </button>
528
+ </div>
529
+ )}
530
+ <div ref={scrollContainerRef} className="ppt-scroll-container">
531
+ <div ref={targetRef} className="ppt-render-target" style={{ width: '100%' }} />
532
+ </div>
533
+ </div>
534
+ );
535
+ };