This commit is contained in:
@@ -42,7 +42,44 @@ function imageSignature(pkg: StorePackageItemDto | StorePackageViewDto) {
|
||||
return normalizeStorePackageImageUrls(pkg).join('|');
|
||||
}
|
||||
|
||||
type FieldChange = { label: string; old: string; now: string };
|
||||
type FieldChange = { label: string; old: string; now: string; kind: 'text' | 'value' };
|
||||
|
||||
/** 文本逐字差异:LCS 比对,产出 equal / delete / insert 段落,用于高亮具体变了哪些字 */
|
||||
function diffText(a: string, b: string): Array<{ type: 'equal' | 'insert' | 'delete'; text: string }> {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
||||
for (let i = m - 1; i >= 0; i--) {
|
||||
for (let j = n - 1; j >= 0; j--) {
|
||||
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
||||
}
|
||||
}
|
||||
const raw: Array<{ type: 'equal' | 'insert' | 'delete'; text: string }> = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
while (i < m && j < n) {
|
||||
if (a[i] === b[j]) {
|
||||
raw.push({ type: 'equal', text: a[i] });
|
||||
i++;
|
||||
j++;
|
||||
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
||||
raw.push({ type: 'delete', text: a[i] });
|
||||
i++;
|
||||
} else {
|
||||
raw.push({ type: 'insert', text: b[j] });
|
||||
j++;
|
||||
}
|
||||
}
|
||||
while (i < m) raw.push({ type: 'delete', text: a[i++] });
|
||||
while (j < n) raw.push({ type: 'insert', text: b[j++] });
|
||||
const merged: Array<{ type: 'equal' | 'insert' | 'delete'; text: string }> = [];
|
||||
for (const s of raw) {
|
||||
const last = merged[merged.length - 1];
|
||||
if (last && last.type === s.type) last.text += s.text;
|
||||
else merged.push({ ...s });
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/** 逐字段比较套餐内容,返回发生变化的字段明细(用于“变动的地方详细列出”) */
|
||||
function fieldChanges(
|
||||
@@ -51,18 +88,21 @@ function fieldChanges(
|
||||
): FieldChange[] {
|
||||
const changes: FieldChange[] = [];
|
||||
const text = (v: string | number | null | undefined) => (v ?? '').toString().trim();
|
||||
const pushIf = (label: string, oldV: string, newV: string) => {
|
||||
if (oldV !== newV) changes.push({ label, old: oldV || '(空)', now: newV || '(空)' });
|
||||
const pushText = (label: string, oldV: string, newV: string) => {
|
||||
if (oldV !== newV) changes.push({ label, old: oldV, now: newV, kind: 'text' });
|
||||
};
|
||||
pushIf('价格', `¥${text(live.price)}`, `¥${text(proposed.price)}`);
|
||||
pushIf('套餐名称', text(live.name), text(proposed.name));
|
||||
pushIf('菜品内容', text(live.dishes), text(proposed.dishes));
|
||||
pushIf('可用时间', text(live.usableTime), text(proposed.usableTime));
|
||||
pushIf('其他说明', text(live.otherNotes), text(proposed.otherNotes));
|
||||
const pushValue = (label: string, oldV: string, newV: string) => {
|
||||
if (oldV !== newV) changes.push({ label, old: oldV || '(空)', now: newV || '(空)', kind: 'value' });
|
||||
};
|
||||
pushValue('价格', `¥${text(live.price)}`, `¥${text(proposed.price)}`);
|
||||
pushText('套餐名称', text(live.name), text(proposed.name));
|
||||
pushText('菜品内容', text(live.dishes), text(proposed.dishes));
|
||||
pushText('可用时间', text(live.usableTime), text(proposed.usableTime));
|
||||
pushText('其他说明', text(live.otherNotes), text(proposed.otherNotes));
|
||||
const liveImgs = normalizeStorePackageImageUrls(live);
|
||||
const proposedImgs = normalizeStorePackageImageUrls(proposed);
|
||||
if (imageSignature(live) !== imageSignature(proposed)) {
|
||||
changes.push({ label: '图片', old: `${liveImgs.length} 张`, now: `${proposedImgs.length} 张` });
|
||||
changes.push({ label: '图片', old: `${liveImgs.length} 张`, now: `${proposedImgs.length} 张`, kind: 'value' });
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
@@ -112,6 +152,43 @@ const CHANGE_LABELS = {
|
||||
unchanged: { text: '未变', color: 'default' },
|
||||
} as const;
|
||||
|
||||
/** 文本逐字差异渲染:原行红色删除线标出被删的字,新行绿色标出新增的字 */
|
||||
function TextDiff({ oldText, newText }: { oldText: string; newText: string }) {
|
||||
const segs = diffText(oldText, newText);
|
||||
return (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<div style={{ lineHeight: 1.6 }}>
|
||||
<Typography.Text type="secondary">原:</Typography.Text>
|
||||
{segs
|
||||
.filter((s) => s.type !== 'insert')
|
||||
.map((s, idx) =>
|
||||
s.type === 'delete' ? (
|
||||
<Typography.Text key={idx} delete style={{ color: '#cf1322' }}>
|
||||
{s.text || '(空)'}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text key={idx}>{s.text}</Typography.Text>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div style={{ lineHeight: 1.6 }}>
|
||||
<Typography.Text type="secondary">新:</Typography.Text>
|
||||
{segs
|
||||
.filter((s) => s.type !== 'delete')
|
||||
.map((s, idx) =>
|
||||
s.type === 'insert' ? (
|
||||
<Typography.Text key={idx} style={{ color: '#389e0d' }}>
|
||||
{s.text || '(空)'}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text key={idx}>{s.text}</Typography.Text>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PackageDetailCard({
|
||||
title,
|
||||
pkg,
|
||||
@@ -198,13 +275,19 @@ function PackageDetailCard({
|
||||
</Typography.Text>
|
||||
<ul style={{ margin: '6px 0 0', paddingLeft: 18 }}>
|
||||
{changes.map((c) => (
|
||||
<li key={c.label} style={{ marginBottom: 4 }}>
|
||||
<li key={c.label} style={{ marginBottom: 6 }}>
|
||||
<Typography.Text type="secondary">{c.label}:</Typography.Text>
|
||||
<Typography.Text delete type="secondary">
|
||||
{c.old}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary"> → </Typography.Text>
|
||||
<Typography.Text strong>{c.now}</Typography.Text>
|
||||
{c.kind === 'text' ? (
|
||||
<TextDiff oldText={c.old} newText={c.now} />
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text delete type="secondary">
|
||||
{c.old}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary"> → </Typography.Text>
|
||||
<Typography.Text strong>{c.now}</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user