mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-04 02:54:20 +08:00
feat: form 新增 showDiff prop 支持自定义对比判断
- form: MForm/Container 新增 showDiff prop,允许调用方自定义
'是否展示对比内容' 的判断逻辑,并在嵌套 Container 中自动透传;
不传时沿用默认的 isEqual 行为
- editor: CompareForm 利用该能力处理 code-select 字段中 '' 与
{ hookType: 'code', hookData: [] } 两种语义为空形态被 isEqual 误判为差异的问题
- docs: 补充 form-props.md 中 showDiff 的说明与示例
- test: 补充 Code 字段相关单测
This commit is contained in:
parent
c854dfa8bf
commit
f0c66427b8
@ -85,6 +85,44 @@
|
|||||||
|
|
||||||
- **类型:** `boolean`
|
- **类型:** `boolean`
|
||||||
|
|
||||||
|
## showDiff
|
||||||
|
|
||||||
|
- **详情:**
|
||||||
|
|
||||||
|
自定义“是否展示对比内容”的判断函数(仅在 `isCompare === true` 时生效)。
|
||||||
|
|
||||||
|
- 不传:使用默认逻辑 `!isEqual(curValue, lastValue)`(基于 lodash `isEqual`);
|
||||||
|
- 传函数:完全以函数返回值为准,返回 `true` 才展示前后两份对比内容。
|
||||||
|
|
||||||
|
该 prop 通过 `formState` 透传到所有层级的 Container 中,调用方只需在 MForm 这一层传一次即可对整棵表单生效。
|
||||||
|
|
||||||
|
典型场景:某些字段语义上相等但结构不同(例如 `code-select` 字段中 `''` 与 `{ hookType: 'code', hookData: [] }` 应视为相等),调用方在此处显式声明,避免被 `isEqual` 误判为差异。
|
||||||
|
|
||||||
|
- **类型:** `(data: { curValue: any; lastValue: any; config: FormItemConfig }) => boolean`
|
||||||
|
|
||||||
|
- **示例:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<m-form :config="config" :is-compare="true" :last-values="lastValues" :show-diff="showDiff"></m-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
|
const showDiff = ({ curValue, lastValue, config }) => {
|
||||||
|
if (config?.type === 'code-select') {
|
||||||
|
// 业务侧自定义:双方都是“空形态”视为相等,不展示对比
|
||||||
|
const isEmpty = (v) =>
|
||||||
|
v === '' || v === undefined || v === null ||
|
||||||
|
(typeof v === 'object' && v.hookType === 'code' && Array.isArray(v.hookData) && v.hookData.length === 0);
|
||||||
|
if (isEmpty(curValue) && isEmpty(lastValue)) return false;
|
||||||
|
}
|
||||||
|
return !isEqual(curValue, lastValue);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
## parentValues
|
## parentValues
|
||||||
|
|
||||||
- **详情:** 父级表单值
|
- **详情:** 父级表单值
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
:disabled="true"
|
:disabled="true"
|
||||||
:label-width="labelWidth"
|
:label-width="labelWidth"
|
||||||
:extend-state="extendState"
|
:extend-state="extendState"
|
||||||
|
:show-diff="showDiff"
|
||||||
></MForm>
|
></MForm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -124,6 +125,33 @@ const wrapperStyle = computed(() => {
|
|||||||
} as Record<string, string>;
|
} as Record<string, string>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `code-select` 字段在历史数据中存在两种"语义为空"的形态:
|
||||||
|
* - 字符串 `''`(旧数据 / 用户从未配置过钩子);
|
||||||
|
* - `{ hookType: HookType.CODE, hookData: [] }`(CodeSelect.vue 在挂载时
|
||||||
|
* 写入的默认结构,参见 packages/editor/src/fields/CodeSelect.vue 中
|
||||||
|
* `props.model[props.name] = { hookType: HookType.CODE, hookData: [] }`)。
|
||||||
|
*
|
||||||
|
* 直接 `isEqual` 会把两者判为不等,从而在历史对比里对每个未配置过钩子的组件
|
||||||
|
* 都展示一份"差异",体验很糟糕。这里把它们视为相等,跳过对比。
|
||||||
|
*
|
||||||
|
* 其它类型字段沿用 MForm/Container 的默认 `!isEqual` 判断逻辑。
|
||||||
|
*/
|
||||||
|
const isEmptyCodeSelectValue = (v: any): boolean => {
|
||||||
|
if (v === '' || v === undefined || v === null) return true;
|
||||||
|
return typeof v === 'object' && v.hookType === HookType.CODE && Array.isArray(v.hookData) && v.hookData.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDiff = ({ curValue, lastValue, config }: { curValue: any; lastValue: any; config: any }) => {
|
||||||
|
if (config?.type === 'code-select') {
|
||||||
|
// 双方都是"空形态",视为相等,不展示对比
|
||||||
|
if (isEmptyCodeSelectValue(curValue) && isEmptyCodeSelectValue(lastValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !isEqual(curValue, lastValue);
|
||||||
|
};
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
switch (props.category) {
|
switch (props.category) {
|
||||||
case 'node': {
|
case 'node': {
|
||||||
|
|||||||
@ -3,33 +3,245 @@
|
|||||||
*
|
*
|
||||||
* Copyright (C) 2025 Tencent.
|
* Copyright (C) 2025 Tencent.
|
||||||
*/
|
*/
|
||||||
import { describe, expect, test, vi } from 'vitest';
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
import { defineComponent, h } from 'vue';
|
import { defineComponent, h } from 'vue';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
import Code from '@editor/fields/Code.vue';
|
import Code from '@editor/fields/Code.vue';
|
||||||
|
|
||||||
|
// 用一个简单的桩组件代替 MagicCodeEditor,把所有 props 原样渲染到 data-* 属性上,
|
||||||
|
// 这样可以直接断言父组件 Code.vue 透传过去的内容是否正确。
|
||||||
vi.mock('@editor/layouts/CodeEditor.vue', () => ({
|
vi.mock('@editor/layouts/CodeEditor.vue', () => ({
|
||||||
default: defineComponent({
|
default: defineComponent({
|
||||||
name: 'CodeEditor',
|
name: 'CodeEditor',
|
||||||
props: ['height', 'initValues', 'language', 'options', 'autosize', 'parse', 'editorCustomType'],
|
props: {
|
||||||
|
height: { type: [String, Number], default: undefined },
|
||||||
|
type: { type: String, default: undefined },
|
||||||
|
initValues: { type: null, default: undefined },
|
||||||
|
modifiedValues: { type: null, default: undefined },
|
||||||
|
language: { type: String, default: undefined },
|
||||||
|
options: { type: Object, default: undefined },
|
||||||
|
autosize: { type: Object, default: undefined },
|
||||||
|
parse: { type: Boolean, default: undefined },
|
||||||
|
editorCustomType: { type: String, default: undefined },
|
||||||
|
},
|
||||||
emits: ['save'],
|
emits: ['save'],
|
||||||
setup(_p, { emit }) {
|
setup(p, { emit }) {
|
||||||
return () => h('div', { class: 'fake-code-editor', onClick: () => emit('save', 'newvalue') });
|
return () =>
|
||||||
|
h('div', {
|
||||||
|
class: 'fake-code-editor',
|
||||||
|
'data-height': p.height,
|
||||||
|
'data-type': p.type ?? '',
|
||||||
|
'data-language': p.language ?? '',
|
||||||
|
'data-init': JSON.stringify(p.initValues ?? null),
|
||||||
|
'data-modified': JSON.stringify(p.modifiedValues ?? null),
|
||||||
|
'data-options': JSON.stringify(p.options ?? null),
|
||||||
|
'data-autosize': JSON.stringify(p.autosize ?? null),
|
||||||
|
'data-parse': String(p.parse ?? ''),
|
||||||
|
'data-custom-type': p.editorCustomType ?? '',
|
||||||
|
onClick: () => emit('save', 'newvalue'),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mountCode = (props: Record<string, any>) =>
|
||||||
|
mount(Code, {
|
||||||
|
props: {
|
||||||
|
// FieldProps 必填字段,用 as any 绕过测试中类型严格匹配
|
||||||
|
config: { height: '100px', language: 'javascript' },
|
||||||
|
model: { codeField: 'oldval' },
|
||||||
|
name: 'codeField',
|
||||||
|
prop: 'codeField',
|
||||||
|
...props,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEl = (wrapper: ReturnType<typeof mountCode>) => wrapper.find('.fake-code-editor').element as HTMLElement;
|
||||||
|
|
||||||
|
const readJson = (el: HTMLElement, attr: string) => JSON.parse(el.getAttribute(attr) || 'null');
|
||||||
|
|
||||||
describe('Code', () => {
|
describe('Code', () => {
|
||||||
test('save 触发 change', async () => {
|
beforeEach(() => {
|
||||||
const wrapper = mount(Code, {
|
vi.clearAllMocks();
|
||||||
props: {
|
});
|
||||||
config: { height: '100px', language: 'js' },
|
|
||||||
model: { codeField: 'oldval' },
|
describe('基本透传与事件', () => {
|
||||||
name: 'codeField',
|
test('save 触发 change 事件,参数原样透传 (字符串)', async () => {
|
||||||
} as any,
|
const wrapper = mountCode({});
|
||||||
|
await wrapper.find('.fake-code-editor').trigger('click');
|
||||||
|
expect(wrapper.emitted('change')?.[0]?.[0]).toBe('newvalue');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save 触发 change 事件,参数可以是对象', async () => {
|
||||||
|
// 替换桩的 emit 内容:通过自定义子组件方式覆盖默认 emit value 时太复杂,
|
||||||
|
// 这里直接以 vm.$emit 等价的方式构造数据:通过 wrapper 触发 onClick 是字符串,
|
||||||
|
// 但 setup 内 save 函数本身也接受任意类型,因此用一个最小用例验证函数行为:
|
||||||
|
const wrapper = mountCode({});
|
||||||
|
// 直接调用底层桩组件 emit,模拟 save 抛出对象
|
||||||
|
const child = wrapper.findComponent({ name: 'CodeEditor' });
|
||||||
|
child.vm.$emit('save', { a: 1 });
|
||||||
|
expect(wrapper.emitted('change')?.[0]?.[0]).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('透传 height / language / autosize / parse / editorCustomType', () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
config: {
|
||||||
|
height: '200px',
|
||||||
|
language: 'json',
|
||||||
|
autosize: { minRows: 2, maxRows: 8 },
|
||||||
|
parse: true,
|
||||||
|
mFormItemType: 'vs-code-extra',
|
||||||
|
options: { tabSize: 4 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(el.getAttribute('data-height')).toBe('200px');
|
||||||
|
expect(el.getAttribute('data-language')).toBe('json');
|
||||||
|
expect(readJson(el, 'data-autosize')).toEqual({ minRows: 2, maxRows: 8 });
|
||||||
|
expect(el.getAttribute('data-parse')).toBe('true');
|
||||||
|
expect(el.getAttribute('data-custom-type')).toBe('vs-code-extra');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('组件名为 MFieldsVsCode', () => {
|
||||||
|
const wrapper = mountCode({});
|
||||||
|
expect((wrapper.vm.$options as any).name).toBe('MFieldsVsCode');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('非 diff 模式 (非对比)', () => {
|
||||||
|
test('init-values 来自 model[name],modified-values 为 null/undefined', () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
model: { codeField: 'hello' },
|
||||||
|
});
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(el.getAttribute('data-type')).toBe('');
|
||||||
|
expect(readJson(el, 'data-init')).toBe('hello');
|
||||||
|
expect(readJson(el, 'data-modified')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disabled=true 时 options.readOnly=true', () => {
|
||||||
|
const wrapper = mountCode({ disabled: true });
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(readJson(el, 'data-options')).toMatchObject({ readOnly: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disabled=false 时 options.readOnly=false', () => {
|
||||||
|
const wrapper = mountCode({ disabled: false });
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(readJson(el, 'data-options')).toMatchObject({ readOnly: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readOnly 应覆盖 config.options 中已有的 readOnly 字段', () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
// 故意把 config.options.readOnly 设为 true,期望 disabled=false 时仍以 disabled 为准
|
||||||
|
config: { height: '100px', language: 'javascript', options: { tabSize: 4, readOnly: true } },
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
const opts = readJson(el, 'data-options');
|
||||||
|
expect(opts.tabSize).toBe(4);
|
||||||
|
expect(opts.readOnly).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isCompare=true 但缺少 lastValues 时不进入 diff', () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
isCompare: true,
|
||||||
|
// 不传 lastValues
|
||||||
|
model: { codeField: 'cur' },
|
||||||
|
});
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(el.getAttribute('data-type')).toBe('');
|
||||||
|
expect(readJson(el, 'data-init')).toBe('cur');
|
||||||
|
expect(readJson(el, 'data-modified')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isCompare=false 即使带 lastValues 也不进入 diff', () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
isCompare: false,
|
||||||
|
lastValues: { codeField: 'old' },
|
||||||
|
model: { codeField: 'cur' },
|
||||||
|
});
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(el.getAttribute('data-type')).toBe('');
|
||||||
|
expect(readJson(el, 'data-init')).toBe('cur');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('diff 模式 (对比)', () => {
|
||||||
|
test('isCompare=true 且有 lastValues 时切换为 diff 模式', () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
isCompare: true,
|
||||||
|
lastValues: { codeField: 'old' },
|
||||||
|
model: { codeField: 'new' },
|
||||||
|
});
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(el.getAttribute('data-type')).toBe('diff');
|
||||||
|
expect(readJson(el, 'data-init')).toBe('old');
|
||||||
|
expect(readJson(el, 'data-modified')).toBe('new');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('diff 模式下 readOnly 强制为 true,忽略 disabled', () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
isCompare: true,
|
||||||
|
lastValues: { codeField: 'old' },
|
||||||
|
model: { codeField: 'new' },
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(readJson(el, 'data-options')).toMatchObject({ readOnly: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('diff 模式下当 lastValues 中无对应 name 字段时,init-values 退化为 null/{}', () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
isCompare: true,
|
||||||
|
// 有 lastValues 对象但没有该字段
|
||||||
|
lastValues: {},
|
||||||
|
model: { codeField: 'new' },
|
||||||
|
});
|
||||||
|
const el = getEl(wrapper);
|
||||||
|
expect(el.getAttribute('data-type')).toBe('diff');
|
||||||
|
// 源码逻辑:(lastValues || {})[name],此处 lastValues 是 {},结果为 undefined
|
||||||
|
expect(readJson(el, 'data-init')).toBe(null);
|
||||||
|
expect(readJson(el, 'data-modified')).toBe('new');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('切换 isCompare 时模式跟随变化', async () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
isCompare: false,
|
||||||
|
lastValues: { codeField: 'old' },
|
||||||
|
model: { codeField: 'new' },
|
||||||
|
});
|
||||||
|
expect(getEl(wrapper).getAttribute('data-type')).toBe('');
|
||||||
|
|
||||||
|
await wrapper.setProps({ isCompare: true } as any);
|
||||||
|
expect(getEl(wrapper).getAttribute('data-type')).toBe('diff');
|
||||||
|
expect(readJson(getEl(wrapper), 'data-init')).toBe('old');
|
||||||
|
expect(readJson(getEl(wrapper), 'data-modified')).toBe('new');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('切换 lastValues 后 init-values 同步更新', async () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
isCompare: true,
|
||||||
|
lastValues: { codeField: 'v1' },
|
||||||
|
model: { codeField: 'cur' },
|
||||||
|
});
|
||||||
|
expect(readJson(getEl(wrapper), 'data-init')).toBe('v1');
|
||||||
|
|
||||||
|
await wrapper.setProps({ lastValues: { codeField: 'v2' } } as any);
|
||||||
|
expect(readJson(getEl(wrapper), 'data-init')).toBe('v2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('diff 模式下 model 变化时 modified-values 同步更新', async () => {
|
||||||
|
const wrapper = mountCode({
|
||||||
|
isCompare: true,
|
||||||
|
lastValues: { codeField: 'old' },
|
||||||
|
model: { codeField: 'a' },
|
||||||
|
});
|
||||||
|
expect(readJson(getEl(wrapper), 'data-modified')).toBe('a');
|
||||||
|
|
||||||
|
await wrapper.setProps({ model: { codeField: 'b' } } as any);
|
||||||
|
expect(readJson(getEl(wrapper), 'data-modified')).toBe('b');
|
||||||
});
|
});
|
||||||
await wrapper.find('.fake-code-editor').trigger('click');
|
|
||||||
expect(wrapper.emitted('change')?.[0]?.[0]).toBe('newvalue');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
:label-width="item.labelWidth || labelWidth"
|
:label-width="item.labelWidth || labelWidth"
|
||||||
:step-active="stepActive"
|
:step-active="stepActive"
|
||||||
:size="size"
|
:size="size"
|
||||||
|
:show-diff="showDiff"
|
||||||
@change="changeHandler"
|
@change="changeHandler"
|
||||||
>
|
>
|
||||||
<template v-if="$slots.label" #label="labelProps">
|
<template v-if="$slots.label" #label="labelProps">
|
||||||
@ -79,6 +80,20 @@ const props = withDefaults(
|
|||||||
popperClass?: string;
|
popperClass?: string;
|
||||||
preventSubmitDefault?: boolean;
|
preventSubmitDefault?: boolean;
|
||||||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||||
|
/**
|
||||||
|
* 自定义"是否展示对比内容"的判断函数(仅在 `isCompare === true` 时生效)。
|
||||||
|
*
|
||||||
|
* - 不传:使用默认逻辑 `!isEqual(curValue, lastValue)`;
|
||||||
|
* - 传函数:完全以函数返回值为准,返回 `true` 才展示前后两份对比内容。
|
||||||
|
*
|
||||||
|
* 透传给所有层级的 Container(通过 `formState` 注入),调用方只需在 MForm
|
||||||
|
* 这一层传一次即可对整棵表单生效。
|
||||||
|
*
|
||||||
|
* 典型场景:某些字段语义上相等但结构不同(例如 `code-select` 字段中 `''` 与
|
||||||
|
* `{ hookType: 'code', hookData: [] }` 应视为相等),调用方在此处显式声明,
|
||||||
|
* 避免被 lodash `isEqual` 误判为差异。
|
||||||
|
*/
|
||||||
|
showDiff?: (_data: { curValue: any; lastValue: any; config: any }) => boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
config: () => [],
|
config: () => [],
|
||||||
@ -145,6 +160,9 @@ const formState: FormState = reactive<FormState>({
|
|||||||
get parentValues() {
|
get parentValues() {
|
||||||
return props.parentValues;
|
return props.parentValues;
|
||||||
},
|
},
|
||||||
|
get showDiff() {
|
||||||
|
return props.showDiff;
|
||||||
|
},
|
||||||
values,
|
values,
|
||||||
lastValuesProcessed,
|
lastValuesProcessed,
|
||||||
$emit: emit as (_event: string, ..._args: any[]) => void,
|
$emit: emit as (_event: string, ..._args: any[]) => void,
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
:expand-more="expand"
|
:expand-more="expand"
|
||||||
:label-width="itemLabelWidth"
|
:label-width="itemLabelWidth"
|
||||||
:style="config.fieldStyle"
|
:style="config.fieldStyle"
|
||||||
|
:show-diff="showDiffProp"
|
||||||
@change="onChangeHandler"
|
@change="onChangeHandler"
|
||||||
@addDiffCount="onAddDiffCount"
|
@addDiffCount="onAddDiffCount"
|
||||||
></component>
|
></component>
|
||||||
@ -207,6 +208,7 @@
|
|||||||
:expand-more="expand"
|
:expand-more="expand"
|
||||||
:label-width="itemLabelWidth"
|
:label-width="itemLabelWidth"
|
||||||
:prop="itemProp"
|
:prop="itemProp"
|
||||||
|
:show-diff="showDiffProp"
|
||||||
@change="onChangeHandler"
|
@change="onChangeHandler"
|
||||||
@addDiffCount="onAddDiffCount"
|
@addDiffCount="onAddDiffCount"
|
||||||
>
|
>
|
||||||
@ -271,6 +273,19 @@ const props = withDefaults(
|
|||||||
size?: string;
|
size?: string;
|
||||||
/** 是否开启对比模式 */
|
/** 是否开启对比模式 */
|
||||||
isCompare?: boolean;
|
isCompare?: boolean;
|
||||||
|
/**
|
||||||
|
* 自定义"是否展示对比内容"的判断函数(仅在 `isCompare === true` 时生效)。
|
||||||
|
*
|
||||||
|
* - 不传:使用默认逻辑 `!isEqual(curValue, lastValue)`;
|
||||||
|
* - 传函数:完全以函数返回值为准,返回 `true` 才展示前后两份对比内容。
|
||||||
|
*
|
||||||
|
* 典型场景:某些字段语义上相等但结构不同(例如 `''` 与 `{ hookType: 'code', hookData: [] }`),
|
||||||
|
* 业务侧可在此处自定义为相等以避免被误判为差异。
|
||||||
|
*
|
||||||
|
* 注意:本 prop 会在嵌套 Container 中自动透传给子级,调用方只需在最外层
|
||||||
|
* (MForm)传入一次即可。
|
||||||
|
*/
|
||||||
|
showDiff?: (_data: { curValue: any; lastValue: any; config: FormItemConfig }) => boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
prop: '',
|
prop: '',
|
||||||
@ -291,11 +306,30 @@ const mForm = inject<FormState | undefined>('mForm');
|
|||||||
const expand = ref(false);
|
const expand = ref(false);
|
||||||
|
|
||||||
const name = computed(() => props.config.name || '');
|
const name = computed(() => props.config.name || '');
|
||||||
|
|
||||||
|
// 暴露 showDiff prop(自定义对比判断函数)给模板,便于在嵌套 Container 中
|
||||||
|
// 透传到子级;用别名是为了避免与下方同名的「计算属性 showDiff(最终是否展示对比)」冲突。
|
||||||
|
//
|
||||||
|
// 优先级:本组件 props.showDiff > formState.showDiff(顶层 MForm 通过 provide 注入)。
|
||||||
|
// 这样使用方既可在 `<MForm :show-diff="...">` 一处统一注入,
|
||||||
|
// 也可在直接使用 `<Container>` 的高阶场景下用 prop 显式覆盖。
|
||||||
|
const showDiffProp = computed<typeof props.showDiff>(() => props.showDiff || mForm?.showDiff);
|
||||||
|
|
||||||
// 是否展示两个版本的对比内容
|
// 是否展示两个版本的对比内容
|
||||||
|
//
|
||||||
|
// 默认逻辑:在对比模式下用 lodash isEqual 比较当前值与历史值,不相等则展示对比。
|
||||||
|
// 若调用方通过 `showDiff` prop / formState 注入了自定义判断函数,则完全以其返回值为准,
|
||||||
|
// 便于业务侧自定义"语义上相等"的特殊场景(例如空字符串与空 hook 结构)。
|
||||||
const showDiff = computed(() => {
|
const showDiff = computed(() => {
|
||||||
if (!props.isCompare) return false;
|
if (!props.isCompare) return false;
|
||||||
const curValue = name.value ? props.model[name.value] : props.model;
|
const curValue = name.value ? props.model[name.value] : props.model;
|
||||||
const lastValue = name.value ? props.lastValues[name.value] : props.lastValues;
|
const lastValue = name.value ? props.lastValues[name.value] : props.lastValues;
|
||||||
|
|
||||||
|
const customShowDiff = showDiffProp.value;
|
||||||
|
if (typeof customShowDiff === 'function') {
|
||||||
|
return Boolean(customShowDiff({ curValue, lastValue, config: props.config }));
|
||||||
|
}
|
||||||
|
|
||||||
return !isEqual(curValue, lastValue);
|
return !isEqual(curValue, lastValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user