feat: unify channel and sales product save/publish UX
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0a2ff6993d
commit
d02e580d91
@ -3444,6 +3444,8 @@ export default {
|
||||
'channel.salesProducts.tabs.records': 'Publish records',
|
||||
|
||||
'channel.salesProducts.actions.save': 'Save',
|
||||
'channel.salesProducts.actions.saveDraft': 'Save draft',
|
||||
'channel.salesProducts.actions.saveAndPublish': 'Save & publish',
|
||||
'channel.salesProducts.actions.add': 'Add',
|
||||
'channel.salesProducts.actions.edit': 'Edit',
|
||||
'channel.salesProducts.actions.delete': 'Delete',
|
||||
@ -3462,6 +3464,38 @@ export default {
|
||||
'channel.salesProducts.customerGuide.selectProduct': 'Select a sales product on the left first',
|
||||
|
||||
'channel.salesProducts.price.effectiveVersion': 'Effective version: {version}',
|
||||
'channel.salesProducts.empty.selectProduct': 'Select a product from the left',
|
||||
'channel.salesProducts.context.product': 'Product',
|
||||
'channel.salesProducts.context.group': 'Group',
|
||||
'channel.salesProducts.context.unsaved': 'Unsaved',
|
||||
'channel.salesProducts.unsaved.title': 'Unsaved changes',
|
||||
'channel.salesProducts.unsaved.message': 'This tab has unsaved changes. Switching will discard them. Continue?',
|
||||
'channel.salesProducts.unsaved.discard': 'Discard',
|
||||
'channel.salesProducts.unsaved.stay': 'Stay',
|
||||
'channel.salesProducts.saveConfirm.title': 'Save confirmation',
|
||||
'channel.salesProducts.saveConfirm.message': 'After saving, the publish preview opens. Publish only after you confirm the diff.',
|
||||
'channel.salesProducts.saveConfirm.cancel': 'Cancel',
|
||||
'channel.salesProducts.messages.saveDraftSuccess': 'Draft saved (not published)',
|
||||
'channel.salesProducts.messages.saveFailed': 'Save failed',
|
||||
'channel.salesProducts.messages.toggleDisableTitle': 'Confirm disable',
|
||||
'channel.salesProducts.messages.toggleDisableConfirm': 'This product will not be available for new orders. Disable "{name}"?',
|
||||
'channel.salesProducts.publish.title': 'Publish confirmation',
|
||||
'channel.salesProducts.publish.status.synced': 'Content matches the live cache; no republish needed',
|
||||
'channel.salesProducts.publish.status.changed': '{count} change(s) detected; confirm to update the live cache',
|
||||
'channel.salesProducts.publish.status.first': 'Not published yet; confirm to write to the live cache',
|
||||
'channel.salesProducts.publish.onlineCache': 'Live cache',
|
||||
'channel.salesProducts.publish.pendingCache': 'Pending publish',
|
||||
'channel.salesProducts.publish.diffCount': 'Diff count: {count}',
|
||||
'channel.salesProducts.publish.showOnlyDiff': 'Show diff only',
|
||||
'channel.salesProducts.publish.confirm': 'Publish',
|
||||
'channel.salesProducts.publish.noNeed': 'No publish needed',
|
||||
'channel.salesProducts.publish.noDiffDetail': 'Matches the latest published version',
|
||||
'channel.salesProducts.publish.legend.add': 'Added',
|
||||
'channel.salesProducts.publish.legend.remove': 'Removed',
|
||||
'channel.salesProducts.publish.legend.change': 'Changed',
|
||||
'channel.salesProducts.publish.legend.same': 'Unchanged',
|
||||
'channel.salesProducts.publish.badge.pending': 'Pending',
|
||||
'channel.salesProducts.publish.badge.synced': 'Synced',
|
||||
|
||||
'channel.salesProducts.rules.schemeTitle': 'Shipping scheme',
|
||||
'channel.salesProducts.rules.scheme.costLowest': 'Lowest cost',
|
||||
@ -3554,6 +3588,8 @@ export default {
|
||||
'channel.serviceChannels.tabs.price': 'Channel pricing',
|
||||
'channel.serviceChannels.tabs.record': 'Publish records',
|
||||
'channel.serviceChannels.actions.save': 'Save',
|
||||
'channel.serviceChannels.actions.saveDraft': 'Save draft',
|
||||
'channel.serviceChannels.actions.saveAndPublish': 'Save & publish',
|
||||
'channel.serviceChannels.actions.add': 'Add',
|
||||
'channel.serviceChannels.actions.edit': 'Edit',
|
||||
'channel.serviceChannels.actions.delete': 'Delete',
|
||||
@ -3612,6 +3648,43 @@ export default {
|
||||
'channel.serviceChannels.drawer.footer.generatePriceList': 'Generate price table',
|
||||
'channel.serviceChannels.drawer.footer.save': 'Save',
|
||||
'channel.serviceChannels.createTime': 'Created: {time}',
|
||||
'channel.serviceChannels.empty.selectChannel': 'Select a channel from the left',
|
||||
'channel.serviceChannels.context.channel': 'Channel',
|
||||
'channel.serviceChannels.context.group': 'Provider group',
|
||||
'channel.serviceChannels.context.app': 'Application',
|
||||
'channel.serviceChannels.context.apiProvider': 'API provider',
|
||||
'channel.serviceChannels.context.unsaved': 'Unsaved',
|
||||
'channel.serviceChannels.saveConfirm.title': 'Save confirmation',
|
||||
'channel.serviceChannels.saveConfirm.message': 'Save only keeps a draft; save & publish pushes channel rules to the live cache.',
|
||||
'channel.serviceChannels.saveConfirm.saveOnly': 'Save only',
|
||||
'channel.serviceChannels.saveConfirm.saveAndPublish': 'Save & publish',
|
||||
'channel.serviceChannels.saveConfirm.cancel': 'Cancel',
|
||||
'channel.serviceChannels.unsaved.title': 'Unsaved changes',
|
||||
'channel.serviceChannels.unsaved.message': 'Channel config has unsaved changes. Switching will discard them. Continue?',
|
||||
'channel.serviceChannels.unsaved.discard': 'Discard',
|
||||
'channel.serviceChannels.unsaved.stay': 'Stay',
|
||||
'channel.serviceChannels.messages.saveDraftSuccess': 'Draft saved (not published)',
|
||||
'channel.serviceChannels.messages.saveAndPublishSuccess': 'Saved and published',
|
||||
'channel.serviceChannels.messages.publishFailedAfterSave': 'Config saved, but publish failed: {msg}',
|
||||
'channel.serviceChannels.messages.toggleDisableConfirm': 'This channel will not be available for new orders. Disable "{name}"?',
|
||||
'channel.serviceChannels.messages.toggleDisableTitle': 'Confirm disable',
|
||||
'channel.serviceChannels.publish.title': 'Confirm price publish',
|
||||
'channel.serviceChannels.publish.status.synced': 'Pricing matches the live cache; no republish needed',
|
||||
'channel.serviceChannels.publish.status.changed': '{count} change(s) detected; confirm to update the live cache',
|
||||
'channel.serviceChannels.publish.status.first': 'No published pricing yet; confirm to write to the live cache',
|
||||
'channel.serviceChannels.publish.onlineCache': 'Live cache',
|
||||
'channel.serviceChannels.publish.pendingCache': 'Pending publish',
|
||||
'channel.serviceChannels.publish.diffCount': 'Diff count: {count}',
|
||||
'channel.serviceChannels.publish.showOnlyDiff': 'Show diff only',
|
||||
'channel.serviceChannels.publish.confirm': 'Publish',
|
||||
'channel.serviceChannels.publish.noNeed': 'No publish needed',
|
||||
'channel.serviceChannels.publish.noDiffDetail': 'Pricing matches the latest published version',
|
||||
'channel.serviceChannels.publish.legend.add': 'Added',
|
||||
'channel.serviceChannels.publish.legend.remove': 'Removed',
|
||||
'channel.serviceChannels.publish.legend.change': 'Changed',
|
||||
'channel.serviceChannels.publish.legend.same': 'Unchanged',
|
||||
'channel.serviceChannels.publish.badge.pending': 'Pending',
|
||||
'channel.serviceChannels.publish.badge.synced': 'Synced',
|
||||
|
||||
// Channel - Blacklist/Remote Zones (shared)
|
||||
'channel.area.listTitle': 'List',
|
||||
|
||||
@ -3464,6 +3464,8 @@ export default {
|
||||
'channel.salesProducts.tabs.records': '发布记录',
|
||||
|
||||
'channel.salesProducts.actions.save': '保存',
|
||||
'channel.salesProducts.actions.saveDraft': '保存草稿',
|
||||
'channel.salesProducts.actions.saveAndPublish': '保存并发布',
|
||||
'channel.salesProducts.actions.add': '新增',
|
||||
'channel.salesProducts.actions.edit': '编辑',
|
||||
'channel.salesProducts.actions.delete': '删除',
|
||||
@ -3482,6 +3484,38 @@ export default {
|
||||
'channel.salesProducts.customerGuide.selectProduct': '请先在左侧选择销售产品',
|
||||
|
||||
'channel.salesProducts.price.effectiveVersion': '生效版本: {version}',
|
||||
'channel.salesProducts.empty.selectProduct': '请从左侧选择销售产品',
|
||||
'channel.salesProducts.context.product': '当前产品',
|
||||
'channel.salesProducts.context.group': '产品分组',
|
||||
'channel.salesProducts.context.unsaved': '未保存',
|
||||
'channel.salesProducts.unsaved.title': '未保存的更改',
|
||||
'channel.salesProducts.unsaved.message': '当前 Tab 有未保存的修改,切换后将丢失。是否继续?',
|
||||
'channel.salesProducts.unsaved.discard': '放弃修改',
|
||||
'channel.salesProducts.unsaved.stay': '留在此页',
|
||||
'channel.salesProducts.saveConfirm.title': '保存确认',
|
||||
'channel.salesProducts.saveConfirm.message': '保存后将打开发布预览,确认差异后才会发布到线上缓存。',
|
||||
'channel.salesProducts.saveConfirm.cancel': '取消',
|
||||
'channel.salesProducts.messages.saveDraftSuccess': '草稿已保存(未发布)',
|
||||
'channel.salesProducts.messages.saveFailed': '保存失败',
|
||||
'channel.salesProducts.messages.toggleDisableTitle': '确认禁用',
|
||||
'channel.salesProducts.messages.toggleDisableConfirm': '禁用后该产品将无法用于新订单,是否确认禁用「{name}」?',
|
||||
'channel.salesProducts.publish.title': '发布确认',
|
||||
'channel.salesProducts.publish.status.synced': '当前内容与线上一致,无需重复发布',
|
||||
'channel.salesProducts.publish.status.changed': '检测到 {count} 处变更,确认后将更新线上缓存',
|
||||
'channel.salesProducts.publish.status.first': '尚未发布过,确认后将写入线上缓存',
|
||||
'channel.salesProducts.publish.onlineCache': '线上缓存',
|
||||
'channel.salesProducts.publish.pendingCache': '待发布内容',
|
||||
'channel.salesProducts.publish.diffCount': '差异数:{count}',
|
||||
'channel.salesProducts.publish.showOnlyDiff': '只看差异',
|
||||
'channel.salesProducts.publish.confirm': '确认发布',
|
||||
'channel.salesProducts.publish.noNeed': '无需发布',
|
||||
'channel.salesProducts.publish.noDiffDetail': '与最新发布版本一致',
|
||||
'channel.salesProducts.publish.legend.add': '新增',
|
||||
'channel.salesProducts.publish.legend.remove': '删除',
|
||||
'channel.salesProducts.publish.legend.change': '修改',
|
||||
'channel.salesProducts.publish.legend.same': '不变',
|
||||
'channel.salesProducts.publish.badge.pending': '待发布',
|
||||
'channel.salesProducts.publish.badge.synced': '已同步',
|
||||
|
||||
'channel.salesProducts.rules.schemeTitle': '排货方案',
|
||||
'channel.salesProducts.rules.scheme.costLowest': '成本最低',
|
||||
@ -3573,6 +3607,8 @@ export default {
|
||||
'channel.serviceChannels.tabs.price': '渠道买价',
|
||||
'channel.serviceChannels.tabs.record': '发布记录',
|
||||
'channel.serviceChannels.actions.save': '保存',
|
||||
'channel.serviceChannels.actions.saveDraft': '保存草稿',
|
||||
'channel.serviceChannels.actions.saveAndPublish': '保存并发布',
|
||||
'channel.serviceChannels.actions.add': '新增',
|
||||
'channel.serviceChannels.actions.edit': '编辑',
|
||||
'channel.serviceChannels.actions.delete': '删除',
|
||||
@ -3633,6 +3669,43 @@ export default {
|
||||
'channel.serviceChannels.drawer.footer.generatePriceList': '生成价格表',
|
||||
'channel.serviceChannels.drawer.footer.save': '保存',
|
||||
'channel.serviceChannels.createTime': '创建: {time}',
|
||||
'channel.serviceChannels.empty.selectChannel': '请从左侧选择渠道',
|
||||
'channel.serviceChannels.context.channel': '当前渠道',
|
||||
'channel.serviceChannels.context.group': '服务商分组',
|
||||
'channel.serviceChannels.context.app': '应用',
|
||||
'channel.serviceChannels.context.apiProvider': 'API 服务商',
|
||||
'channel.serviceChannels.context.unsaved': '未保存',
|
||||
'channel.serviceChannels.saveConfirm.title': '保存确认',
|
||||
'channel.serviceChannels.saveConfirm.message': '仅保存不会发布到线上缓存;保存并发布会立即发布渠道规则。',
|
||||
'channel.serviceChannels.saveConfirm.saveOnly': '仅保存',
|
||||
'channel.serviceChannels.saveConfirm.saveAndPublish': '保存并发布',
|
||||
'channel.serviceChannels.saveConfirm.cancel': '取消',
|
||||
'channel.serviceChannels.unsaved.title': '未保存的更改',
|
||||
'channel.serviceChannels.unsaved.message': '渠道配置有未保存的修改,切换后将丢失。是否继续?',
|
||||
'channel.serviceChannels.unsaved.discard': '放弃修改',
|
||||
'channel.serviceChannels.unsaved.stay': '留在此页',
|
||||
'channel.serviceChannels.messages.saveDraftSuccess': '草稿已保存(未发布)',
|
||||
'channel.serviceChannels.messages.saveAndPublishSuccess': '保存并发布成功',
|
||||
'channel.serviceChannels.messages.publishFailedAfterSave': '配置已保存,但发布失败:{msg}',
|
||||
'channel.serviceChannels.messages.toggleDisableConfirm': '禁用后该渠道将无法用于新订单,是否确认禁用「{name}」?',
|
||||
'channel.serviceChannels.messages.toggleDisableTitle': '确认禁用',
|
||||
'channel.serviceChannels.publish.title': '价格发布确认',
|
||||
'channel.serviceChannels.publish.status.synced': '当前买价与线上一致,无需重复发布',
|
||||
'channel.serviceChannels.publish.status.changed': '检测到 {count} 处变更,确认后将更新线上缓存',
|
||||
'channel.serviceChannels.publish.status.first': '尚未发布过买价,确认后将写入线上缓存',
|
||||
'channel.serviceChannels.publish.onlineCache': '线上缓存',
|
||||
'channel.serviceChannels.publish.pendingCache': '待发布内容',
|
||||
'channel.serviceChannels.publish.diffCount': '差异数:{count}',
|
||||
'channel.serviceChannels.publish.showOnlyDiff': '只看差异',
|
||||
'channel.serviceChannels.publish.confirm': '确认发布',
|
||||
'channel.serviceChannels.publish.noNeed': '无需发布',
|
||||
'channel.serviceChannels.publish.noDiffDetail': '买价配置与最新发布版本一致',
|
||||
'channel.serviceChannels.publish.legend.add': '新增',
|
||||
'channel.serviceChannels.publish.legend.remove': '删除',
|
||||
'channel.serviceChannels.publish.legend.change': '修改',
|
||||
'channel.serviceChannels.publish.legend.same': '不变',
|
||||
'channel.serviceChannels.publish.badge.pending': '待发布',
|
||||
'channel.serviceChannels.publish.badge.synced': '已同步',
|
||||
|
||||
// 渠道 - 黑名单区/偏远分区(共用)
|
||||
'channel.area.listTitle': '列表',
|
||||
|
||||
101
src/utils/publishSnapshotDiff.ts
Normal file
101
src/utils/publishSnapshotDiff.ts
Normal file
@ -0,0 +1,101 @@
|
||||
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')
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user