@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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将块文档(.rdoc)导出为 PDF。
|
|
3
|
+
*
|
|
4
|
+
* 与 Markdown 导出完全独立:
|
|
5
|
+
* 1. 直接克隆编辑器真实 DOM(.bn-editor),并放在 .richdoc-editor-host 作用域下,
|
|
6
|
+
* 复用编辑器自身的 BlockNote / richdoc 样式,保证导出与编辑时所见一致;
|
|
7
|
+
* 2. 按块/行边界分页(见 pdfPagination),不会把一行文字切成两半;
|
|
8
|
+
* 3. 逐页 html2canvas 截图后写入 jsPDF,触发浏览器下载。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { buildPageSlices, measureBlocks } from '../pdfPagination';
|
|
12
|
+
|
|
13
|
+
export interface ExportRDocPdfOptions {
|
|
14
|
+
/** 编辑器根 DOM(BlockNote 的 editor.domElement) */
|
|
15
|
+
editorElement: HTMLElement;
|
|
16
|
+
/** 文件名(.rdoc 后缀会被替换为 .pdf) */
|
|
17
|
+
fileName: string;
|
|
18
|
+
/** 分页进度回调(当前页 / 总页数) */
|
|
19
|
+
onProgress?: (page: number, total: number) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A4:210mm x 297mm,四周留白 10mm,内容区 190mm x 277mm
|
|
23
|
+
const PAGE_MARGIN_MM = 10;
|
|
24
|
+
const PAGE_WIDTH_MM = 210;
|
|
25
|
+
const PAGE_HEIGHT_MM = 297;
|
|
26
|
+
const CONTENT_WIDTH_MM = PAGE_WIDTH_MM - PAGE_MARGIN_MM * 2; // 190
|
|
27
|
+
const CONTENT_HEIGHT_MM = PAGE_HEIGHT_MM - PAGE_MARGIN_MM * 2; // 277
|
|
28
|
+
// 内容区 CSS 像素宽度(96dpi),离屏容器按此固定宽度排版
|
|
29
|
+
const CONTENT_WIDTH_PX = Math.round((CONTENT_WIDTH_MM / 25.4) * 96); // 718
|
|
30
|
+
const CONTENT_HEIGHT_PX = (CONTENT_WIDTH_PX * CONTENT_HEIGHT_MM) / CONTENT_WIDTH_MM;
|
|
31
|
+
// 截图倍率,保证 PDF 清晰度
|
|
32
|
+
const SCALE = 3;
|
|
33
|
+
// 图片加载最长等待时间(ms),避免图片异常时永久阻塞导出
|
|
34
|
+
const IMAGE_WAIT_TIMEOUT = 8000;
|
|
35
|
+
|
|
36
|
+
/** 离屏容器标记,供后续定位/清理 */
|
|
37
|
+
const EXPORT_ROOT_ATTR = 'data-rdoc-export-root';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 导出时对克隆 DOM 的少量修正:
|
|
41
|
+
* 1. 解除编辑器的视口高度限制,让内容自然撑开;
|
|
42
|
+
* 2. 去掉交互痕迹(光标、占位符、侧边菜单、末尾空块);
|
|
43
|
+
* 3. 修正行内 code 的灰底位置——html2canvas 1.4 按行盒画内联元素背景,
|
|
44
|
+
* 行高 1.5 时灰底会比文字高出一截且整体偏上,需把行高收紧到 1。
|
|
45
|
+
*/
|
|
46
|
+
const RDOC_EXPORT_STYLES = `
|
|
47
|
+
.rdoc-export-preview {
|
|
48
|
+
background: #ffffff;
|
|
49
|
+
height: auto !important;
|
|
50
|
+
overflow: visible !important;
|
|
51
|
+
}
|
|
52
|
+
.rdoc-export-preview .bn-editor {
|
|
53
|
+
min-height: 0 !important;
|
|
54
|
+
height: auto !important;
|
|
55
|
+
padding-block: 0 !important;
|
|
56
|
+
outline: none !important;
|
|
57
|
+
}
|
|
58
|
+
/* 只影响行内 code,不能波及代码块(pre > code)的行距 */
|
|
59
|
+
.rdoc-export-preview .bn-editor :not(pre) > code {
|
|
60
|
+
line-height: 1 !important;
|
|
61
|
+
vertical-align: baseline !important;
|
|
62
|
+
}
|
|
63
|
+
/* 修复 html2canvas 1.4 的字形测量环境(与 Markdown 导出一致):
|
|
64
|
+
它会在 body 下临时插入一个 1x1 img 来推算基线,Tailwind preflight 的 img{display:block}
|
|
65
|
+
会把这个测量图挤出文字行盒,导致所有文字被系统性下移。 */
|
|
66
|
+
body > div > img[width="1"][height="1"] {
|
|
67
|
+
display: inline !important;
|
|
68
|
+
}
|
|
69
|
+
.rdoc-export-preview .bn-editor pre code {
|
|
70
|
+
background: none !important;
|
|
71
|
+
color: inherit !important;
|
|
72
|
+
}
|
|
73
|
+
.rdoc-export-preview .bn-side-menu,
|
|
74
|
+
.rdoc-export-preview .bn-drag-handle,
|
|
75
|
+
.rdoc-export-preview .bn-block-actions,
|
|
76
|
+
.rdoc-export-preview .bn-toolbar,
|
|
77
|
+
.rdoc-export-preview .bn-suggestion-menu,
|
|
78
|
+
.rdoc-export-preview .bn-file-panel,
|
|
79
|
+
.rdoc-export-preview .bn-link-toolbar,
|
|
80
|
+
.rdoc-export-preview .ProseMirror-cursor,
|
|
81
|
+
.rdoc-export-preview .ProseMirror-separator,
|
|
82
|
+
.rdoc-export-preview .bn-trailing-block {
|
|
83
|
+
display: none !important;
|
|
84
|
+
}
|
|
85
|
+
/* 空块不显示占位提示文字 */
|
|
86
|
+
.rdoc-export-preview [data-placeholder]::before {
|
|
87
|
+
content: none !important;
|
|
88
|
+
}
|
|
89
|
+
/* 文件/图片块:去掉 hover 态与操作按钮的占位高度 */
|
|
90
|
+
.rdoc-export-preview .bn-file-block-content-wrapper {
|
|
91
|
+
min-height: 0 !important;
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
/** 只保留排版相关的 DOM:清掉编辑态属性与交互元素 */
|
|
96
|
+
const cloneEditorDom = (source: HTMLElement): HTMLElement => {
|
|
97
|
+
const clone = source.cloneNode(true) as HTMLElement;
|
|
98
|
+
clone.classList.remove('ProseMirror-focused');
|
|
99
|
+
clone.removeAttribute('contenteditable');
|
|
100
|
+
clone.removeAttribute('spellcheck');
|
|
101
|
+
clone.removeAttribute('translate');
|
|
102
|
+
|
|
103
|
+
clone
|
|
104
|
+
.querySelectorAll(
|
|
105
|
+
'.bn-side-menu, .bn-drag-handle, .bn-block-actions, .bn-toolbar, .bn-suggestion-menu, .bn-file-panel, .bn-link-toolbar',
|
|
106
|
+
)
|
|
107
|
+
.forEach((el) => el.remove());
|
|
108
|
+
clone.querySelectorAll('[contenteditable]').forEach((el) => {
|
|
109
|
+
el.removeAttribute('contenteditable');
|
|
110
|
+
el.removeAttribute('spellcheck');
|
|
111
|
+
});
|
|
112
|
+
clone.querySelectorAll('[data-placeholder]').forEach((el) => el.removeAttribute('data-placeholder'));
|
|
113
|
+
clone.querySelectorAll('[data-selected]').forEach((el) => el.removeAttribute('data-selected'));
|
|
114
|
+
clone.querySelectorAll('[data-drop-target]').forEach((el) => el.removeAttribute('data-drop-target'));
|
|
115
|
+
clone
|
|
116
|
+
.querySelectorAll('.ProseMirror-selectednode, .bn-block-selected')
|
|
117
|
+
.forEach((el) => el.classList.remove('ProseMirror-selectednode', 'bn-block-selected'));
|
|
118
|
+
return clone;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* BlockNote 的项目符号/序号由 .bn-block-content::before 伪元素绘制,
|
|
123
|
+
* html2canvas 不渲染伪元素,导出会丢失列表标记。
|
|
124
|
+
* 这里把伪元素算出的文本落成真实的行内元素(块内容本身是 flex 容器,标记原本就是首个 flex item)。
|
|
125
|
+
*/
|
|
126
|
+
const injectListMarkers = (root: HTMLElement): void => {
|
|
127
|
+
const contents = root.querySelectorAll<HTMLElement>(
|
|
128
|
+
'.bn-block-content[data-content-type="bulletListItem"], .bn-block-content[data-content-type="numberedListItem"]',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
contents.forEach((content) => {
|
|
132
|
+
if (content.querySelector(':scope > [data-rdoc-list-marker]')) return;
|
|
133
|
+
|
|
134
|
+
// 序号直接取 BlockNote 写在元素上的 data-index(比解析伪元素 content 更可靠);
|
|
135
|
+
// 项目符号取自伪元素(层级不同分别是 • / ◦ / ▪︎)
|
|
136
|
+
let text: string;
|
|
137
|
+
if (content.dataset.contentType === 'numberedListItem') {
|
|
138
|
+
text = `${content.getAttribute('data-index') ?? content.getAttribute('data-start') ?? '1'}.`;
|
|
139
|
+
} else {
|
|
140
|
+
const raw = window.getComputedStyle(content, '::before').content;
|
|
141
|
+
const matched = raw?.match(/^"((?:\\.|[^"\\])*)"$/);
|
|
142
|
+
text = matched ? matched[1] : '•';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const pseudo = window.getComputedStyle(content, '::before');
|
|
146
|
+
|
|
147
|
+
const marker = document.createElement('span');
|
|
148
|
+
marker.setAttribute('data-rdoc-list-marker', 'true');
|
|
149
|
+
marker.textContent = text;
|
|
150
|
+
marker.style.cssText = [
|
|
151
|
+
'display:flex',
|
|
152
|
+
'align-items:center',
|
|
153
|
+
'justify-content:center',
|
|
154
|
+
'flex:0 0 auto',
|
|
155
|
+
`min-width:${pseudo.minWidth === 'auto' ? '24px' : pseudo.minWidth}`,
|
|
156
|
+
`padding-right:${pseudo.paddingRight || '4px'}`,
|
|
157
|
+
`font-size:${pseudo.fontSize}`,
|
|
158
|
+
`line-height:${pseudo.lineHeight}`,
|
|
159
|
+
`color:${pseudo.color}`,
|
|
160
|
+
'white-space:pre',
|
|
161
|
+
].join(';');
|
|
162
|
+
content.insertBefore(marker, content.firstChild);
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 复选框在 BlockNote 里是原生 input,html2canvas 只会画出一个灰方块(不画对勾)。
|
|
168
|
+
* 导出时替换成等价的小方框 + 对勾。
|
|
169
|
+
*/
|
|
170
|
+
const replaceCheckboxes = (root: HTMLElement): void => {
|
|
171
|
+
root
|
|
172
|
+
.querySelectorAll<HTMLInputElement>(
|
|
173
|
+
'.bn-block-content[data-content-type="checkListItem"] input[type="checkbox"]',
|
|
174
|
+
)
|
|
175
|
+
.forEach((input) => {
|
|
176
|
+
// 勾选状态以块上的 data-checked 为准(与编辑器渲染的删除线样式同源)
|
|
177
|
+
const content = input.closest<HTMLElement>('.bn-block-content[data-checked]');
|
|
178
|
+
const checked = content
|
|
179
|
+
? content.getAttribute('data-checked') === 'true'
|
|
180
|
+
: input.checked;
|
|
181
|
+
const box = document.createElement('span');
|
|
182
|
+
box.setAttribute('data-rdoc-checkbox', checked ? 'checked' : 'unchecked');
|
|
183
|
+
box.textContent = checked ? '✓' : '';
|
|
184
|
+
box.style.cssText = [
|
|
185
|
+
'display:inline-flex',
|
|
186
|
+
'align-items:center',
|
|
187
|
+
'justify-content:center',
|
|
188
|
+
'flex:0 0 auto',
|
|
189
|
+
'box-sizing:border-box',
|
|
190
|
+
'width:12px',
|
|
191
|
+
'height:12px',
|
|
192
|
+
'margin:6px 8px 6px 4px',
|
|
193
|
+
'border:1px solid #9ca3af',
|
|
194
|
+
'border-radius:2px',
|
|
195
|
+
'font-size:10px',
|
|
196
|
+
'line-height:1',
|
|
197
|
+
'color:#1f2937',
|
|
198
|
+
].join(';');
|
|
199
|
+
input.replaceWith(box);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** 等待容器内图片加载完成(失败也放行,不阻塞导出) */
|
|
204
|
+
const waitForImagesLoaded = (root: HTMLElement): Promise<void> => {
|
|
205
|
+
const images = Array.from(root.querySelectorAll('img'));
|
|
206
|
+
if (images.length === 0) return Promise.resolve();
|
|
207
|
+
|
|
208
|
+
return Promise.all(
|
|
209
|
+
images.map(
|
|
210
|
+
(img) =>
|
|
211
|
+
new Promise<void>((resolve) => {
|
|
212
|
+
if (img.complete) {
|
|
213
|
+
resolve();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let settled = false;
|
|
217
|
+
const finish = () => {
|
|
218
|
+
if (settled) return;
|
|
219
|
+
settled = true;
|
|
220
|
+
window.clearTimeout(timer);
|
|
221
|
+
img.removeEventListener('load', finish);
|
|
222
|
+
img.removeEventListener('error', finish);
|
|
223
|
+
resolve();
|
|
224
|
+
};
|
|
225
|
+
const timer = window.setTimeout(finish, IMAGE_WAIT_TIMEOUT);
|
|
226
|
+
img.addEventListener('load', finish);
|
|
227
|
+
img.addEventListener('error', finish);
|
|
228
|
+
}),
|
|
229
|
+
),
|
|
230
|
+
).then(() => undefined);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 创建离屏渲染容器,返回 BlockNote 容器节点。
|
|
235
|
+
* 外层带 .richdoc-editor-host,使编辑器的 max-width / code 配色等样式在导出时同样生效。
|
|
236
|
+
*/
|
|
237
|
+
const buildOffscreenContainer = (editorElement: HTMLElement): HTMLElement => {
|
|
238
|
+
const wrapper = document.createElement('div');
|
|
239
|
+
wrapper.setAttribute(EXPORT_ROOT_ATTR, 'true');
|
|
240
|
+
wrapper.className = 'richdoc-editor-host rdoc-export-preview';
|
|
241
|
+
Object.assign(wrapper.style, {
|
|
242
|
+
position: 'fixed',
|
|
243
|
+
top: '0',
|
|
244
|
+
left: '-100000px',
|
|
245
|
+
width: `${CONTENT_WIDTH_PX}px`,
|
|
246
|
+
height: 'auto',
|
|
247
|
+
background: '#ffffff',
|
|
248
|
+
pointerEvents: 'none',
|
|
249
|
+
} as Partial<CSSStyleDeclaration>);
|
|
250
|
+
|
|
251
|
+
const styleEl = document.createElement('style');
|
|
252
|
+
styleEl.setAttribute('data-rdoc-export', 'true');
|
|
253
|
+
styleEl.textContent = RDOC_EXPORT_STYLES;
|
|
254
|
+
wrapper.appendChild(styleEl);
|
|
255
|
+
|
|
256
|
+
// 与 BlockNoteView 的真实结构一致:根节点同时带 bn-root / bn-container,
|
|
257
|
+
// 前者提供配色变量与字体,后者提供编辑器基础排版
|
|
258
|
+
const container = document.createElement('div');
|
|
259
|
+
container.className = 'bn-root bn-container';
|
|
260
|
+
container.setAttribute('data-color-scheme', 'light');
|
|
261
|
+
Object.assign(container.style, {
|
|
262
|
+
width: `${CONTENT_WIDTH_PX}px`,
|
|
263
|
+
height: 'auto',
|
|
264
|
+
background: '#ffffff',
|
|
265
|
+
overflow: 'visible',
|
|
266
|
+
} as Partial<CSSStyleDeclaration>);
|
|
267
|
+
|
|
268
|
+
// 应用级 CSS 变量定义在 App 根节点上,离屏容器挂在 body 下取不到,
|
|
269
|
+
// 这里补上富文本样式用到的变量。导出固定为浅色排版,
|
|
270
|
+
// 因此固定取浅色主题的值(暗色主题下的变量会让行内 code 底色偏深)。
|
|
271
|
+
container.style.setProperty('--color-markdown-comment', '133 133 133');
|
|
272
|
+
|
|
273
|
+
container.appendChild(cloneEditorDom(editorElement));
|
|
274
|
+
wrapper.appendChild(container);
|
|
275
|
+
document.body.appendChild(wrapper);
|
|
276
|
+
return container;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const toPdfFileName = (fileName: string): string => {
|
|
280
|
+
const base = fileName.replace(/\.rdoc$/i, '').trim() || 'document';
|
|
281
|
+
return `${base}.pdf`;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 将块文档导出为 PDF 并触发浏览器下载。
|
|
286
|
+
* @throws 导出失败时抛出异常,由调用方提示用户
|
|
287
|
+
*/
|
|
288
|
+
export const exportRDocPdf = async (options: ExportRDocPdfOptions): Promise<void> => {
|
|
289
|
+
const { editorElement, fileName, onProgress } = options;
|
|
290
|
+
if (!editorElement || editorElement.querySelectorAll('.bn-block-outer').length === 0) {
|
|
291
|
+
throw new Error('内容为空,无法导出 PDF');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const container = buildOffscreenContainer(editorElement);
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// 伪元素标记/复选框先落成真实 DOM,再参与分页测量
|
|
298
|
+
injectListMarkers(container);
|
|
299
|
+
replaceCheckboxes(container);
|
|
300
|
+
|
|
301
|
+
if (document.fonts?.ready) {
|
|
302
|
+
await document.fonts.ready.catch(() => undefined);
|
|
303
|
+
}
|
|
304
|
+
await waitForImagesLoaded(container);
|
|
305
|
+
|
|
306
|
+
// 双 raf 确保最终布局(图片尺寸、换行)生效
|
|
307
|
+
await new Promise<void>((resolve) =>
|
|
308
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// 顶层块(嵌套在其它 bn-block-outer 里的不算):分页以块/行边界为准
|
|
312
|
+
const topLevelBlocks = Array.from(container.querySelectorAll('.bn-block-outer')).filter(
|
|
313
|
+
(el) => !el.parentElement?.closest('.bn-block-outer'),
|
|
314
|
+
);
|
|
315
|
+
const slices = buildPageSlices(measureBlocks(container, topLevelBlocks), CONTENT_HEIGHT_PX);
|
|
316
|
+
if (slices.length === 0) {
|
|
317
|
+
throw new Error('内容为空,无法导出 PDF');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 依赖体积较大,按需动态加载
|
|
321
|
+
const [{ default: html2canvas }, { jsPDF }] = await Promise.all([
|
|
322
|
+
import('html2canvas'),
|
|
323
|
+
import('jspdf'),
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
const doc = new jsPDF({
|
|
327
|
+
orientation: 'portrait',
|
|
328
|
+
unit: 'mm',
|
|
329
|
+
format: 'a4',
|
|
330
|
+
compress: true,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// 注:html2canvas 1.4 的 Options 类型把大量字段声明为必填,
|
|
334
|
+
// y/height 在每次调用时补齐
|
|
335
|
+
const baseCanvasOptions = {
|
|
336
|
+
scale: SCALE,
|
|
337
|
+
x: 0,
|
|
338
|
+
width: CONTENT_WIDTH_PX,
|
|
339
|
+
backgroundColor: '#ffffff',
|
|
340
|
+
useCORS: true,
|
|
341
|
+
logging: false,
|
|
342
|
+
scrollX: window.scrollX,
|
|
343
|
+
scrollY: window.scrollY,
|
|
344
|
+
windowWidth: document.documentElement.clientWidth,
|
|
345
|
+
windowHeight: document.documentElement.clientHeight,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
for (let page = 0; page < slices.length; page++) {
|
|
349
|
+
const slice = slices[page];
|
|
350
|
+
|
|
351
|
+
const canvas = await html2canvas(container, {
|
|
352
|
+
...baseCanvasOptions,
|
|
353
|
+
y: slice.y,
|
|
354
|
+
height: slice.height,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// 使用 PNG(无损)而非 JPEG:JPEG 压缩会让 1px 细线在部分 PDF 渲染器中丢失
|
|
358
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
359
|
+
const imgWidthMm = (canvas.width / SCALE / 96) * 25.4;
|
|
360
|
+
const imgHeightMm = (canvas.height / SCALE / 96) * 25.4;
|
|
361
|
+
doc.addImage(dataUrl, 'PNG', PAGE_MARGIN_MM, PAGE_MARGIN_MM, imgWidthMm, imgHeightMm);
|
|
362
|
+
|
|
363
|
+
onProgress?.(page + 1, slices.length);
|
|
364
|
+
|
|
365
|
+
if (page < slices.length - 1) {
|
|
366
|
+
doc.addPage();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
doc.save(toPdfFileName(fileName));
|
|
371
|
+
} finally {
|
|
372
|
+
container.parentElement?.remove();
|
|
373
|
+
}
|
|
374
|
+
};
|