@jhl548/duplicate-doc-vue 0.1.0 → 0.1.1
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/README.md +71 -537
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,33 +1,8 @@
|
|
|
1
|
-
# @jhl548/duplicate-doc-vue
|
|
1
|
+
# @jhl548/duplicate-doc-vue
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
面向 Vue 3 的 Word 文档重复点编辑与高亮插件。插件只负责单个标准化文档的渲染、编辑、重复点高亮、滚动定位和编辑快照输出;主从文档编排、上传分析、导出回写、权限审批等业务流程由宿主项目处理。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- 前端 Vue3 项目如何集成插件,并保证功能和样式正常使用�?
|
|
8
|
-
- 后端需要如何处�?Word 格式转换、文本范围映射和重复点数据,保证插件可以准确高亮�?
|
|
9
|
-
|
|
10
|
-
## 适用场景
|
|
11
|
-
|
|
12
|
-
插件适合用于以下业务�?
|
|
13
|
-
|
|
14
|
-
- 后端已经�?`.doc` / `.docx` 转成可编�?HTML�?
|
|
15
|
-
- 后端或算法服务可以输出文档纯文本、结构块范围和重复点范围�?
|
|
16
|
-
- 前端需要在 Vue3 页面中展示一个或多个富文本编辑器,并定位重复内容�?
|
|
17
|
-
- 用户编辑后,前端需要把最�?HTML / plainText / Tiptap JSON 回传后端用于保存或导出�?
|
|
18
|
-
|
|
19
|
-
插件不负责:
|
|
20
|
-
|
|
21
|
-
- Word 文件上传�?
|
|
22
|
-
- 多文档主从关系编排�?
|
|
23
|
-
- 相似度算法�?
|
|
24
|
-
- DOCX 导出�?
|
|
25
|
-
- 后端格式转换�?
|
|
26
|
-
- 业务权限、审批、版本管理等系统逻辑�?
|
|
27
|
-
|
|
28
|
-
## 安装与引�?
|
|
29
|
-
|
|
30
|
-
在现�?Vue3 项目中安装插件:
|
|
5
|
+
## 安装
|
|
31
6
|
|
|
32
7
|
```bash
|
|
33
8
|
npm install @jhl548/duplicate-doc-vue
|
|
@@ -43,7 +18,7 @@ import "@jhl548/duplicate-doc-vue/style.css";
|
|
|
43
18
|
createApp(App).mount("#app");
|
|
44
19
|
```
|
|
45
20
|
|
|
46
|
-
|
|
21
|
+
## 基础用法
|
|
47
22
|
|
|
48
23
|
```vue
|
|
49
24
|
<script setup lang="ts">
|
|
@@ -58,9 +33,9 @@ import { computed, ref } from "vue";
|
|
|
58
33
|
const documentModel = ref<NormalizedDocument>({
|
|
59
34
|
documentId: "main-001",
|
|
60
35
|
role: "main",
|
|
61
|
-
fileName: "
|
|
62
|
-
html: '<h1 data-dupdoc-block="b-0"
|
|
63
|
-
plainText: "项目计划书\n
|
|
36
|
+
fileName: "main.docx",
|
|
37
|
+
html: '<h1 data-dupdoc-block="b-0">项目计划书</h1><p data-dupdoc-block="b-1">这是一段需要高亮的重复内容。</p>',
|
|
38
|
+
plainText: "项目计划书\n这是一段需要高亮的重复内容。",
|
|
64
39
|
rangeMap: [
|
|
65
40
|
{
|
|
66
41
|
blockId: "b-0",
|
|
@@ -83,13 +58,13 @@ const highlights = computed<DuplicateHighlight[]>(() => [
|
|
|
83
58
|
documentId: "main-001",
|
|
84
59
|
similarity: 0.92,
|
|
85
60
|
active: true,
|
|
86
|
-
label: "
|
|
61
|
+
label: "重复点 1",
|
|
87
62
|
ranges: [{ start: 6, end: 20, blockId: "b-1" }]
|
|
88
63
|
}
|
|
89
64
|
]);
|
|
90
65
|
|
|
91
66
|
function handleChange(payload: EditorChangePayload) {
|
|
92
|
-
console.log("
|
|
67
|
+
console.log("editor snapshot", payload);
|
|
93
68
|
}
|
|
94
69
|
</script>
|
|
95
70
|
|
|
@@ -108,258 +83,30 @@ function handleChange(payload: EditorChangePayload) {
|
|
|
108
83
|
|
|
109
84
|
### Props
|
|
110
85
|
|
|
111
|
-
`documentModel: NormalizedDocument
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
`highlights?: DuplicateHighlight[]`
|
|
116
|
-
|
|
117
|
-
当前文档需要渲染的重复点高亮。宿主页面应先根据当前文�?ID、当前重复点、主从文档关系计算出该编辑器真正需要展示的高亮数组�?
|
|
118
|
-
|
|
119
|
-
`editable?: boolean`
|
|
120
|
-
|
|
121
|
-
是否允许编辑,默�?`true`。设�?`false` 后,编辑器进入只读展示状态�?
|
|
122
|
-
|
|
123
|
-
`autofocusHighlight?: boolean`
|
|
124
|
-
|
|
125
|
-
高亮变化后是否自动滚动到当前高亮位置,默�?`true`�?
|
|
86
|
+
- `documentModel: NormalizedDocument`:当前编辑器要渲染的标准化文档,必填。
|
|
87
|
+
- `highlights?: DuplicateHighlight[]`:当前文档需要展示的重复点高亮,默认为空数组。
|
|
88
|
+
- `editable?: boolean`:是否允许编辑,默认为 `true`。
|
|
89
|
+
- `autofocusHighlight?: boolean`:高亮变化后是否自动滚动到当前高亮位置,默认为 `true`。
|
|
126
90
|
|
|
127
91
|
### Events
|
|
128
92
|
|
|
129
|
-
`change(payload: EditorChangePayload)`
|
|
93
|
+
- `change(payload: EditorChangePayload)`:编辑器内容变化时触发,包含 `documentId`、`html`、`plainText` 和可选 `json`。
|
|
94
|
+
- `ready()`:编辑器初始化完成时触发。
|
|
130
95
|
|
|
131
|
-
|
|
96
|
+
### Ref
|
|
132
97
|
|
|
133
98
|
```ts
|
|
134
|
-
interface
|
|
135
|
-
documentId: string;
|
|
136
|
-
html: string;
|
|
137
|
-
plainText: string;
|
|
138
|
-
json?: unknown;
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
`ready()`
|
|
143
|
-
|
|
144
|
-
编辑器初始化完成时触发,可用于宿主页面做埋点、加载状态切换或首次定位�?
|
|
145
|
-
|
|
146
|
-
### Ref 方法
|
|
147
|
-
|
|
148
|
-
可以通过组件 `ref` 主动读取编辑器快照:
|
|
149
|
-
|
|
150
|
-
```vue
|
|
151
|
-
<script setup lang="ts">
|
|
152
|
-
import { ref } from "vue";
|
|
153
|
-
import { DuplicateDocumentEditor, type EditorChangePayload } from "@jhl548/duplicate-doc-vue";
|
|
154
|
-
|
|
155
|
-
const editorRef = ref<{
|
|
99
|
+
interface DuplicateDocumentEditorRef {
|
|
156
100
|
getSnapshot: () => EditorChangePayload;
|
|
157
101
|
getHTML: () => string;
|
|
158
102
|
getPlainText: () => string;
|
|
159
103
|
focus: () => void;
|
|
160
|
-
} | null>(null);
|
|
161
|
-
|
|
162
|
-
function submit() {
|
|
163
|
-
const snapshot = editorRef.value?.getSnapshot();
|
|
164
|
-
if (!snapshot) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
console.log(snapshot.html, snapshot.plainText, snapshot.json);
|
|
169
104
|
}
|
|
170
|
-
</script>
|
|
171
|
-
|
|
172
|
-
<template>
|
|
173
|
-
<DuplicateDocumentEditor ref="editorRef" :document-model="documentModel" />
|
|
174
|
-
</template>
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## 前端页面推荐数据结构
|
|
178
|
-
|
|
179
|
-
真实业务中建议后端一次返回页面渲染所需的完整结构化数据,避免前端通过多个接口拼装页面。页面可以只保留少量交互状态,例如当前选中的重复点、当前从文档、编辑缓存�?
|
|
180
|
-
|
|
181
|
-
推荐接口�?
|
|
182
|
-
|
|
183
|
-
```http
|
|
184
|
-
GET /api/projects/{projectId}/duplicate-doc-view
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
推荐响应外壳�?
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
interface ApiResponse<T> {
|
|
191
|
-
code: number;
|
|
192
|
-
message: string;
|
|
193
|
-
data: T;
|
|
194
|
-
meta?: {
|
|
195
|
-
traceId?: string;
|
|
196
|
-
timestamp?: string;
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
推荐 `data` 结构�?
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
interface DuplicateDocViewData {
|
|
205
|
-
project: {
|
|
206
|
-
projectId: string;
|
|
207
|
-
name: string;
|
|
208
|
-
mainDocumentId: string;
|
|
209
|
-
};
|
|
210
|
-
documents: NormalizedDocument[];
|
|
211
|
-
duplicates: DuplicatePoint[];
|
|
212
|
-
activeDefaults: {
|
|
213
|
-
duplicateId?: string;
|
|
214
|
-
slaveDocumentId?: string;
|
|
215
|
-
};
|
|
216
|
-
stats: {
|
|
217
|
-
documentTotal: number;
|
|
218
|
-
duplicateTotal: number;
|
|
219
|
-
rangeMapTotal: number;
|
|
220
|
-
plainTextTotal: number;
|
|
221
|
-
};
|
|
222
|
-
capabilities: {
|
|
223
|
-
supportedFileTypes: string[];
|
|
224
|
-
supportedContentTypes: string[];
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
前端收到该结构后,按当前选中�?`duplicateId` 和文�?ID 生成插件入参�?
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
function buildMainHighlights(duplicate: DuplicatePoint, mainDocumentId: string): DuplicateHighlight[] {
|
|
233
|
-
return [
|
|
234
|
-
{
|
|
235
|
-
duplicateId: duplicate.duplicateId,
|
|
236
|
-
documentId: mainDocumentId,
|
|
237
|
-
similarity: duplicate.similarity,
|
|
238
|
-
ranges: duplicate.main.ranges,
|
|
239
|
-
active: true,
|
|
240
|
-
label: duplicate.label,
|
|
241
|
-
reason: duplicate.reason,
|
|
242
|
-
ignored: duplicate.ignored,
|
|
243
|
-
severity: duplicate.severity,
|
|
244
|
-
semanticType: duplicate.semanticType,
|
|
245
|
-
region: duplicate.region,
|
|
246
|
-
noiseReason: duplicate.noiseReason
|
|
247
|
-
}
|
|
248
|
-
];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function buildSlaveHighlights(duplicate: DuplicatePoint, slaveDocumentId: string): DuplicateHighlight[] {
|
|
252
|
-
const slave = duplicate.slaves.find((item) => item.documentId === slaveDocumentId);
|
|
253
|
-
if (!slave) {
|
|
254
|
-
return [];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return [
|
|
258
|
-
{
|
|
259
|
-
duplicateId: duplicate.duplicateId,
|
|
260
|
-
documentId: slaveDocumentId,
|
|
261
|
-
similarity: duplicate.similarity,
|
|
262
|
-
ranges: slave.ranges,
|
|
263
|
-
active: true,
|
|
264
|
-
label: duplicate.label,
|
|
265
|
-
reason: duplicate.reason,
|
|
266
|
-
ignored: duplicate.ignored,
|
|
267
|
-
severity: duplicate.severity,
|
|
268
|
-
semanticType: duplicate.semanticType,
|
|
269
|
-
region: duplicate.region,
|
|
270
|
-
noiseReason: duplicate.noiseReason
|
|
271
|
-
}
|
|
272
|
-
];
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## 后端数据处理总览
|
|
277
|
-
|
|
278
|
-
后端的核心职责是把原�?Word 文件转换成插件能稳定消费的数据结构。建议流程如下:
|
|
279
|
-
|
|
280
|
-
```mermaid
|
|
281
|
-
flowchart LR
|
|
282
|
-
UploadWord[Upload Word] --> ConvertHtml[Convert to HTML]
|
|
283
|
-
ConvertHtml --> NormalizeHtml[Normalize HTML Blocks]
|
|
284
|
-
NormalizeHtml --> PlainText[Build plainText]
|
|
285
|
-
NormalizeHtml --> RangeMap[Build rangeMap]
|
|
286
|
-
PlainText --> DuplicateEngine[Duplicate Engine]
|
|
287
|
-
RangeMap --> DuplicateEngine
|
|
288
|
-
DuplicateEngine --> DuplicatePoint[Build DuplicatePoint]
|
|
289
|
-
PlainText --> NormalizedDocument[Build NormalizedDocument]
|
|
290
|
-
RangeMap --> NormalizedDocument
|
|
291
|
-
NormalizedDocument --> ViewData[Build Page View Data]
|
|
292
|
-
DuplicatePoint --> ViewData
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
后端至少需要输出两类数据:
|
|
296
|
-
|
|
297
|
-
- `NormalizedDocument[]`:每个可编辑文档的标准化内容�?
|
|
298
|
-
- `DuplicatePoint[]`:重复点及其在主文档、从文档中的纯文本范围�?
|
|
299
|
-
|
|
300
|
-
## Word �?HTML 的格式转�?
|
|
301
|
-
|
|
302
|
-
### 推荐转换方式
|
|
303
|
-
|
|
304
|
-
优先使用 LibreOffice headless 转换�?
|
|
305
|
-
|
|
306
|
-
```bash
|
|
307
|
-
soffice --headless --convert-to html --outdir ./converted ./input.docx
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
优点�?
|
|
311
|
-
|
|
312
|
-
- �?`.doc` �?`.docx` 都比较友好�?
|
|
313
|
-
- 能保留更�?Word 中的标题、段落、表格、列表、图片和基础样式�?
|
|
314
|
-
- 适合服务端批量转换�?
|
|
315
|
-
|
|
316
|
-
如果服务器没�?LibreOffice,可以对 `.docx` 使用 `python-docx` 做兜底转换,但兜底通常只能保留基础段落文本,格式损失会明显增加�?
|
|
317
|
-
|
|
318
|
-
不建议后端直接把 Word 原始二进制交给前端处理。插件需要的是标准化 HTML、纯文本和范围映射,而不�?Word 文件本身�?
|
|
319
|
-
|
|
320
|
-
### 转换后的 HTML 规范�?
|
|
321
|
-
|
|
322
|
-
转换得到�?HTML 不能直接丢给插件使用,建议后端做一次规范化�?
|
|
323
|
-
|
|
324
|
-
1. 提取正文区域,忽略无关的 Word 导出样式壳�?
|
|
325
|
-
2. 按块级结构切分内容,例如 `h1`、`h2`、`h3`、`p`、`table`、`ul`、`ol`、`blockquote`、`pre`�?
|
|
326
|
-
3. 为每个结构块生成稳定�?`blockId`�?
|
|
327
|
-
4. �?HTML 节点补充 `data-dupdoc-block` 属性�?
|
|
328
|
-
5. 生成�?HTML 同步�?`plainText`�?
|
|
329
|
-
6. 生成每个块对应的 `rangeMap`�?
|
|
330
|
-
|
|
331
|
-
示例 HTML�?
|
|
332
|
-
|
|
333
|
-
```html
|
|
334
|
-
<h1 data-dupdoc-block="b-0">项目计划�?/h1>
|
|
335
|
-
<p data-dupdoc-block="b-1">投标人须提供营业执照、资质证书和类似业绩证明�?/p>
|
|
336
|
-
<table data-dupdoc-block="b-2" data-dupdoc-table="table-001">
|
|
337
|
-
<tbody>
|
|
338
|
-
<tr>
|
|
339
|
-
<th>项目名称</th>
|
|
340
|
-
<th>合同金额</th>
|
|
341
|
-
</tr>
|
|
342
|
-
<tr>
|
|
343
|
-
<td>智慧园区平台</td>
|
|
344
|
-
<td>380万元</td>
|
|
345
|
-
</tr>
|
|
346
|
-
</tbody>
|
|
347
|
-
</table>
|
|
348
105
|
```
|
|
349
106
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
```text
|
|
353
|
-
项目计划�?
|
|
354
|
-
投标人须提供营业执照、资质证书和类似业绩证明�?
|
|
355
|
-
项目名称 合同金额 智慧园区平台 380万元
|
|
356
|
-
```
|
|
107
|
+
## 数据结构约定
|
|
357
108
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
## NormalizedDocument 数据结构
|
|
361
|
-
|
|
362
|
-
每个文档必须转换�?`NormalizedDocument`�?
|
|
109
|
+
后端或宿主应用需要把 Word 文档转换为插件可消费的标准模型:
|
|
363
110
|
|
|
364
111
|
```ts
|
|
365
112
|
interface NormalizedDocument {
|
|
@@ -374,303 +121,90 @@ interface NormalizedDocument {
|
|
|
374
121
|
}
|
|
375
122
|
```
|
|
376
123
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
- `documentId`:后端生成的稳定文档 ID,同一次页面数据中必须唯一�?
|
|
380
|
-
- `role`:`main` 表示主文档,`slave` 表示从文档�?
|
|
381
|
-
- `fileName`:原始文件名或业务文件名�?
|
|
382
|
-
- `html`:可�?Tiptap 渲染和编辑的标准�?HTML�?
|
|
383
|
-
- `plainText`:算法使用的纯文本,重复�?`start/end` 必须基于它计算�?
|
|
384
|
-
- `rangeMap`:结构块与纯文本偏移之间的映射�?
|
|
385
|
-
- `assets`:图片、字体等资源信息,可选�?
|
|
386
|
-
- `meta`:转换器、原始文件名、格式损失标记等调试信息,可选�?
|
|
387
|
-
|
|
388
|
-
`rangeMap` 示例�?
|
|
389
|
-
|
|
390
|
-
```ts
|
|
391
|
-
interface RangeMapEntry {
|
|
392
|
-
blockId: string;
|
|
393
|
-
textStart: number;
|
|
394
|
-
textEnd: number;
|
|
395
|
-
selector?: string;
|
|
396
|
-
sectionPath?: string[];
|
|
397
|
-
region?: "body" | "header" | "footer" | "table" | "caption" | "unknown";
|
|
398
|
-
listMarker?: string;
|
|
399
|
-
tableContext?: TableCellContext;
|
|
400
|
-
semanticType?: TenderSemanticType;
|
|
401
|
-
}
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
注意事项�?
|
|
124
|
+
关键要求:
|
|
405
125
|
|
|
406
|
-
- `
|
|
407
|
-
-
|
|
408
|
-
-
|
|
409
|
-
- `
|
|
410
|
-
-
|
|
126
|
+
- `html` 应是 Tiptap 可解析的标准 HTML。
|
|
127
|
+
- 建议为每个块级节点添加稳定的 `data-dupdoc-block`。
|
|
128
|
+
- `plainText` 必须与算法计算重复点范围时使用的文本一致。
|
|
129
|
+
- `rangeMap` 负责把结构块和纯文本偏移关联起来,便于联调和问题排查。
|
|
130
|
+
- `DuplicateHighlight.ranges[].start/end` 使用基于 `plainText` 的左闭右开区间。
|
|
411
131
|
|
|
412
|
-
##
|
|
132
|
+
## Word 转 HTML 建议
|
|
413
133
|
|
|
414
|
-
|
|
134
|
+
推荐后端优先使用 LibreOffice headless 转换 Word。没有 LibreOffice 时,可以对 `.docx` 使用 `python-docx` 做基础文本兜底,但复杂样式、表格、图片可能丢失。
|
|
415
135
|
|
|
416
|
-
|
|
417
|
-
interface DuplicatePoint {
|
|
418
|
-
duplicateId: string;
|
|
419
|
-
groupId?: string;
|
|
420
|
-
similarity: number;
|
|
421
|
-
label?: string;
|
|
422
|
-
summary?: string;
|
|
423
|
-
reason?: string;
|
|
424
|
-
ignored?: boolean;
|
|
425
|
-
severity?: "noise" | "low" | "medium" | "high";
|
|
426
|
-
semanticType?: TenderSemanticType;
|
|
427
|
-
region?: DocumentRegion;
|
|
428
|
-
noiseReason?: string;
|
|
429
|
-
main: {
|
|
430
|
-
documentId: string;
|
|
431
|
-
ranges: TextRange[];
|
|
432
|
-
};
|
|
433
|
-
slaves: Array<{
|
|
434
|
-
documentId: string;
|
|
435
|
-
ranges: TextRange[];
|
|
436
|
-
}>;
|
|
437
|
-
}
|
|
438
|
-
```
|
|
136
|
+
推荐转换流程:
|
|
439
137
|
|
|
440
|
-
|
|
138
|
+
1. 将 Word 转为 HTML。
|
|
139
|
+
2. 清理 Word 导出的无关样式壳。
|
|
140
|
+
3. 按 `h1`、`h2`、`p`、`table`、`ul`、`ol`、`blockquote` 等块级结构生成稳定 `blockId`。
|
|
141
|
+
4. 为 HTML 节点补充 `data-dupdoc-block`。
|
|
142
|
+
5. 同步生成 `plainText` 和 `rangeMap`。
|
|
143
|
+
6. 将查重结果转换为 `DuplicatePoint[]` 或当前编辑器需要的 `DuplicateHighlight[]`。
|
|
441
144
|
|
|
442
|
-
|
|
443
|
-
interface TextRange {
|
|
444
|
-
start: number;
|
|
445
|
-
end: number;
|
|
446
|
-
blockId?: string;
|
|
447
|
-
sectionPath?: string[];
|
|
448
|
-
region?: DocumentRegion;
|
|
449
|
-
tableContext?: TableCellContext;
|
|
450
|
-
confidence?: number;
|
|
451
|
-
semanticType?: TenderSemanticType;
|
|
452
|
-
}
|
|
453
|
-
```
|
|
145
|
+
## 高亮定位规则
|
|
454
146
|
|
|
455
|
-
|
|
147
|
+
高亮是否准确主要取决于三件事:
|
|
456
148
|
|
|
457
|
-
- `
|
|
458
|
-
- `
|
|
459
|
-
-
|
|
460
|
-
- `similarity` 范围建议�?`0` �?`1`�?
|
|
461
|
-
- `ignored` �?`noiseReason` 可用于标记项目名称、页眉页脚、日期等自然重复内容�?
|
|
462
|
-
- 如果重复点落在表格中,建议带�?`tableContext`�?
|
|
149
|
+
- 后端 `plainText` 的生成规则。
|
|
150
|
+
- 后端 `DuplicatePoint.ranges[].start/end` 的计算规则。
|
|
151
|
+
- 前端 Tiptap 文档中实际文本的顺序。
|
|
463
152
|
|
|
464
|
-
|
|
153
|
+
建议统一以下规则:
|
|
465
154
|
|
|
466
|
-
|
|
155
|
+
- 每个块级节点之间使用一个 `\n` 拼接。
|
|
156
|
+
- 块内文本按视觉阅读顺序提取。
|
|
157
|
+
- 表格按行、列顺序提取单元格文本。
|
|
158
|
+
- 空块尽量不参与 `plainText`,除非业务需要保留。
|
|
159
|
+
- `start/end` 使用 JavaScript 字符串下标语义,也就是 UTF-16 code unit 偏移。
|
|
467
160
|
|
|
468
|
-
|
|
161
|
+
如果后端使用 Python 计算偏移,需要注意 emoji、生僻字和组合字符可能造成 Python 字符下标与浏览器 UTF-16 下标不一致。业务文档可能包含这类字符时,建议统一使用 UTF-16 code unit 规则计算偏移,或增加前后端契约测试。
|
|
469
162
|
|
|
470
|
-
|
|
471
|
-
- Word 标题 2 转成 `h2`�?
|
|
472
|
-
- Word 标题 3 转成 `h3`�?
|
|
473
|
-
- 普通正文转�?`p`�?
|
|
163
|
+
## 样式和子路径
|
|
474
164
|
|
|
475
|
-
|
|
165
|
+
插件样式通过子路径导出:
|
|
476
166
|
|
|
477
167
|
```ts
|
|
478
|
-
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
### 列表
|
|
482
|
-
|
|
483
|
-
有序列表和无序列表建议保留为 `ol` / `ul` / `li`。生成纯文本时需要明确列表项之间是否加入空格或换行,并保持算法与插件一致�?
|
|
484
|
-
|
|
485
|
-
建议�?
|
|
486
|
-
|
|
487
|
-
- 块级列表整体作为一个结构块�?
|
|
488
|
-
- `listMarker` 保留原始编号,例�?`1.`、`一、`、`(一)`�?
|
|
489
|
-
- 列表项文本可以合并为该块的文本�?
|
|
490
|
-
|
|
491
|
-
### 表格
|
|
492
|
-
|
|
493
|
-
表格是最容易导致偏移不一致的结构,建议后端明确处理:
|
|
494
|
-
|
|
495
|
-
- HTML 中保�?`table`、`tr`、`th`、`td`�?
|
|
496
|
-
- 给表格生�?`data-dupdoc-table`�?
|
|
497
|
-
- `plainText` 中按单元格顺序提取文本�?
|
|
498
|
-
- 单元格之间建议使用空格分隔�?
|
|
499
|
-
- 表格块在 `rangeMap` 中标�?`region: "table"`�?
|
|
500
|
-
- 重复点落在某个单元格时,尽量补充 `tableContext`�?
|
|
501
|
-
|
|
502
|
-
`tableContext` 示例�?
|
|
503
|
-
|
|
504
|
-
```ts
|
|
505
|
-
interface TableCellContext {
|
|
506
|
-
tableId: string;
|
|
507
|
-
rowIndex: number;
|
|
508
|
-
cellIndex: number;
|
|
509
|
-
headerText?: string;
|
|
510
|
-
rowHeaderText?: string;
|
|
511
|
-
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### 图片和附件资�?
|
|
515
|
-
|
|
516
|
-
如果 Word 中包含图片:
|
|
517
|
-
|
|
518
|
-
- 简单场景可以转�?base64 图片放在 HTML 中�?
|
|
519
|
-
- 生产场景建议后端上传图片资源,HTML 中引用资�?URL�?
|
|
520
|
-
- `assets` 中记录图�?ID、URL 和类型�?
|
|
521
|
-
|
|
522
|
-
示例�?
|
|
523
|
-
|
|
524
|
-
```ts
|
|
525
|
-
assets: [
|
|
526
|
-
{
|
|
527
|
-
id: "asset-001",
|
|
528
|
-
url: "https://cdn.example.com/docs/asset-001.png",
|
|
529
|
-
type: "image"
|
|
530
|
-
}
|
|
531
|
-
]
|
|
168
|
+
import "@jhl548/duplicate-doc-vue/style.css";
|
|
532
169
|
```
|
|
533
170
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
插件基于 Tiptap,建议保留以下常�?HTML�?
|
|
171
|
+
如果宿主项目存在全局 CSS reset 或 scoped 样式覆盖,请检查 `.dupdoc-editor`、`.dupdoc-editor__toolbar`、`.dupdoc-highlight` 等 class 是否被覆盖。
|
|
537
172
|
|
|
538
|
-
|
|
539
|
-
- `em` / `i`:斜体�?
|
|
540
|
-
- `u`:下划线�?
|
|
541
|
-
- `s` / `strike`:删除线�?
|
|
542
|
-
- `code`:行内代码�?
|
|
543
|
-
- `mark`:高亮�?
|
|
544
|
-
- `span style="color: ..."`:文字颜色�?
|
|
545
|
-
- `sub` / `sup`:上下标�?
|
|
546
|
-
- `a href="..."`:链接�?
|
|
173
|
+
## 发布前校验
|
|
547
174
|
|
|
548
|
-
|
|
175
|
+
本包发布前应在仓库根目录执行:
|
|
549
176
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
重复点高亮是否准确,主要取决于三件事�?
|
|
553
|
-
|
|
554
|
-
1. 后端 `plainText` 的生成规则�?
|
|
555
|
-
2. 后端 `DuplicatePoint.ranges[].start/end` 的计算规则�?
|
|
556
|
-
3. 前端 Tiptap 文档中实际文本的顺序�?
|
|
557
|
-
|
|
558
|
-
建议统一以下规则�?
|
|
559
|
-
|
|
560
|
-
- 每个块级节点之间用一�?`\n` 拼接�?
|
|
561
|
-
- 块内文本按视觉阅读顺序提取�?
|
|
562
|
-
- 表格按行、列顺序提取单元格文本�?
|
|
563
|
-
- 空块尽量不参�?`plainText`,除非业务需要保留�?
|
|
564
|
-
- `start/end` 使用 JavaScript 字符串下标语义,�?UTF-16 code unit 偏移�?
|
|
565
|
-
|
|
566
|
-
如果后端使用 Python 计算偏移,需要注意中文通常没有问题,但 emoji、生僻字、组合字符可能造成 Python 字符下标与浏览器 UTF-16 下标不一致。若业务文档可能包含这类字符,建议统一使用 UTF-16 code unit 规则计算偏移,或在前后端增加契约测试�?
|
|
567
|
-
|
|
568
|
-
## 编辑回传与导�?
|
|
569
|
-
|
|
570
|
-
用户编辑后,前端可以通过 `change` 事件或组�?`ref` 获取最新快照:
|
|
571
|
-
|
|
572
|
-
```ts
|
|
573
|
-
interface EditorChangePayload {
|
|
574
|
-
documentId: string;
|
|
575
|
-
html: string;
|
|
576
|
-
plainText: string;
|
|
577
|
-
json?: unknown;
|
|
578
|
-
}
|
|
177
|
+
```bash
|
|
178
|
+
npm run verify:publish
|
|
579
179
|
```
|
|
580
180
|
|
|
581
|
-
|
|
181
|
+
该命令会执行编码检查、类型检查、构建和 `npm pack --dry-run`。编码检查会拦截替换字符、常见中文错码片段和 Latin-1 错码片段,避免乱码 README 或源码被再次发布到 npm。
|
|
582
182
|
|
|
583
|
-
|
|
584
|
-
- 导出 DOCX:将 `html` 包装成完�?HTML 文档,再通过 LibreOffice 转成 `.docx`�?
|
|
585
|
-
|
|
586
|
-
导出建议优先使用 LibreOffice�?
|
|
183
|
+
发布到 npm 后再执行:
|
|
587
184
|
|
|
588
185
|
```bash
|
|
589
|
-
|
|
186
|
+
npm run verify:npm
|
|
590
187
|
```
|
|
591
188
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
## 最小后端伪代码
|
|
595
|
-
|
|
596
|
-
```py
|
|
597
|
-
def build_view_data(main_file, slave_files):
|
|
598
|
-
main_document = upload_to_normalized_document(main_file, role="main")
|
|
599
|
-
slave_documents = [
|
|
600
|
-
upload_to_normalized_document(file, role="slave")
|
|
601
|
-
for file in slave_files
|
|
602
|
-
]
|
|
603
|
-
|
|
604
|
-
documents = [main_document, *slave_documents]
|
|
605
|
-
duplicates = duplicate_engine(documents)
|
|
606
|
-
|
|
607
|
-
return {
|
|
608
|
-
"code": 0,
|
|
609
|
-
"message": "ok",
|
|
610
|
-
"data": {
|
|
611
|
-
"project": {
|
|
612
|
-
"projectId": "project-001",
|
|
613
|
-
"name": "查重项目",
|
|
614
|
-
"mainDocumentId": main_document["documentId"],
|
|
615
|
-
},
|
|
616
|
-
"documents": documents,
|
|
617
|
-
"duplicates": duplicates,
|
|
618
|
-
"activeDefaults": {
|
|
619
|
-
"duplicateId": duplicates[0]["duplicateId"] if duplicates else None,
|
|
620
|
-
"slaveDocumentId": duplicates[0]["slaves"][0]["documentId"] if duplicates and duplicates[0]["slaves"] else None,
|
|
621
|
-
},
|
|
622
|
-
"stats": {
|
|
623
|
-
"documentTotal": len(documents),
|
|
624
|
-
"duplicateTotal": len(duplicates),
|
|
625
|
-
"rangeMapTotal": sum(len(doc["rangeMap"]) for doc in documents),
|
|
626
|
-
"plainTextTotal": sum(len(doc["plainText"]) for doc in documents),
|
|
627
|
-
},
|
|
628
|
-
"capabilities": {
|
|
629
|
-
"supportedFileTypes": ["DOC", "DOCX"],
|
|
630
|
-
"supportedContentTypes": ["标题", "段落", "表格", "列表", "图片", "基础行内样式"],
|
|
631
|
-
},
|
|
632
|
-
},
|
|
633
|
-
"meta": {
|
|
634
|
-
"traceId": "trace-id",
|
|
635
|
-
"timestamp": "2026-05-14T16:00:00+08:00",
|
|
636
|
-
},
|
|
637
|
-
}
|
|
638
|
-
```
|
|
189
|
+
该命令会从 npm registry 拉取最新包,并让 demo 以 npm 模式完成类型检查和构建,避免 Vite alias 或 TypeScript paths 回退到本地源码。
|
|
639
190
|
|
|
640
|
-
##
|
|
191
|
+
## 联调检查清单
|
|
641
192
|
|
|
642
|
-
-
|
|
643
|
-
- `documentModel.documentId`
|
|
644
|
-
- `
|
|
645
|
-
- `
|
|
646
|
-
-
|
|
647
|
-
-
|
|
648
|
-
-
|
|
649
|
-
-
|
|
650
|
-
- 编辑�?`change` 事件能返回最�?HTML �?plainText�?
|
|
651
|
-
- 导出 DOCX 时后端能接受前端回传�?HTML�?
|
|
193
|
+
- 应用入口已导入 `@jhl548/duplicate-doc-vue/style.css`。
|
|
194
|
+
- `documentModel.documentId` 与 `highlights[].documentId` 一致。
|
|
195
|
+
- `TextRange.start/end` 不越界。
|
|
196
|
+
- `rangeMap.textStart/textEnd` 与 `plainText` 对齐。
|
|
197
|
+
- 后端块级换行规则与插件映射规则一致。
|
|
198
|
+
- 表格、列表、标题、图片在 HTML 中仍是 Tiptap 可解析结构。
|
|
199
|
+
- 编辑器 `change` 事件能返回最新 `html` 和 `plainText`。
|
|
200
|
+
- 导出 DOCX 时后端能接受前端回传的 HTML。
|
|
652
201
|
|
|
653
202
|
## 常见问题
|
|
654
203
|
|
|
655
|
-
###
|
|
656
|
-
|
|
657
|
-
优先检查后�?`plainText` 是否和插件渲染后的文本顺序一致。常见原因是表格单元格分隔符、列表项换行、空段落、HTML 实体、Word 导出冗余节点导致偏移不一致�?
|
|
658
|
-
|
|
659
|
-
### 样式没有生效怎么�?
|
|
660
|
-
|
|
661
|
-
确认应用入口已经导入�?
|
|
662
|
-
|
|
663
|
-
```ts
|
|
664
|
-
import "@jhl548/duplicate-doc-vue/style.css";
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
如果宿主项目�?CSS reset �?scoped 样式覆盖插件样式,需要检�?`.dupdoc-editor`、`.dupdoc-editor__toolbar`、`.dupdoc-highlight` �?class 是否被覆盖�?
|
|
668
|
-
|
|
669
|
-
### 能否只传 HTML,不�?plainText �?rangeMap
|
|
670
|
-
|
|
671
|
-
不建议。插件可以只�?HTML 渲染编辑器,但重复点高亮依赖 `plainText` 偏移和后端算法输出。缺�?`plainText` �?`rangeMap` 会让联调和问题排查变得困难�?
|
|
204
|
+
### 高亮位置偏移怎么办?
|
|
672
205
|
|
|
673
|
-
|
|
206
|
+
优先检查后端 `plainText` 是否和插件渲染后的文本顺序一致。常见原因包括表格单元格分隔符、列表项换行、空段落、HTML 实体、Word 导出冗余节点导致偏移不一致。
|
|
674
207
|
|
|
675
|
-
|
|
208
|
+
### 能否只传 HTML?
|
|
676
209
|
|
|
210
|
+
不建议。插件可以只用 HTML 渲染编辑器,但重复点高亮依赖 `plainText` 偏移和后端算法输出。缺少 `plainText` 和 `rangeMap` 会让联调和问题排查变得困难。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhl548/duplicate-doc-vue",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
+
"prepack": "node ../../scripts/check-encoding.mjs && npm run typecheck && npm run build",
|
|
25
26
|
"build": "vite build",
|
|
26
27
|
"typecheck": "vue-tsc -p tsconfig.json --noEmit"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@jhl548/duplicate-doc-core": "0.1.
|
|
30
|
+
"@jhl548/duplicate-doc-core": "0.1.1",
|
|
30
31
|
"@tiptap/extension-color": "^3.23.4",
|
|
31
32
|
"@tiptap/extension-highlight": "^3.23.4",
|
|
32
33
|
"@tiptap/extension-image": "^3.23.4",
|