@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.
- package/package.json +2 -36
- package/src/api/_base.ts +243 -0
- package/src/api/index.ts +17 -0
- package/src/api/project.ts +1345 -0
- package/src/api/user.ts +1174 -0
- package/src/contract.ts +50 -0
- package/src/hooks/index.ts +2 -0
- package/{dist/hooks/index.js → src/hooks/useLongPress.ts} +53 -49
- package/src/hooks/useOnlineStatus.ts +22 -0
- package/src/index.ts +14 -0
- package/src/types/id.ts +2 -0
- package/src/types/index.ts +5 -0
- package/src/types/project/base.ts +63 -0
- package/src/types/project/contract.ts +79 -0
- package/src/types/project/contractStatus.tsx +15 -0
- package/src/types/project/index.ts +5 -0
- package/src/types/project/member.ts +46 -0
- package/src/types/project/progress.ts +43 -0
- package/src/types/projectRole.ts +17 -0
- package/src/types/theme.ts +1 -0
- package/src/types/user/index.ts +1 -0
- package/src/types/user/role.ts +7 -0
- package/src/ui/CommentTimeline/index.tsx +330 -0
- package/src/ui/FileManager/FileContextMenu.tsx +628 -0
- package/src/ui/FileManager/FileGridView.tsx +529 -0
- package/src/ui/FileManager/FileTableView.tsx +405 -0
- package/src/ui/FileManager/FileToolbar.tsx +119 -0
- package/src/ui/FileManager/FileVersionHistoryModal.tsx +175 -0
- package/src/ui/FileManager/MoveCopyToProjectModal.tsx +119 -0
- package/src/ui/FileManager/ShareToWechatModal.tsx +419 -0
- package/src/ui/FileManager/hooks/useFileManagerActions.ts +2532 -0
- package/src/ui/FileManager/hooks/useFileManagerState.ts +419 -0
- package/src/ui/FileManager/index.tsx +580 -0
- package/src/ui/FileManager/projectFileManagerApi.ts +57 -0
- package/src/ui/FileManager/types.ts +184 -0
- package/src/ui/FileManager/utils/fileDeltaWorker.ts +193 -0
- package/src/ui/FileManager/utils/fileTree.ts +178 -0
- package/src/ui/FileManager/utils/fileUpload.ts +714 -0
- package/src/ui/FileManager/utils/fileUtils.ts +65 -0
- package/src/ui/InvoiceRecordsList/index.tsx +813 -0
- package/src/ui/MyFileSelectorModal.tsx +261 -0
- package/src/ui/ProjectSelector/index.tsx +472 -0
- package/src/ui/ProjectSelector/projectCache.ts +40 -0
- package/src/ui/RoleSelector/index.tsx +473 -0
- package/src/ui/RoleSelector/roleCache.ts +40 -0
- package/src/ui/UIAlert.tsx +399 -0
- package/src/ui/UIAlertExample.tsx +129 -0
- package/src/ui/UIConfirm.tsx +335 -0
- package/src/ui/UIConfirmExample.tsx +165 -0
- package/src/ui/UIDatePicker.tsx +146 -0
- package/src/ui/UIDateRangePicker.tsx +38 -0
- package/src/ui/UIFilePreview/ExcelEditor/index.tsx +617 -0
- package/src/ui/UIFilePreview/ExcelEditor/types.ts +24 -0
- package/src/ui/UIFilePreview/ExportDropdown.css +85 -0
- package/src/ui/UIFilePreview/ExportDropdown.tsx +109 -0
- package/src/ui/UIFilePreview/IndexedDBUtils.ts +564 -0
- package/src/ui/UIFilePreview/MarkdownEditor/exportMarkdownPdf.ts +576 -0
- package/src/ui/UIFilePreview/MarkdownEditor/index.tsx +677 -0
- package/src/ui/UIFilePreview/MarkdownEditor/markdownEditor.css +1005 -0
- package/src/ui/UIFilePreview/MarkdownEditor/types.ts +15 -0
- package/src/ui/UIFilePreview/PPTXEditor/index.tsx +602 -0
- package/src/ui/UIFilePreview/RichDocEditor/exportRDocMarkdown.ts +77 -0
- package/src/ui/UIFilePreview/RichDocEditor/exportRDocPdf.ts +374 -0
- package/src/ui/UIFilePreview/RichDocEditor/index.tsx +730 -0
- package/src/ui/UIFilePreview/RichDocEditor/richdoc-editor.css +59 -0
- package/src/ui/UIFilePreview/UIFilePreview.tsx +611 -0
- package/src/ui/UIFilePreview/WordEditor/index.tsx +3933 -0
- package/src/ui/UIFilePreview/WordEditor/style.ts +1213 -0
- package/src/ui/UIFilePreview/WordEditor/word-editor.css +999 -0
- package/src/ui/UIFilePreview/filePreview.css +193 -0
- package/src/ui/UIFilePreview/filetypes/archive.tsx +66 -0
- package/src/ui/UIFilePreview/filetypes/cad.tsx +287 -0
- package/src/ui/UIFilePreview/filetypes/excel.tsx +355 -0
- package/src/ui/UIFilePreview/filetypes/image.tsx +20 -0
- package/src/ui/UIFilePreview/filetypes/imageLoader.ts +63 -0
- package/src/ui/UIFilePreview/filetypes/pdf.tsx +156 -0
- package/src/ui/UIFilePreview/filetypes/ppt.tsx +535 -0
- package/src/ui/UIFilePreview/filetypes/pptExportPdf.ts +523 -0
- package/src/ui/UIFilePreview/filetypes/video.tsx +482 -0
- package/src/ui/UIFilePreview/filetypes/word.tsx +245 -0
- package/src/ui/UIFilePreview/filetypes/wordExportMarkdown.ts +86 -0
- package/src/ui/UIFilePreview/filetypes/wordExportPdf.tsx +427 -0
- package/src/ui/UIFilePreview/index.ts +35 -0
- package/src/ui/UIFilePreview/pdfPagination.ts +143 -0
- package/src/ui/UIFilePreview/previewUtils.tsx +531 -0
- package/src/ui/UIFileUpload.tsx +382 -0
- package/src/ui/UIImageUpload.tsx +195 -0
- package/src/ui/UILoading.tsx +255 -0
- package/src/ui/UILoadingExample.tsx +139 -0
- package/src/ui/UISelect.tsx +105 -0
- package/src/ui/UIToast.tsx +381 -0
- package/src/ui/UIToastExample.tsx +69 -0
- package/src/ui/UIToastSingle.tsx +279 -0
- package/src/ui/UIViewToggle.tsx +64 -0
- package/src/ui/UserSelector/index.tsx +583 -0
- package/src/ui/index.ts +84 -0
- package/src/ui/upload/image.ts +135 -0
- package/src/ui/upload/index.ts +73 -0
- package/src/ui/upload/submitFile.ts +106 -0
- package/src/ui/upload/submitForm.ts +124 -0
- package/src/ui/upload/upload.err.ts +15 -0
- package/src/ui/upload/upload.ts +250 -0
- package/src/utils/CacheDepartmentInfo.ts +372 -0
- package/src/utils/CacheFileContent.ts +315 -0
- package/src/utils/CachePermissionInfo.ts +235 -0
- package/src/utils/CacheRoleInfo.ts +303 -0
- package/src/utils/CacheUserInfo.ts +818 -0
- package/src/utils/CacheViewToggleInfo.ts +182 -0
- package/src/utils/MessageContentUtils.ts +268 -0
- package/src/utils/clipboard.ts +70 -0
- package/src/utils/companyEmail.ts +19 -0
- package/src/utils/forbidDebugger.ts +59 -0
- package/src/utils/formatters.ts +94 -0
- package/src/utils/htmlUtils.tsx +59 -0
- package/src/utils/index.ts +18 -0
- package/src/utils/keyboard.ts +114 -0
- package/src/utils/mammothApi.ts +16 -0
- package/src/utils/polyfills.ts +928 -0
- package/src/utils/regex.ts +14 -0
- package/src/utils/storage.ts +95 -0
- package/src/utils/workerBootstrap.ts +87 -0
- package/dist/api/index.js +0 -1553
- package/dist/contract.js +0 -5
- package/dist/index.js +0 -4492
- package/dist/types/index.js +0 -49
- package/dist/ui/index.css +0 -1876
- package/dist/ui/index.js +0 -22245
- package/dist/utils/index.js +0 -4296
- /package/{dist → src}/styles/ant-modal.css +0 -0
- /package/{dist → src}/styles/ant-spin.css +0 -0
- /package/{dist → src}/styles/ant-table.css +0 -0
- /package/{dist → src}/styles/index.css +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { message } from 'antd';
|
|
3
|
+
import { FileViewer } from '@file-viewer/react';
|
|
4
|
+
import { buildWordViewerOptions, exportWordPdf } from './wordExportPdf';
|
|
5
|
+
import { ExportDropdown } from '../ExportDropdown';
|
|
6
|
+
import { downloadBlob } from '../previewUtils';
|
|
7
|
+
|
|
8
|
+
export interface WordPreviewProps {
|
|
9
|
+
file: File | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const WordPreview: React.FC<WordPreviewProps> = ({ file }) => {
|
|
13
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
14
|
+
const [progress, setProgress] = useState(0);
|
|
15
|
+
const [hasError, setHasError] = useState(false);
|
|
16
|
+
const [exporting, setExporting] = useState(false);
|
|
17
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const retryRef = useRef(false);
|
|
19
|
+
|
|
20
|
+
const options = useMemo(() => buildWordViewerOptions(), []);
|
|
21
|
+
|
|
22
|
+
// 离屏重新按 Word 原生分页渲染后逐页截图,直接下载 PDF(无打印对话框)。
|
|
23
|
+
// 预览虽同为分页视图,但仍独立渲染一份离屏 DOM,避免缩放/样式干扰截图。
|
|
24
|
+
const handleExportPdf = useCallback(async () => {
|
|
25
|
+
if (!file) {
|
|
26
|
+
message.warning('文档尚未加载完成,请稍后再试');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setExporting(true);
|
|
30
|
+
message.loading({ key: 'word-export-pdf', content: '正在准备文档…', duration: 0 });
|
|
31
|
+
try {
|
|
32
|
+
await exportWordPdf({
|
|
33
|
+
file,
|
|
34
|
+
fileName: file.name,
|
|
35
|
+
onProgress: (page, total) => {
|
|
36
|
+
message.loading({ key: 'word-export-pdf', content: `正在导出第 ${page} / ${total} 页…`, duration: 0 });
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Word 导出 PDF 失败:', error);
|
|
41
|
+
message.error(`导出 PDF 失败:${(error as Error)?.message || '请重试'}`);
|
|
42
|
+
} finally {
|
|
43
|
+
message.destroy('word-export-pdf');
|
|
44
|
+
setExporting(false);
|
|
45
|
+
}
|
|
46
|
+
}, [file]);
|
|
47
|
+
|
|
48
|
+
// 下载原始文件:只读预览拿到的 File 就是原始文件
|
|
49
|
+
const handleDownload = useCallback(() => {
|
|
50
|
+
if (!file) return;
|
|
51
|
+
downloadBlob(file, file.name);
|
|
52
|
+
}, [file]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!containerRef.current || !file) return;
|
|
56
|
+
|
|
57
|
+
let progressTimer: ReturnType<typeof setInterval>;
|
|
58
|
+
let checkTimer: ReturnType<typeof setTimeout>;
|
|
59
|
+
let timeoutTimer: ReturnType<typeof setTimeout>;
|
|
60
|
+
|
|
61
|
+
const cancelAll = () => {
|
|
62
|
+
clearInterval(progressTimer);
|
|
63
|
+
clearTimeout(checkTimer);
|
|
64
|
+
clearTimeout(timeoutTimer);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const incrementProgress = () => {
|
|
68
|
+
setProgress(prev => {
|
|
69
|
+
if (prev >= 95) return prev;
|
|
70
|
+
return prev + Math.random() * 15 + 5;
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
progressTimer = setInterval(incrementProgress, 300);
|
|
75
|
+
|
|
76
|
+
const checkContent = () => {
|
|
77
|
+
const container = containerRef.current;
|
|
78
|
+
if (!container) return;
|
|
79
|
+
|
|
80
|
+
const hasContent = container.innerHTML.trim().length > 0 &&
|
|
81
|
+
container.querySelector('.docx-wrapper, .docx-page-frame, .docx-flow-frame');
|
|
82
|
+
|
|
83
|
+
if (hasContent) {
|
|
84
|
+
setProgress(100);
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
setIsLoading(false);
|
|
87
|
+
setHasError(false);
|
|
88
|
+
}, 200);
|
|
89
|
+
cancelAll();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
checkTimer = setTimeout(checkContent, 100);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
checkTimer = setTimeout(checkContent, 100);
|
|
97
|
+
|
|
98
|
+
timeoutTimer = setTimeout(() => {
|
|
99
|
+
clearInterval(progressTimer);
|
|
100
|
+
clearTimeout(checkTimer);
|
|
101
|
+
const container = containerRef.current;
|
|
102
|
+
if (container && !container.querySelector('.docx-wrapper, .docx-page-frame, .docx-flow-frame')) {
|
|
103
|
+
setHasError(true);
|
|
104
|
+
}
|
|
105
|
+
}, 30000);
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
cancelAll();
|
|
109
|
+
};
|
|
110
|
+
}, [file, retryRef.current]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (file) {
|
|
114
|
+
setIsLoading(true);
|
|
115
|
+
setProgress(0);
|
|
116
|
+
setHasError(false);
|
|
117
|
+
}
|
|
118
|
+
}, [file]);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="ui-file-preview-word" data-global-context-menu-ignore="true" style={{ position: 'relative' }}>
|
|
122
|
+
<style>{`
|
|
123
|
+
.word-export-pdf-btn {
|
|
124
|
+
position: absolute;
|
|
125
|
+
top: 7px;
|
|
126
|
+
right: 16px;
|
|
127
|
+
z-index: 50;
|
|
128
|
+
}
|
|
129
|
+
.word-toolbar {
|
|
130
|
+
padding-right: 50px !important;
|
|
131
|
+
}
|
|
132
|
+
.word-loading-overlay {
|
|
133
|
+
position: absolute;
|
|
134
|
+
top: 0;
|
|
135
|
+
left: 0;
|
|
136
|
+
right: 0;
|
|
137
|
+
bottom: 0;
|
|
138
|
+
background: #ffffff;
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-direction: column;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
z-index: 100;
|
|
144
|
+
}
|
|
145
|
+
.word-loading-spinner {
|
|
146
|
+
width: 40px;
|
|
147
|
+
height: 40px;
|
|
148
|
+
border: 3px solid #f3f3f3;
|
|
149
|
+
border-top: 3px solid #5292f7;
|
|
150
|
+
border-radius: 50%;
|
|
151
|
+
animation: spin 1s linear infinite;
|
|
152
|
+
}
|
|
153
|
+
.word-loading-text {
|
|
154
|
+
margin-top: 16px;
|
|
155
|
+
color: #666;
|
|
156
|
+
font-size: 14px;
|
|
157
|
+
}
|
|
158
|
+
.word-loading-progress {
|
|
159
|
+
margin-top: 24px;
|
|
160
|
+
width: 200px;
|
|
161
|
+
height: 6px;
|
|
162
|
+
background: #e8e8e8;
|
|
163
|
+
border-radius: 3px;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
}
|
|
166
|
+
.word-loading-progress-bar {
|
|
167
|
+
height: 100%;
|
|
168
|
+
background: linear-gradient(90deg, #5292f7, #67b8ff);
|
|
169
|
+
border-radius: 3px;
|
|
170
|
+
transition: width 0.3s ease;
|
|
171
|
+
}
|
|
172
|
+
.word-loading-percent {
|
|
173
|
+
margin-top: 8px;
|
|
174
|
+
color: #999;
|
|
175
|
+
font-size: 12px;
|
|
176
|
+
}
|
|
177
|
+
.word-loading-error-icon {
|
|
178
|
+
font-size: 48px;
|
|
179
|
+
color: #f56c6c;
|
|
180
|
+
}
|
|
181
|
+
.word-loading-error-text {
|
|
182
|
+
margin-top: 16px;
|
|
183
|
+
color: #f56c6c;
|
|
184
|
+
font-size: 14px;
|
|
185
|
+
}
|
|
186
|
+
.word-loading-retry-btn {
|
|
187
|
+
margin-top: 20px;
|
|
188
|
+
padding: 8px 24px;
|
|
189
|
+
background: #5292f7;
|
|
190
|
+
color: #fff;
|
|
191
|
+
border: none;
|
|
192
|
+
border-radius: 4px;
|
|
193
|
+
cursor: pointer;
|
|
194
|
+
font-size: 14px;
|
|
195
|
+
}
|
|
196
|
+
.word-loading-retry-btn:hover {
|
|
197
|
+
background: #67b8ff;
|
|
198
|
+
}
|
|
199
|
+
@keyframes spin {
|
|
200
|
+
0% { transform: rotate(0deg); }
|
|
201
|
+
100% { transform: rotate(360deg); }
|
|
202
|
+
}`}
|
|
203
|
+
</style>
|
|
204
|
+
{!isLoading && !hasError && file && (
|
|
205
|
+
<ExportDropdown
|
|
206
|
+
onExportPdf={handleExportPdf}
|
|
207
|
+
onDownload={handleDownload}
|
|
208
|
+
exportingPdf={exporting}
|
|
209
|
+
buttonProps={{ className: 'vendorsys-export-trigger-on-light word-export-pdf-btn mr-[30px] bg-transparent border-none' }}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
{file && isLoading && !hasError && (
|
|
213
|
+
<div className="word-loading-overlay">
|
|
214
|
+
<div className="word-loading-spinner"></div>
|
|
215
|
+
<div className="word-loading-text">加载中...</div>
|
|
216
|
+
<div className="word-loading-progress">
|
|
217
|
+
<div className="word-loading-progress-bar" style={{ width: `${Math.min(progress, 100)}%` }}></div>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="word-loading-percent">{Math.round(Math.min(progress, 100))}%</div>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
{file && hasError && (
|
|
223
|
+
<div className="word-loading-overlay">
|
|
224
|
+
<div className="word-loading-error-icon">✕</div>
|
|
225
|
+
<div className="word-loading-error-text">文档加载失败</div>
|
|
226
|
+
<button className="word-loading-retry-btn" onClick={() => {
|
|
227
|
+
setHasError(false);
|
|
228
|
+
setIsLoading(true);
|
|
229
|
+
setProgress(0);
|
|
230
|
+
retryRef.current = !retryRef.current;
|
|
231
|
+
}}>重新加载</button>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
{file ? (
|
|
235
|
+
<div ref={containerRef} style={{ width: '100%', height: '100dvh' }}>
|
|
236
|
+
<FileViewer
|
|
237
|
+
file={file}
|
|
238
|
+
name={file.name}
|
|
239
|
+
options={options}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
) : null}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import TurndownService from 'turndown';
|
|
2
|
+
import { gfm } from 'turndown-plugin-gfm';
|
|
3
|
+
import { mammoth } from '../../../utils/mammothApi';
|
|
4
|
+
import { downloadBlob } from '../previewUtils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Word 导出 Markdown。
|
|
8
|
+
*
|
|
9
|
+
* 说明:markitdown 是 Python 库,浏览器端跑不了,这里用等价链路实现:
|
|
10
|
+
* docx --mammoth--> 语义化 HTML --turndown(gfm)--> Markdown
|
|
11
|
+
* mammoth 负责 docx 的结构解析(标题、列表、表格、加粗等),turndown + gfm
|
|
12
|
+
* 负责 HTML → Markdown 的还原,表格用 gfm 插件保住 Markdown 表格语法。
|
|
13
|
+
* mammoth 已是项目依赖(原本用于 docx 文本提取)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface ExportWordMarkdownOptions {
|
|
17
|
+
/** docx/doc 文件(编辑器侧应传入含最新修改的 save() 产物) */
|
|
18
|
+
file: Blob;
|
|
19
|
+
/** 文件名(.docx/.doc 后缀会被替换为 .md) */
|
|
20
|
+
fileName: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 把 .docx/.doc 文件名换成 .md */
|
|
24
|
+
const toMarkdownFileName = (fileName: string): string =>
|
|
25
|
+
`${fileName.replace(/\.(docx|doc)$/i, '') || 'document'}.md`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* mammoth 输出的表格单元格一律是 <td>,而 turndown-plugin-gfm 的 table 规则只在
|
|
29
|
+
* 「首行全部是 <th>(有表头行)」时才转成 Markdown 管道表格,否则会把整张表原样保留成
|
|
30
|
+
* HTML。这里做两步预处理:
|
|
31
|
+
* 1. 把首行的 <td> 提升为 <th> 并包一层 <thead>,让管道表格规则生效(与 markitdown 一致:
|
|
32
|
+
* 拿首行当表头);
|
|
33
|
+
* 2. 单元格内的 <p>/<br> 是块级/换行元素,会把管道表格撑破行,这里拍平成行内文本。
|
|
34
|
+
*/
|
|
35
|
+
const normalizeTablesForMarkdown = (html: string): HTMLElement => {
|
|
36
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
37
|
+
|
|
38
|
+
doc.querySelectorAll('table').forEach((table) => {
|
|
39
|
+
const firstRow = table.querySelector('tr');
|
|
40
|
+
if (!firstRow || !firstRow.querySelector('td')) return;
|
|
41
|
+
const thead = doc.createElement('thead');
|
|
42
|
+
firstRow.querySelectorAll('td').forEach((cell) => {
|
|
43
|
+
const th = doc.createElement('th');
|
|
44
|
+
th.innerHTML = cell.innerHTML;
|
|
45
|
+
cell.replaceWith(th);
|
|
46
|
+
});
|
|
47
|
+
table.insertBefore(thead, table.firstChild);
|
|
48
|
+
thead.appendChild(firstRow);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
doc.querySelectorAll('table td, table th').forEach((cell) => {
|
|
52
|
+
cell.querySelectorAll('p').forEach((p) => {
|
|
53
|
+
const nodes = Array.from(p.childNodes);
|
|
54
|
+
if (p.nextElementSibling) nodes.push(doc.createTextNode(' '));
|
|
55
|
+
p.replaceWith(...nodes);
|
|
56
|
+
});
|
|
57
|
+
cell.querySelectorAll('br').forEach((br) => {
|
|
58
|
+
br.replaceWith(doc.createTextNode(' '));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return doc.body;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** docx → Markdown 文本(不下载,便于复用/测试) */
|
|
66
|
+
export const convertDocxToMarkdown = async (file: Blob): Promise<string> => {
|
|
67
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
68
|
+
const { value: html } = await mammoth.convertToHtml({ arrayBuffer });
|
|
69
|
+
|
|
70
|
+
const turndown = new TurndownService({
|
|
71
|
+
headingStyle: 'atx',
|
|
72
|
+
bulletListMarker: '-',
|
|
73
|
+
codeBlockStyle: 'fenced',
|
|
74
|
+
hr: '---',
|
|
75
|
+
});
|
|
76
|
+
turndown.use(gfm);
|
|
77
|
+
|
|
78
|
+
return turndown.turndown(normalizeTablesForMarkdown(html));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** docx → Markdown 并直接下载 .md 文件,返回转换结果 */
|
|
82
|
+
export const exportWordMarkdown = async ({ file, fileName }: ExportWordMarkdownOptions): Promise<string> => {
|
|
83
|
+
const markdown = await convertDocxToMarkdown(file);
|
|
84
|
+
downloadBlob(new Blob([markdown], { type: 'text/markdown;charset=utf-8' }), toMarkdownFileName(fileName));
|
|
85
|
+
return markdown;
|
|
86
|
+
};
|