front/src/utils/publishSnapshotDiff.ts
zzh d02e580d91 feat: unify channel and sales product save/publish UX
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 11:25:35 +08:00

102 lines
3.0 KiB
TypeScript

import { diffJson } from 'diff'
/** 统一键名大小写,避免预览(camelCase)与落库(PascalCase)误判差异 */
function canonicalKey(key: string): string {
if (!key) return key
return key.charAt(0).toLowerCase() + key.slice(1)
}
/** 递归规范化快照对象,便于语义对比 */
export function normalizeSnapshotForDiff(input: unknown): unknown {
if (input === null || input === undefined) return input
if (Array.isArray(input)) {
return input.map((item) => normalizeSnapshotForDiff(item))
}
if (typeof input === 'object') {
const record = input as Record<string, unknown>
const keys = Object.keys(record).sort((a, b) =>
canonicalKey(a).localeCompare(canonicalKey(b))
)
const out: Record<string, unknown> = {}
for (const key of keys) {
out[canonicalKey(key)] = normalizeSnapshotForDiff(record[key])
}
return out
}
return input
}
export function parseSnapshotJson(raw: unknown): unknown {
if (raw == null || raw === '') return {}
if (typeof raw === 'string') {
try {
return JSON.parse(raw)
} catch {
return {}
}
}
return raw
}
export function formatSnapshotJson(raw: unknown): string {
try {
const parsed = parseSnapshotJson(raw)
return JSON.stringify(parsed, null, 2)
} catch {
return typeof raw === 'string' ? raw : '{}'
}
}
export type SnapshotDiffRow =
| { type: 'add' | 'remove' | 'same'; text: string }
| { type: 'change'; oldText: string; newText: string }
function flattenDiffParts(diff: ReturnType<typeof diffJson>) {
const flat: Array<{ text: string; status: 'add' | 'remove' | 'same' }> = []
diff.forEach((part) => {
const status = part.added ? 'add' : part.removed ? 'remove' : 'same'
const lines = (part.value || '').split('\n')
lines.forEach((line, idx) => {
if (idx === lines.length - 1 && line === '') return
flat.push({ text: line, status })
})
})
const merged: SnapshotDiffRow[] = []
let i = 0
while (i < flat.length) {
const curr = flat[i]
const next = flat[i + 1]
if (curr?.status === 'remove' && next?.status === 'add') {
merged.push({ type: 'change', oldText: curr.text, newText: next.text })
i += 2
} else if (curr) {
merged.push({ type: curr.status, text: curr.text })
i += 1
} else {
i += 1
}
}
return merged
}
export function computeSnapshotDiff(
originalRaw: unknown,
modifiedRaw: unknown
): { length: number; rows: SnapshotDiffRow[] } {
try {
const original = normalizeSnapshotForDiff(parseSnapshotJson(originalRaw))
const modified = normalizeSnapshotForDiff(parseSnapshotJson(modifiedRaw))
const diff = diffJson(original, modified)
const rows = flattenDiffParts(diff)
const length = diff.filter((item) => item.added || item.removed).length
return { length, rows }
} catch {
return { length: 0, rows: [] }
}
}
export function filterDiffRows(rows: SnapshotDiffRow[], showOnlyDiff: boolean) {
if (!showOnlyDiff) return rows
return rows.filter((row) => row.type !== 'same')
}