@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,482 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { PlayCircleOutlined, LoadingOutlined, ReloadOutlined, PauseCircleOutlined } from '@ant-design/icons';
3
+ import { AuthorizationHandler } from '../../../api/_base';
4
+ import { Button } from 'antd';
5
+
6
+ export interface VideoPreviewProps {
7
+ previewUrl: string;
8
+ type: string;
9
+ }
10
+
11
+ export const VideoPreview: React.FC<VideoPreviewProps> = ({ previewUrl, type }) => {
12
+ const [isLoading, setIsLoading] = useState(false);
13
+ const [isLoaded, setIsLoaded] = useState(false);
14
+ const [isError, setIsError] = useState(false);
15
+ const [hasInteracted, setHasInteracted] = useState(false);
16
+ const [playbackRate, setPlaybackRate] = useState(1);
17
+ const [posterBlobUrl, setPosterBlobUrl] = useState<string | null>(null);
18
+ const [isPlaying, setIsPlaying] = useState(false);
19
+ const [bufferProgress, setBufferProgress] = useState(0);
20
+ const videoRef = useRef<HTMLVideoElement>(null);
21
+
22
+ // 获取带有 Authorization header 的请求头
23
+ const getAuthHeaders = useCallback(() => {
24
+ const headers: Record<string, string> = {};
25
+ const handler = {
26
+ headers: {
27
+ set: (key: string, value: string) => {
28
+ headers[key] = value;
29
+ },
30
+ } as any,
31
+ };
32
+ AuthorizationHandler.requestHandleSid(handler);
33
+ return headers;
34
+ }, []);
35
+
36
+ // 尝试获取视频缩略图
37
+ useEffect(() => {
38
+ if (type !== 'video') return;
39
+
40
+ const generatePoster = async () => {
41
+ const headers = getAuthHeaders();
42
+
43
+ try {
44
+ // 使用 HEAD 请求获取文件信息(避免下载完整文件)
45
+ const headResponse = await fetch(previewUrl, {
46
+ method: 'HEAD',
47
+ headers: { Authorization: headers.Authorization || '' },
48
+ });
49
+
50
+ if (!headResponse.ok) {
51
+ console.warn('HEAD request failed, trying GET for poster');
52
+ return;
53
+ }
54
+
55
+ // 尝试获取视频第一帧(使用 Range 请求只获取部分数据)
56
+ const rangeResponse = await fetch(previewUrl, {
57
+ method: 'GET',
58
+ headers: {
59
+ Authorization: headers.Authorization || '',
60
+ Range: 'bytes=0-102400', // 获取前 100KB
61
+ },
62
+ });
63
+
64
+ if (!rangeResponse.ok && rangeResponse.status !== 206) {
65
+ console.warn('Failed to fetch video header for poster');
66
+ return;
67
+ }
68
+
69
+ const blob = await rangeResponse.blob();
70
+ const videoUrl = URL.createObjectURL(blob);
71
+
72
+ const video = document.createElement('video');
73
+ video.crossOrigin = 'anonymous';
74
+ video.src = videoUrl;
75
+
76
+ try {
77
+ await new Promise<void>((resolve, reject) => {
78
+ video.onloadedmetadata = () => {
79
+ video.currentTime = 0.1;
80
+ resolve();
81
+ };
82
+ video.onerror = reject;
83
+ });
84
+
85
+ await new Promise<void>((resolve) => {
86
+ video.onseeked = () => resolve();
87
+ });
88
+
89
+ const canvas = document.createElement('canvas');
90
+ canvas.width = video.videoWidth || 640;
91
+ canvas.height = video.videoHeight || 360;
92
+ const ctx = canvas.getContext('2d');
93
+ if (ctx) {
94
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
95
+ const posterUrl = canvas.toDataURL('image/jpeg', 0.7);
96
+ setPosterBlobUrl(posterUrl);
97
+ }
98
+ } catch (error) {
99
+ console.warn('Failed to generate video poster:', error);
100
+ } finally {
101
+ URL.revokeObjectURL(videoUrl);
102
+ }
103
+ } catch (error) {
104
+ console.warn('Failed to fetch video for poster:', error);
105
+ }
106
+ };
107
+
108
+ generatePoster();
109
+ }, [previewUrl, type, getAuthHeaders]);
110
+
111
+ const handlePlayClick = async () => {
112
+ if (isError) {
113
+ setIsError(false);
114
+ setIsLoading(true);
115
+ setHasInteracted(true);
116
+ await setupStreamedVideo();
117
+ return;
118
+ }
119
+
120
+ setHasInteracted(true);
121
+ setIsLoading(true);
122
+ await setupStreamedVideo();
123
+ };
124
+
125
+ // 设置流式视频加载
126
+ const setupStreamedVideo = async () => {
127
+ try {
128
+ const headers = getAuthHeaders();
129
+
130
+ // 先检查服务器是否支持 Range 请求(流式加载的关键)
131
+ const headResponse = await fetch(previewUrl, {
132
+ method: 'HEAD',
133
+ headers: { Authorization: headers.Authorization || '' },
134
+ });
135
+
136
+ const acceptRanges = headResponse.headers.get('accept-ranges');
137
+ const contentLength = headResponse.headers.get('content-length');
138
+
139
+ console.log(`Server supports ranges: ${acceptRanges}`);
140
+ console.log(`Content length: ${contentLength}`);
141
+
142
+ // 创建支持流式播放的视频源
143
+ const videoSource = document.createElement('source');
144
+ videoSource.src = previewUrl;
145
+
146
+ // 如果服务器支持范围请求,视频将自动流式加载
147
+ if (videoRef.current) {
148
+ // 清除现有源
149
+ while (videoRef.current.firstChild) {
150
+ videoRef.current.removeChild(videoRef.current.firstChild);
151
+ }
152
+ videoRef.current.appendChild(videoSource);
153
+
154
+ // 设置跨域和认证头
155
+ videoRef.current.crossOrigin = 'use-credentials';
156
+
157
+ // 加载视频
158
+ await videoRef.current.load();
159
+
160
+ setIsLoading(false);
161
+ setIsLoaded(true);
162
+ setIsPlaying(true);
163
+
164
+ // 尝试播放(可能因浏览器策略失败,需要用户交互)
165
+ videoRef.current.play().catch(() => {});
166
+ }
167
+ } catch (error) {
168
+ console.error('Video setup error:', error);
169
+ // 降级到完整下载模式
170
+ await fallbackToFullDownload();
171
+ }
172
+ };
173
+
174
+ // 降级方案:完整下载后播放
175
+ const fallbackToFullDownload = async () => {
176
+ try {
177
+ const headers = getAuthHeaders();
178
+
179
+ const response = await fetch(previewUrl, {
180
+ headers: { Authorization: headers.Authorization || '' },
181
+ method: 'GET',
182
+ });
183
+
184
+ if (!response.ok) {
185
+ throw new Error(`视频加载失败,状态码: ${response.status}`);
186
+ }
187
+
188
+ const blob = await response.blob();
189
+ const url = URL.createObjectURL(blob);
190
+
191
+ if (videoRef.current) {
192
+ videoRef.current.src = url;
193
+ await videoRef.current.load();
194
+ setIsLoading(false);
195
+ setIsLoaded(true);
196
+ setIsPlaying(true);
197
+ videoRef.current.play().catch(() => {});
198
+ }
199
+
200
+ // 清理:组件卸载时释放 blob URL
201
+ return () => {
202
+ URL.revokeObjectURL(url);
203
+ };
204
+ } catch (error) {
205
+ console.error('Fallback download error:', error);
206
+ setIsLoading(false);
207
+ setIsError(true);
208
+ }
209
+ };
210
+
211
+ const handleVideoProgress = () => {
212
+ if (videoRef.current) {
213
+ const buffered = videoRef.current.buffered;
214
+ const duration = videoRef.current.duration;
215
+
216
+ if (buffered.length > 0 && duration > 0) {
217
+ const bufferedEnd = buffered.end(buffered.length - 1);
218
+ const percent = Math.min((bufferedEnd / duration) * 100, 100);
219
+ setBufferProgress(percent);
220
+
221
+ // 检测是否缓冲完成
222
+ if (percent >= 99) {
223
+ console.log('Video fully buffered');
224
+ }
225
+ }
226
+ }
227
+ };
228
+
229
+ const handleVideoLoaded = () => {
230
+ setIsLoading(false);
231
+ setIsLoaded(true);
232
+ };
233
+
234
+ const handleVideoError = () => {
235
+ console.error('Video element error');
236
+ setIsLoading(false);
237
+ setIsError(true);
238
+ };
239
+
240
+ const handleVideoPlay = () => {
241
+ setIsPlaying(true);
242
+ };
243
+
244
+ const handleVideoPause = () => {
245
+ setIsPlaying(false);
246
+ };
247
+
248
+ const handlePlaybackRateChange = (rate: number) => {
249
+ setPlaybackRate(rate);
250
+ if (videoRef.current) {
251
+ videoRef.current.playbackRate = rate;
252
+ }
253
+ };
254
+
255
+ const togglePlayPause = () => {
256
+ if (videoRef.current) {
257
+ if (isPlaying) {
258
+ videoRef.current.pause();
259
+ } else {
260
+ videoRef.current.play().catch(() => {});
261
+ }
262
+ }
263
+ };
264
+
265
+ if (type !== 'video') return null;
266
+
267
+ return (
268
+ <div style={{
269
+ width: '100%',
270
+ height: '100%',
271
+ display: 'flex',
272
+ justifyContent: 'center',
273
+ alignItems: 'center',
274
+ background: '#000',
275
+ position: 'relative',
276
+ }}>
277
+ {/* 视频元素 - 支持流式加载 */}
278
+ <video
279
+ ref={videoRef}
280
+ controls
281
+ preload="metadata"
282
+ onLoadedData={handleVideoLoaded}
283
+ onError={handleVideoError}
284
+ onProgress={handleVideoProgress}
285
+ onPlay={handleVideoPlay}
286
+ onPause={handleVideoPause}
287
+ style={{
288
+ maxWidth: '100%',
289
+ maxHeight: 'calc(100vh - 40px)',
290
+ objectFit: 'contain',
291
+ display: hasInteracted && !isLoading && !isError ? 'block' : 'none',
292
+ }}
293
+ crossOrigin="use-credentials"
294
+ />
295
+
296
+ {/* 缓冲进度条 */}
297
+ {isPlaying && bufferProgress > 0 && bufferProgress < 100 && (
298
+ <div
299
+ style={{
300
+ position: 'absolute',
301
+ bottom: '45px',
302
+ left: '20px',
303
+ right: '20px',
304
+ height: '4px',
305
+ backgroundColor: 'rgba(255, 255, 255, 0.3)',
306
+ borderRadius: '2px',
307
+ }}
308
+ >
309
+ <div
310
+ style={{
311
+ height: '100%',
312
+ backgroundColor: '#1890ff',
313
+ borderRadius: '2px',
314
+ transition: 'width 0.3s ease',
315
+ width: `${bufferProgress}%`,
316
+ }}
317
+ />
318
+ </div>
319
+ )}
320
+
321
+ {/* 缩略图/加载遮罩 */}
322
+ {!hasInteracted || isLoading || isError ? (
323
+ <div
324
+ style={{
325
+ position: 'absolute',
326
+ top: 0,
327
+ left: 0,
328
+ right: 0,
329
+ bottom: 0,
330
+ display: 'flex',
331
+ flexDirection: 'column',
332
+ justifyContent: 'center',
333
+ alignItems: 'center',
334
+ background: '#1a1a1a',
335
+ }}
336
+ >
337
+ {/* 缩略图 */}
338
+ {!isLoading && !isError && (
339
+ <img
340
+ src={posterBlobUrl || 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 72"%3E%3Crect fill="%232a2a2a" width="128" height="72"/%3E%3Ctext fill="%234a4a4a" font-family="sans-serif" font-size="12" x="50%25" y="50%25" text-anchor="middle" dominant-baseline="middle"%3E视频预览%3C/text%3E%3C/svg%3E'}
341
+ alt="Video preview"
342
+ style={{
343
+ maxWidth: '100%',
344
+ maxHeight: 'calc(100vh - 200px)',
345
+ objectFit: 'contain',
346
+ }}
347
+ />
348
+ )}
349
+
350
+ {/* 加载状态 */}
351
+ {isLoading && (
352
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
353
+ <LoadingOutlined style={{ fontSize: 48, color: '#1890ff' }} spin />
354
+ <span style={{ color: '#999', fontSize: 14 }}>正在准备视频...</span>
355
+ </div>
356
+ )}
357
+
358
+ {/* 错误状态 */}
359
+ {isError && (
360
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
361
+ <div style={{ fontSize: 48, color: '#ff4d4f' }}>
362
+ <svg viewBox="0 0 24 24" fill="currentColor">
363
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
364
+ </svg>
365
+ </div>
366
+ <span style={{ color: '#ff4d4f', fontSize: 14 }}>视频加载失败</span>
367
+ <Button
368
+ type="primary"
369
+ onClick={handlePlayClick}
370
+ style={{
371
+ padding: '8px 16px',
372
+ color: '#fff',
373
+ border: 'none',
374
+ borderRadius: '4px',
375
+ cursor: 'pointer',
376
+ display: 'flex',
377
+ alignItems: 'center',
378
+ gap: 8,
379
+ }}
380
+ >
381
+ <ReloadOutlined />
382
+ 重试加载
383
+ </Button>
384
+ </div>
385
+ )}
386
+
387
+ {/* 播放按钮 */}
388
+ {!isLoading && !isError && (
389
+ <Button
390
+ type="primary"
391
+ onClick={handlePlayClick}
392
+ style={{
393
+ position: 'absolute',
394
+ width: '80px',
395
+ height: '80px',
396
+ borderRadius: '50%',
397
+ backgroundColor: 'rgba(24, 144, 255, 0.9)',
398
+ border: 'none',
399
+ cursor: 'pointer',
400
+ display: 'flex',
401
+ justifyContent: 'center',
402
+ alignItems: 'center',
403
+ transition: 'transform 0.2s',
404
+ }}
405
+ onMouseEnter={(e) => {
406
+ (e.currentTarget as HTMLElement).style.transform = 'scale(1.1)';
407
+ }}
408
+ onMouseLeave={(e) => {
409
+ (e.currentTarget as HTMLElement).style.transform = 'scale(1)';
410
+ }}
411
+ >
412
+ <PlayCircleOutlined style={{ fontSize: 48, color: '#fff' }} />
413
+ </Button>
414
+ )}
415
+ </div>
416
+ ) : null}
417
+
418
+ {/* 播放/暂停切换按钮(视频加载后显示在中央) */}
419
+ {isLoaded && (
420
+ <Button
421
+ type="text"
422
+ onClick={togglePlayPause}
423
+ style={{
424
+ position: 'absolute',
425
+ width: '60px',
426
+ height: '60px',
427
+ borderRadius: '50%',
428
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
429
+ border: 'none',
430
+ cursor: 'pointer',
431
+ display: 'none',
432
+ justifyContent: 'center',
433
+ alignItems: 'center',
434
+ transition: 'opacity 0.3s',
435
+ }}
436
+ className="video-overlay-play-btn"
437
+ >
438
+ {isPlaying ? (
439
+ <PauseCircleOutlined style={{ fontSize: 36, color: '#fff' }} />
440
+ ) : (
441
+ <PlayCircleOutlined style={{ fontSize: 36, color: '#fff' }} />
442
+ )}
443
+ </Button>
444
+ )}
445
+
446
+ {/* 播放速度控制 */}
447
+ {isLoaded && (
448
+ <div
449
+ style={{
450
+ position: 'absolute',
451
+ bottom: '60px',
452
+ right: '20px',
453
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
454
+ padding: '4px',
455
+ borderRadius: '4px',
456
+ display: 'flex',
457
+ gap: '4px',
458
+ }}
459
+ >
460
+ {[0.5, 1, 1.25, 1.5, 2].map((rate) => (
461
+ <Button
462
+ type="text"
463
+ key={rate}
464
+ onClick={() => handlePlaybackRateChange(rate)}
465
+ style={{
466
+ padding: '4px 8px',
467
+ backgroundColor: playbackRate === rate ? '#1890ff' : 'transparent',
468
+ color: '#fff',
469
+ border: 'none',
470
+ borderRadius: '2px',
471
+ fontSize: '12px',
472
+ cursor: 'pointer',
473
+ }}
474
+ >
475
+ {rate}x
476
+ </Button>
477
+ ))}
478
+ </div>
479
+ )}
480
+ </div>
481
+ );
482
+ };