From 08d4e2a2633e8f73b64d5735172ee0e6f414c2db Mon Sep 17 00:00:00 2001 From: hanshiyang Date: Wed, 3 Jun 2026 20:32:16 +0800 Subject: [PATCH] feat: improve ocd conflict merge ids --- package.json | 1 + pnpm-lock.yaml | 3 + src/editor/core/draw/Draw.ts | 3 +- src/editor/core/worker/WorkerManager.ts | 3 +- src/main.ts | 22 ++++-- src/utils/ocd.ts | 100 ++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 src/utils/ocd.ts diff --git a/package.json b/package.json index 92f3f68..59ef451 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "vue": "^3.2.45" }, "dependencies": { + "nanoid": "3.3.11", "prismjs": "^1.27.0" }, "simple-git-hooks": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7839a57..9df3261 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + nanoid: + specifier: 3.3.11 + version: 3.3.11 prismjs: specifier: ^1.27.0 version: 1.30.0 diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index b10940e..357f474 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -29,6 +29,7 @@ import { } from '../../interface/Element' import { IRow, IRowElement } from '../../interface/Row' import { deepClone, getUUID, nextTick } from '../../utils' +import { prepareOcdDocumentForSave } from '../../../utils/ocd' import { Cursor } from '../cursor/Cursor' import { CanvasEvent } from '../event/CanvasEvent' import { GlobalEvent } from '../event/GlobalEvent' @@ -1196,7 +1197,7 @@ export class Draw { } return { version, - data, + data: prepareOcdDocumentForSave(data), options: deepClone(this.options) } } diff --git a/src/editor/core/worker/WorkerManager.ts b/src/editor/core/worker/WorkerManager.ts index 09a19d6..89fd620 100644 --- a/src/editor/core/worker/WorkerManager.ts +++ b/src/editor/core/worker/WorkerManager.ts @@ -8,6 +8,7 @@ import { ICatalog } from '../../interface/Catalog' import { IEditorResult } from '../../interface/Editor' import { IGetValueOption } from '../../interface/Draw' import { deepClone } from '../../utils' +import { prepareOcdDocumentForSave } from '../../../utils/ocd' export class WorkerManager { private draw: Draw @@ -78,7 +79,7 @@ export class WorkerManager { this.valueWorker.onmessage = evt => { resolve({ version, - data: evt.data, + data: prepareOcdDocumentForSave(evt.data), options: deepClone(this.draw.getOptions()) }) } diff --git a/src/main.ts b/src/main.ts index 53838f6..d2283fb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,6 +26,10 @@ import { Dialog } from './components/dialog/Dialog' import { formatPrismToken } from './utils/prism' import { Signature } from './components/signature/Signature' import { debounce, nextTick, scrollIntoView } from './utils' +import { + prepareOcdDocumentForOpen, + prepareOcdDocumentForSave +} from './utils/ocd' /** * 从本地文件URL中提取文件路径 @@ -249,7 +253,7 @@ window.onload = function () { } catch { editorData = { main: [{ value: text }] } } - instance.command.executeSetValue(editorData) + instance.command.executeSetValue(prepareOcdDocumentForOpen(editorData)) console.log('[iframe] 通过 Electron 加载本地文件完成:', localFilePath) } catch (err) { console.error('[iframe] 本地文件解析失败:', err) @@ -273,7 +277,7 @@ window.onload = function () { } catch { editorData = { main: [{ value: text }] } } - instance.command.executeSetValue(editorData) + instance.command.executeSetValue(prepareOcdDocumentForOpen(editorData)) console.log('[iframe] 根据 filePath 加载完成:', url) } catch (err) { console.error('[iframe] 加载失败:', err) @@ -334,7 +338,7 @@ window.onload = function () { } // 设置编辑器内容 - instance.command.executeSetValue(editorData) + instance.command.executeSetValue(prepareOcdDocumentForOpen(editorData)) console.log('文档已打开') } catch (error) { console.error('打开文档失败:', error) @@ -358,7 +362,11 @@ window.onload = function () { try { // 获取编辑器内容 const editorValue = instance.command.getValue() - const content = JSON.stringify(editorValue.data, null, 2) + const content = JSON.stringify( + prepareOcdDocumentForSave(editorValue.data), + null, + 2 + ) // 创建下载链接 const blob = new Blob([content], { type: 'application/json' }) @@ -391,7 +399,11 @@ window.onload = function () { saveCallbackDom.onclick = function () { try { const editorValue = instance.command.getValue() - const content = JSON.stringify(editorValue.data, null, 2) + const content = JSON.stringify( + prepareOcdDocumentForSave(editorValue.data), + null, + 2 + ) const parentWin: any = window.parent || window const viewer = diff --git a/src/utils/ocd.ts b/src/utils/ocd.ts new file mode 100644 index 0000000..ceae514 --- /dev/null +++ b/src/utils/ocd.ts @@ -0,0 +1,100 @@ +import { nanoid } from 'nanoid' +import { IEditorData } from '../editor' + +const CONFLICT_MARKER_HIGHLIGHT = '#fff3cd' +const CONFLICT_MARKER_COLOR = '#b42318' +const CONFLICT_MARKER_REG = /^(<<<<<<<|=======|>>>>>>>)\b/ + +type UnknownRecord = Record + +function isRecord(value: unknown): value is UnknownRecord { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} + +function isElementList(value: unknown): value is UnknownRecord[] { + return Array.isArray(value) && value.every(isRecord) +} + +function isConflictMarkerText(value: unknown) { + return typeof value === 'string' && CONFLICT_MARKER_REG.test(value.trim()) +} + +function normalizeConflictElement(element: UnknownRecord, isMarker: boolean) { + if (isMarker) { + element.highlight = CONFLICT_MARKER_HIGHLIGHT + element.color = CONFLICT_MARKER_COLOR + element.bold = true + return + } + if (element.highlight === CONFLICT_MARKER_HIGHLIGHT) { + delete element.highlight + } + if (element.color === CONFLICT_MARKER_COLOR) { + delete element.color + } +} + +function prepareElementList(elementList: UnknownRecord[]) { + for (const element of elementList) { + if (isElementList(element.valueList)) { + const isMarker = + element._ocdConflictMarker === true || + element.valueList.some(item => isConflictMarkerText(item.value)) + for (const item of element.valueList) { + if (typeof item.id !== 'string' || !item.id) { + item.id = nanoid() + } + normalizeConflictElement(item, isMarker || isConflictMarkerText(item.value)) + prepareNestedElement(item) + } + } + prepareNestedElement(element) + } +} + +function prepareNestedElement(element: UnknownRecord) { + const trList = element.trList + if (Array.isArray(trList)) { + for (const tr of trList) { + if (!isRecord(tr) || !Array.isArray(tr.tdList)) continue + for (const td of tr.tdList) { + if (!isRecord(td) || !isElementList(td.value)) continue + prepareElementList(td.value) + } + } + } + + const control = element.control + if (isRecord(control) && isElementList(control.value)) { + prepareElementList(control.value) + } +} + +function cloneEditorData(data: IEditorData): IEditorData { + if (typeof structuredClone === 'function') { + return structuredClone(data) as IEditorData + } + return JSON.parse(JSON.stringify(data)) as IEditorData +} + +export function prepareOcdDocumentForSave(data: IEditorData): IEditorData { + const prepared = cloneEditorData(data) + ;(['header', 'main', 'footer'] as const).forEach(key => { + const elementList = prepared[key] + if (isElementList(elementList)) { + prepareElementList(elementList) + } + }) + return prepared +} + +export function prepareOcdDocumentForOpen(data: IEditorData): IEditorData { + if (!isRecord(data)) return data + ;(['header', 'main', 'footer'] as const).forEach(key => { + const elementList = data[key] + if (isElementList(elementList)) { + prepareElementList(elementList) + } + }) + return data +}