@veltra/desktop 1.7.11 → 1.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/batch-edit/batch-edit-form.js +2 -6
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +0 -1
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +12 -13
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/use-node-interceptor.js +31 -3
- package/dist/components/form/use-node-interceptor.js.map +1 -1
- package/dist/components/form-item/form-item.js.map +1 -1
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +1 -2
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/types/batch-edit.d.ts +0 -2
- package/dist/types/form.d.ts +0 -2
- package/package.json +5 -5
- package/src/components/batch-edit/__test__/batch-edit.test.ts +0 -138
- package/src/components/batch-edit/batch-edit-form.vue +2 -16
- package/src/components/form/__test__/form.test.ts +125 -33
- package/src/components/form/form.vue +10 -11
- package/src/components/form/use-node-interceptor.ts +36 -2
- package/src/components/form-item/__test__/form-item.test.ts +1 -10
- package/src/components/form-item/form-item.vue +2 -9
- package/src/types/batch-edit.ts +0 -2
- package/src/types/form.ts +0 -2
|
@@ -15,7 +15,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
15
15
|
__name: "batch-edit-form",
|
|
16
16
|
setup(__props, { expose: __expose }) {
|
|
17
17
|
const batchEditCtx = inject(BatchEditDIKey);
|
|
18
|
-
const { cls, props, state,
|
|
18
|
+
const { cls, props, state, handleClose, handleSave, staticFeatures, dynamicFeatures, syncing } = batchEditCtx;
|
|
19
19
|
const focused = toRef(batchEditCtx, "focused");
|
|
20
20
|
const formComponentRef = shallowRef();
|
|
21
21
|
__expose({
|
|
@@ -28,9 +28,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
28
28
|
if (syncing.value) return;
|
|
29
29
|
if (state.row && props.quickEdit) o(state.row.data).set(field, value);
|
|
30
30
|
}
|
|
31
|
-
function handleFormFieldChange(field, ...args) {
|
|
32
|
-
emit("field:change", field, ...args);
|
|
33
|
-
}
|
|
34
31
|
const creatable = computed(() => {
|
|
35
32
|
if (state.formActionType !== "create") return false;
|
|
36
33
|
return staticFeatures.value.has("create") || dynamicFeatures.value.create?.(state.row);
|
|
@@ -89,8 +86,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
89
86
|
model: unref(props).model,
|
|
90
87
|
readonly: unref(props).readonly,
|
|
91
88
|
"label-width": unref(props).labelWidth,
|
|
92
|
-
"onField:update": handleFieldUpdate
|
|
93
|
-
"onField:change": handleFormFieldChange
|
|
89
|
+
"onField:update": handleFieldUpdate
|
|
94
90
|
}, {
|
|
95
91
|
default: withCtx(() => [renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps({
|
|
96
92
|
row: unref(state).row,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-edit-form.js","names":[],"sources":["../../../src/components/batch-edit/batch-edit-form.vue"],"sourcesContent":["<template>\n <!-- 表单常驻挂载(快照只在挂载时对传入的 model 拍一次),仅用 v-show 控制显隐 -->\n <aside :class=\"cls.e('form')\" v-if=\"!!props.model\" v-show=\"state.formVisible\">\n <header :class=\"cls.e('form-header')\">\n <span :class=\"[cls.e('form-icon'), bem.is(state.formActionType)]\">\n <u-icon>\n <component :is=\"headerInfo.icon\" />\n </u-icon>\n </span>\n <div :class=\"cls.e('form-title')\">\n <h3 :class=\"cls.e('form-title-text')\">{{ headerInfo.title }}</h3>\n </div>\n <u-button\n :class=\"cls.e('form-close')\"\n :icon=\"Close\"\n text\n circle\n size=\"small\"\n title=\"关闭\"\n @click=\"handleClose\"\n />\n </header>\n\n <u-scroll always :class=\"cls.e('form-body')\">\n <transition name=\"fade\" appear mode=\"out-in\">\n <u-form\n ref=\"formComponentRef\"\n :model=\"props.model\"\n :readonly=\"props.readonly\"\n :label-width=\"props.labelWidth\"\n @field:update=\"handleFieldUpdate\"\n
|
|
1
|
+
{"version":3,"file":"batch-edit-form.js","names":[],"sources":["../../../src/components/batch-edit/batch-edit-form.vue"],"sourcesContent":["<template>\n <!-- 表单常驻挂载(快照只在挂载时对传入的 model 拍一次),仅用 v-show 控制显隐 -->\n <aside :class=\"cls.e('form')\" v-if=\"!!props.model\" v-show=\"state.formVisible\">\n <header :class=\"cls.e('form-header')\">\n <span :class=\"[cls.e('form-icon'), bem.is(state.formActionType)]\">\n <u-icon>\n <component :is=\"headerInfo.icon\" />\n </u-icon>\n </span>\n <div :class=\"cls.e('form-title')\">\n <h3 :class=\"cls.e('form-title-text')\">{{ headerInfo.title }}</h3>\n </div>\n <u-button\n :class=\"cls.e('form-close')\"\n :icon=\"Close\"\n text\n circle\n size=\"small\"\n title=\"关闭\"\n @click=\"handleClose\"\n />\n </header>\n\n <u-scroll always :class=\"cls.e('form-body')\">\n <transition name=\"fade\" appear mode=\"out-in\">\n <u-form\n ref=\"formComponentRef\"\n :model=\"props.model\"\n :readonly=\"props.readonly\"\n :label-width=\"props.labelWidth\"\n @field:update=\"handleFieldUpdate\"\n >\n <slot\n v-bind=\"{\n row: state.row,\n parentRow: state.parentRow,\n formActionType: state.formActionType,\n depth: state.depth,\n indexes: state.indexPath,\n index: state.row?.index\n }\"\n />\n </u-form>\n </transition>\n </u-scroll>\n\n <footer :class=\"cls.e('form-footer')\">\n <span :class=\"cls.e('form-hint')\">\n <template v-if=\"props.readonly\"> 只读模式 </template>\n\n <span :class=\"{ [bem.is('concealed')]: !focused }\">\n <template v-if=\"!(state.formActionType === 'update' && props.quickEdit)\">\n <u-kbd>Ctrl + S</u-kbd> 保存 ·\n </template>\n <u-kbd>Esc</u-kbd> 关闭\n </span>\n </span>\n\n <div :class=\"cls.e('form-actions')\">\n <u-button size=\"small\" text :loading=\"state.loading\" @click=\"handleClose\"> 取消 </u-button>\n <u-button\n v-if=\"showSaveBtn\"\n type=\"primary\"\n size=\"small\"\n title=\"保存\"\n :icon=\"Save\"\n :loading=\"state.loading\"\n @click=\"handleSave\"\n />\n </div>\n </footer>\n </aside>\n</template>\n\n<script lang=\"ts\" setup>\nimport { o } from '@cat-kit/core'\nimport { Close, Save } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, shallowRef, toRef } from 'vue'\n\nimport type { _FormExposed, FormExposed } from '../../types/form'\nimport { UButton } from '../button'\nimport { UForm } from '../form'\nimport { UIcon } from '../icon'\nimport { UKbd } from '../kbd'\nimport { UScroll } from '../scroll'\nimport { BatchEditDIKey } from './di'\nimport { FORM_ACTION_HEADER_MAP } from './form-action-header'\n\ndefineOptions({ name: 'UBatchEditForm' })\n\nconst batchEditCtx = inject(BatchEditDIKey)!\n\nconst { cls, props, state, handleClose, handleSave, staticFeatures, dynamicFeatures, syncing } =\n batchEditCtx\n\nconst focused = toRef(batchEditCtx, 'focused')\n\nconst formComponentRef = shallowRef<FormExposed>()\n\ndefineExpose<_FormExposed>({\n el: toRef(() => formComponentRef.value?.el),\n validate: () => formComponentRef.value?.validate() ?? Promise.resolve(true),\n clearValidate: () => formComponentRef.value?.clearValidate(),\n reset: () => formComponentRef.value?.reset()\n})\n\nfunction handleFieldUpdate(field: string, value: any) {\n // 编程方式重置/回显期间不写回行数据,避免默认值污染源数据\n if (syncing.value) return\n\n if (state.row && props.quickEdit) {\n o(state.row.data).set(field, value)\n }\n}\n\nconst creatable = computed(() => {\n if (state.formActionType !== 'create') return false\n return staticFeatures.value.has('create') || dynamicFeatures.value.create?.(state.row)\n})\n\nconst creatableChild = computed(() => {\n if (state.formActionType !== 'createChild') return false\n return (\n staticFeatures.value.has('createChild') || dynamicFeatures.value.createChild?.(state.parentRow)\n )\n})\n\nconst updatable = computed(() => {\n if (state.formActionType !== 'update') return false\n return staticFeatures.value.has('update') || dynamicFeatures.value.update?.(state.row)\n})\n\nconst showSaveBtn = computed(() => {\n if (props.readonly) return false\n if (props.quickEdit && state.formActionType === 'update') return false\n return creatable.value || updatable.value || creatableChild.value\n})\n\nconst headerInfo = computed(() => FORM_ACTION_HEADER_MAP[state.formActionType])\n</script>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-edit.js","names":[],"sources":["../../../src/components/batch-edit/batch-edit.vue"],"sourcesContent":["<template>\n <u-layout\n :class=\"cls.b\"\n :cols=\"cols\"\n rows=\"minmax(0, 1fr) \"\n gap=\"8px\"\n resizable\n tabindex=\"-1\"\n @focusin=\"handleFocusIn\"\n @focusout=\"handleFocusOut\"\n @keydown.capture=\"handleKeydown\"\n >\n <!-- 编辑列表 -->\n <BatchEditList :slots=\"slots\" />\n\n <!-- 编辑表单 -->\n <BatchEditForm ref=\"form\" v-slot=\"scoped\">\n <slot name=\"form\" v-bind=\"scoped\" />\n </BatchEditForm>\n </u-layout>\n</template>\n\n<script lang=\"ts\" setup>\nimport { bem } from '@veltra/utils'\nimport { computed, provide, useTemplateRef } from 'vue'\n\nimport type { BatchEditEmits, BatchEditProps, BatchEditSlots, FormExposed } from '../../types'\nimport { ULayout } from '../layout'\nimport BatchEditForm from './batch-edit-form.vue'\nimport BatchEditList from './batch-edit-list.vue'\nimport { BatchEditDIKey } from './di'\nimport { useEditState } from './use-edit-state'\nimport { useFeatures } from './use-features'\nimport { useHandlers } from './use-handlers'\nimport { useShortcutKey } from './use-shortcut-key'\n\ndefineOptions({ name: 'UBatchEdit' })\n\nconst props = withDefaults(defineProps<BatchEditProps>(), { cols: () => ['1fr', '420px'] })\n\nconst emit = defineEmits<BatchEditEmits>()\n\nconst slots = defineSlots<BatchEditSlots>()\n\nconst cls = bem('batch-edit')\n\nconst formRef = useTemplateRef<FormExposed>('form')\n\nconst { state, resetState, syncing } = useEditState({ props, formRef })\n\nconst { staticFeatures, dynamicFeatures } = useFeatures({ props })\n\nconst handlers = useHandlers({ props, emit, state, resetState, formRef })\n\n// 快捷键处理\nconst { handleFocusIn, handleFocusOut, handleKeydown, focused } = useShortcutKey({\n props,\n onSave: handlers.handleSave,\n onClose: handlers.handleClose,\n state\n})\n\nconst cols = computed(() => {\n return state.formVisible ? props.cols : undefined\n})\n\nprovide(BatchEditDIKey, {\n cls,\n props,\n emit,\n state,\n staticFeatures,\n dynamicFeatures,\n focused,\n syncing,\n ...handlers\n})\n</script>\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"batch-edit.js","names":[],"sources":["../../../src/components/batch-edit/batch-edit.vue"],"sourcesContent":["<template>\n <u-layout\n :class=\"cls.b\"\n :cols=\"cols\"\n rows=\"minmax(0, 1fr) \"\n gap=\"8px\"\n resizable\n tabindex=\"-1\"\n @focusin=\"handleFocusIn\"\n @focusout=\"handleFocusOut\"\n @keydown.capture=\"handleKeydown\"\n >\n <!-- 编辑列表 -->\n <BatchEditList :slots=\"slots\" />\n\n <!-- 编辑表单 -->\n <BatchEditForm ref=\"form\" v-slot=\"scoped\">\n <slot name=\"form\" v-bind=\"scoped\" />\n </BatchEditForm>\n </u-layout>\n</template>\n\n<script lang=\"ts\" setup>\nimport { bem } from '@veltra/utils'\nimport { computed, provide, useTemplateRef } from 'vue'\n\nimport type { BatchEditEmits, BatchEditProps, BatchEditSlots, FormExposed } from '../../types'\nimport { ULayout } from '../layout'\nimport BatchEditForm from './batch-edit-form.vue'\nimport BatchEditList from './batch-edit-list.vue'\nimport { BatchEditDIKey } from './di'\nimport { useEditState } from './use-edit-state'\nimport { useFeatures } from './use-features'\nimport { useHandlers } from './use-handlers'\nimport { useShortcutKey } from './use-shortcut-key'\n\ndefineOptions({ name: 'UBatchEdit' })\n\nconst props = withDefaults(defineProps<BatchEditProps>(), { cols: () => ['1fr', '420px'] })\n\nconst emit = defineEmits<BatchEditEmits>()\n\nconst slots = defineSlots<BatchEditSlots>()\n\nconst cls = bem('batch-edit')\n\nconst formRef = useTemplateRef<FormExposed>('form')\n\nconst { state, resetState, syncing } = useEditState({ props, formRef })\n\nconst { staticFeatures, dynamicFeatures } = useFeatures({ props })\n\nconst handlers = useHandlers({ props, emit, state, resetState, formRef })\n\n// 快捷键处理\nconst { handleFocusIn, handleFocusOut, handleKeydown, focused } = useShortcutKey({\n props,\n onSave: handlers.handleSave,\n onClose: handlers.handleClose,\n state\n})\n\nconst cols = computed(() => {\n return state.formVisible ? props.cols : undefined\n})\n\nprovide(BatchEditDIKey, {\n cls,\n props,\n emit,\n state,\n staticFeatures,\n dynamicFeatures,\n focused,\n syncing,\n ...handlers\n})\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsCA,MAAM,QAAQ;EAEd,MAAM,OAAO;EAEb,MAAM,QAAQ,SAAA;EAEd,MAAM,MAAM,IAAI,YAAY;EAE5B,MAAM,UAAU,eAA4B,MAAM;EAElD,MAAM,EAAE,OAAO,YAAY,YAAY,aAAa;GAAE;GAAO;EAAQ,CAAC;EAEtE,MAAM,EAAE,gBAAgB,oBAAoB,YAAY,EAAE,MAAM,CAAC;EAEjE,MAAM,WAAW,YAAY;GAAE;GAAO;GAAM;GAAO;GAAY;EAAQ,CAAC;EAGxE,MAAM,EAAE,eAAe,gBAAgB,eAAe,YAAY,eAAe;GAC/E;GACA,QAAQ,SAAS;GACjB,SAAS,SAAS;GAClB;EACF,CAAC;EAED,MAAM,OAAO,eAAe;GAC1B,OAAO,MAAM,cAAc,MAAM,OAAO,KAAA;EAC1C,CAAC;EAED,QAAQ,gBAAgB;GACtB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,GAAG;EACL,CAAC;;GA3EC,OAAA,UAAA,GAAA,YAkBW,MAAA,WAAA,GAAA;IAjBR,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,CAAC;IACZ,MAAM,KAAA;IACP,MAAK;IACL,KAAI;IACJ,WAAA;IACA,UAAS;IACR,WAAS,MAAA,aAAA;IACT,YAAU,MAAA,cAAA;IACO,kBAAA,MAAA,aAAA;;IAElB,SAAA,cAAa;KAAb,mBAAA,QAAA;KACA,YAAgC,aAAA,EAAT,MAAK,CAAA;KAE5B,mBAAA,QAAA;KACA,YAEgB,aAAA,EAFD,KAAI,OAAM,GAAA;MACvB,SAAA,SADgC,WAAM,CACtC,WAAoC,KAAA,QAAA,QAAA,eAAA,mBAAV,MAAM,CAAA,CAAA,CAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.js","names":[],"sources":["../../../src/components/form/form.vue"],"sourcesContent":["<template>\n <u-grid\n tag=\"form\"\n ref=\"grid\"\n @submit.prevent\n gap=\"0 12px\"\n :cols=\"cols || breakpointCols\"\n :class=\"[cls.b, bem.is('readonly', readonly)]\"\n >\n <template\n v-for=\"{ node, isFormItem, formItemProps, field, modelValue } of getSlotsNodes()\"\n :key=\"
|
|
1
|
+
{"version":3,"file":"form.js","names":[],"sources":["../../../src/components/form/form.vue"],"sourcesContent":["<template>\n <u-grid\n tag=\"form\"\n ref=\"grid\"\n @submit.prevent\n gap=\"0 12px\"\n :cols=\"cols || breakpointCols\"\n :class=\"[cls.b, bem.is('readonly', readonly)]\"\n >\n <template\n v-for=\"{ node, isFormItem, formItemProps, field, modelValue, renderKey } of getSlotsNodes()\"\n :key=\"renderKey\"\n >\n <u-form-item v-if=\"!isFormItem && field\" v-bind=\"formItemProps\">\n <component\n :is=\"node\"\n :model-value=\"modelValue ?? o(model ?? {}).get(field)\"\n @update:model-value=\"handleUpdateValue(field, $event)\"\n />\n <div v-if=\"shouldShowModified(field)\" :class=\"cls.e('data-before')\">\n <span :class=\"cls.e('changed-tag')\">{{ modifiedLabel }}</span>\n <component :is=\"createModifiedPreview(node, field)\" />\n </div>\n </u-form-item>\n\n <component v-else :is=\"node\" />\n </template>\n </u-grid>\n</template>\n\n<script lang=\"tsx\" setup>\nimport { o } from '@cat-kit/core'\nimport { bem } from '@veltra/utils'\nimport { provideFormContext } from '@veltra/utils'\nimport { nextTick, toRef, useTemplateRef, type VNode } from 'vue'\n\nimport type { BreakCols, FormProps, _FormExposed, FormEmits } from '../../types'\nimport { UFormItem } from '../form-item'\nimport { UGrid } from '../grid'\nimport { isFieldModified } from './is-field-modified'\nimport { useFormFields } from './use-form-fields'\nimport { forkUnmountedVNode, useNodeInterceptor } from './use-node-interceptor'\n\ndefineOptions({ name: 'UForm' })\n\nconst props = withDefaults(defineProps<FormProps>(), { modifiedLabel: '变更前:' })\n\nconst emit = defineEmits<FormEmits>()\n\ndefineSlots<{ default(props?: { data: Record<string, any> | undefined }): any }>()\n\nconst cls = bem('form')\nconst formItemCls = bem('form-item')\nconst gridRef = useTemplateRef('grid')\n\nconst breakpointCols: BreakCols = { xs: 1, md: 2, lg: 3, xl: 4, default: 4 }\n\nconst {\n validate: runValidate,\n clearValidate,\n reset,\n registerField,\n unregisterField,\n shouldValidate,\n getBaselineModel\n} = useFormFields({ props })\n\nasync function validate(keys?: string[]) {\n const valid = await runValidate(keys)\n if (!valid) {\n await nextTick()\n\n gridRef.value?.el\n ?.querySelector(`.${formItemCls.e('error-text')}`)\n ?.scrollIntoView({ block: 'center' })\n }\n return valid\n}\n\nfunction handleFieldUpdate(field: string, value: any) {\n emit('field:update', field, value)\n}\n\nprovideFormContext({\n formProps: props,\n registerField,\n unregisterField,\n validateFields: validate,\n shouldValidate,\n handleFieldUpdate\n})\n\nconst { getSlotsNodes } = useNodeInterceptor()\n\nfunction handleUpdateValue(field: string, value: any) {\n if (!props.model) return\n o(props.model).set(field, value)\n}\n\nfunction getBaselineFieldValue(field: string) {\n return o(getBaselineModel() ?? {}).get(field)\n}\n\nfunction createModifiedPreview(node: VNode, field: string) {\n return forkUnmountedVNode(node, { readonly: true, modelValue: getBaselineFieldValue(field) })\n}\n\nfunction shouldShowModified(field: string) {\n if (!props.showModified) return false\n const baseline = getBaselineModel()\n if (!baseline || !props.model) return false\n const current = o(props.model).get(field)\n const initial = o(baseline).get(field)\n return isFieldModified(current, initial)\n}\n\ndefineExpose<_FormExposed>({ el: toRef(() => gridRef.value?.el), validate, clearValidate, reset })\n</script>\n"],"mappings":""}
|
|
@@ -2,7 +2,7 @@ import _sfc_main from "../grid/grid.js";
|
|
|
2
2
|
import form_item_default from "../form-item/form-item.js";
|
|
3
3
|
import { isFieldModified } from "./is-field-modified.js";
|
|
4
4
|
import { useFormFields } from "./use-form-fields.js";
|
|
5
|
-
import { useNodeInterceptor } from "./use-node-interceptor.js";
|
|
5
|
+
import { forkUnmountedVNode, useNodeInterceptor } from "./use-node-interceptor.js";
|
|
6
6
|
import { Fragment, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, mergeProps, nextTick, normalizeClass, openBlock, renderList, resolveDynamicComponent, toDisplayString, toRef, unref, useTemplateRef, withCtx, withModifiers } from "vue";
|
|
7
7
|
import { bem, provideFormContext } from "@veltra/utils";
|
|
8
8
|
import { o } from "@cat-kit/core";
|
|
@@ -23,7 +23,7 @@ var form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
|
|
|
23
23
|
disabled: { type: Boolean },
|
|
24
24
|
size: {}
|
|
25
25
|
},
|
|
26
|
-
emits: ["field:
|
|
26
|
+
emits: ["field:update"],
|
|
27
27
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
28
28
|
const props = __props;
|
|
29
29
|
const emit = __emit;
|
|
@@ -49,17 +49,13 @@ var form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
|
|
|
49
49
|
function handleFieldUpdate(field, value) {
|
|
50
50
|
emit("field:update", field, value);
|
|
51
51
|
}
|
|
52
|
-
function handleFieldChange(field, ...args) {
|
|
53
|
-
emit("field:change", field, ...args);
|
|
54
|
-
}
|
|
55
52
|
provideFormContext({
|
|
56
53
|
formProps: props,
|
|
57
54
|
registerField,
|
|
58
55
|
unregisterField,
|
|
59
56
|
validateFields: validate,
|
|
60
57
|
shouldValidate,
|
|
61
|
-
handleFieldUpdate
|
|
62
|
-
handleFieldChange
|
|
58
|
+
handleFieldUpdate
|
|
63
59
|
});
|
|
64
60
|
const { getSlotsNodes } = useNodeInterceptor();
|
|
65
61
|
function handleUpdateValue(field, value) {
|
|
@@ -69,6 +65,12 @@ var form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
|
|
|
69
65
|
function getBaselineFieldValue(field) {
|
|
70
66
|
return o(getBaselineModel() ?? {}).get(field);
|
|
71
67
|
}
|
|
68
|
+
function createModifiedPreview(node, field) {
|
|
69
|
+
return forkUnmountedVNode(node, {
|
|
70
|
+
readonly: true,
|
|
71
|
+
modelValue: getBaselineFieldValue(field)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
72
74
|
function shouldShowModified(field) {
|
|
73
75
|
if (!props.showModified) return false;
|
|
74
76
|
const baseline = getBaselineModel();
|
|
@@ -92,8 +94,8 @@ var form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
|
|
|
92
94
|
cols: __props.cols || breakpointCols,
|
|
93
95
|
class: normalizeClass([unref(cls).b, unref(bem).is("readonly", __props.readonly)])
|
|
94
96
|
}, {
|
|
95
|
-
default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(getSlotsNodes)(), ({ node, isFormItem, formItemProps, field, modelValue }) => {
|
|
96
|
-
return openBlock(), createElementBlock(Fragment, { key:
|
|
97
|
+
default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(getSlotsNodes)(), ({ node, isFormItem, formItemProps, field, modelValue, renderKey }) => {
|
|
98
|
+
return openBlock(), createElementBlock(Fragment, { key: renderKey }, [!isFormItem && field ? (openBlock(), createBlock(unref(form_item_default), mergeProps({
|
|
97
99
|
key: 0,
|
|
98
100
|
ref_for: true
|
|
99
101
|
}, formItemProps), {
|
|
@@ -103,10 +105,7 @@ var form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
|
|
|
103
105
|
}, null, 8, ["model-value", "onUpdate:modelValue"])), shouldShowModified(field) ? (openBlock(), createElementBlock("div", {
|
|
104
106
|
key: 0,
|
|
105
107
|
class: normalizeClass(unref(cls).e("data-before"))
|
|
106
|
-
}, [createElementVNode("span", { class: normalizeClass(unref(cls).e("changed-tag")) }, toDisplayString(__props.modifiedLabel), 3), (openBlock(), createBlock(resolveDynamicComponent(node),
|
|
107
|
-
readonly: "",
|
|
108
|
-
"model-value": getBaselineFieldValue(field)
|
|
109
|
-
}, null, 8, ["model-value"]))], 2)) : createCommentVNode("v-if", true)]),
|
|
108
|
+
}, [createElementVNode("span", { class: normalizeClass(unref(cls).e("changed-tag")) }, toDisplayString(__props.modifiedLabel), 3), (openBlock(), createBlock(resolveDynamicComponent(createModifiedPreview(node, field))))], 2)) : createCommentVNode("v-if", true)]),
|
|
110
109
|
_: 2
|
|
111
110
|
}, 1040)) : (openBlock(), createBlock(resolveDynamicComponent(node), { key: 1 }))], 64);
|
|
112
111
|
}), 128))]),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/form/form.vue"],"sourcesContent":["<template>\n <u-grid\n tag=\"form\"\n ref=\"grid\"\n @submit.prevent\n gap=\"0 12px\"\n :cols=\"cols || breakpointCols\"\n :class=\"[cls.b, bem.is('readonly', readonly)]\"\n >\n <template\n v-for=\"{ node, isFormItem, formItemProps, field, modelValue } of getSlotsNodes()\"\n :key=\"
|
|
1
|
+
{"version":3,"file":"form.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/form/form.vue"],"sourcesContent":["<template>\n <u-grid\n tag=\"form\"\n ref=\"grid\"\n @submit.prevent\n gap=\"0 12px\"\n :cols=\"cols || breakpointCols\"\n :class=\"[cls.b, bem.is('readonly', readonly)]\"\n >\n <template\n v-for=\"{ node, isFormItem, formItemProps, field, modelValue, renderKey } of getSlotsNodes()\"\n :key=\"renderKey\"\n >\n <u-form-item v-if=\"!isFormItem && field\" v-bind=\"formItemProps\">\n <component\n :is=\"node\"\n :model-value=\"modelValue ?? o(model ?? {}).get(field)\"\n @update:model-value=\"handleUpdateValue(field, $event)\"\n />\n <div v-if=\"shouldShowModified(field)\" :class=\"cls.e('data-before')\">\n <span :class=\"cls.e('changed-tag')\">{{ modifiedLabel }}</span>\n <component :is=\"createModifiedPreview(node, field)\" />\n </div>\n </u-form-item>\n\n <component v-else :is=\"node\" />\n </template>\n </u-grid>\n</template>\n\n<script lang=\"tsx\" setup>\nimport { o } from '@cat-kit/core'\nimport { bem } from '@veltra/utils'\nimport { provideFormContext } from '@veltra/utils'\nimport { nextTick, toRef, useTemplateRef, type VNode } from 'vue'\n\nimport type { BreakCols, FormProps, _FormExposed, FormEmits } from '../../types'\nimport { UFormItem } from '../form-item'\nimport { UGrid } from '../grid'\nimport { isFieldModified } from './is-field-modified'\nimport { useFormFields } from './use-form-fields'\nimport { forkUnmountedVNode, useNodeInterceptor } from './use-node-interceptor'\n\ndefineOptions({ name: 'UForm' })\n\nconst props = withDefaults(defineProps<FormProps>(), { modifiedLabel: '变更前:' })\n\nconst emit = defineEmits<FormEmits>()\n\ndefineSlots<{ default(props?: { data: Record<string, any> | undefined }): any }>()\n\nconst cls = bem('form')\nconst formItemCls = bem('form-item')\nconst gridRef = useTemplateRef('grid')\n\nconst breakpointCols: BreakCols = { xs: 1, md: 2, lg: 3, xl: 4, default: 4 }\n\nconst {\n validate: runValidate,\n clearValidate,\n reset,\n registerField,\n unregisterField,\n shouldValidate,\n getBaselineModel\n} = useFormFields({ props })\n\nasync function validate(keys?: string[]) {\n const valid = await runValidate(keys)\n if (!valid) {\n await nextTick()\n\n gridRef.value?.el\n ?.querySelector(`.${formItemCls.e('error-text')}`)\n ?.scrollIntoView({ block: 'center' })\n }\n return valid\n}\n\nfunction handleFieldUpdate(field: string, value: any) {\n emit('field:update', field, value)\n}\n\nprovideFormContext({\n formProps: props,\n registerField,\n unregisterField,\n validateFields: validate,\n shouldValidate,\n handleFieldUpdate\n})\n\nconst { getSlotsNodes } = useNodeInterceptor()\n\nfunction handleUpdateValue(field: string, value: any) {\n if (!props.model) return\n o(props.model).set(field, value)\n}\n\nfunction getBaselineFieldValue(field: string) {\n return o(getBaselineModel() ?? {}).get(field)\n}\n\nfunction createModifiedPreview(node: VNode, field: string) {\n return forkUnmountedVNode(node, { readonly: true, modelValue: getBaselineFieldValue(field) })\n}\n\nfunction shouldShowModified(field: string) {\n if (!props.showModified) return false\n const baseline = getBaselineModel()\n if (!baseline || !props.model) return false\n const current = o(props.model).get(field)\n const initial = o(baseline).get(field)\n return isFieldModified(current, initial)\n}\n\ndefineExpose<_FormExposed>({ el: toRef(() => gridRef.value?.el), validate, clearValidate, reset })\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;EA6CA,MAAM,QAAQ;EAEd,MAAM,OAAO;EAIb,MAAM,MAAM,IAAI,MAAM;EACtB,MAAM,cAAc,IAAI,WAAW;EACnC,MAAM,UAAU,eAAe,MAAM;EAErC,MAAM,iBAA4B;GAAE,IAAI;GAAG,IAAI;GAAG,IAAI;GAAG,IAAI;GAAG,SAAS;EAAE;EAE3E,MAAM,EACJ,UAAU,aACV,eACA,OACA,eACA,iBACA,gBACA,qBACE,cAAc,EAAE,MAAM,CAAC;EAE3B,eAAe,SAAS,MAAiB;GACvC,MAAM,QAAQ,MAAM,YAAY,IAAI;GACpC,IAAI,CAAC,OAAO;IACV,MAAM,SAAS;IAEf,QAAQ,OAAO,IACX,cAAc,IAAI,YAAY,EAAE,YAAY,GAAG,CAAA,EAC/C,eAAe,EAAE,OAAO,SAAS,CAAC;GACxC;GACA,OAAO;EACT;EAEA,SAAS,kBAAkB,OAAe,OAAY;GACpD,KAAK,gBAAgB,OAAO,KAAK;EACnC;EAEA,mBAAmB;GACjB,WAAW;GACX;GACA;GACA,gBAAgB;GAChB;GACA;EACF,CAAC;EAED,MAAM,EAAE,kBAAkB,mBAAmB;EAE7C,SAAS,kBAAkB,OAAe,OAAY;GACpD,IAAI,CAAC,MAAM,OAAO;GAClB,EAAE,MAAM,KAAK,CAAC,CAAC,IAAI,OAAO,KAAK;EACjC;EAEA,SAAS,sBAAsB,OAAe;GAC5C,OAAO,EAAE,iBAAiB,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK;EAC9C;EAEA,SAAS,sBAAsB,MAAa,OAAe;GACzD,OAAO,mBAAmB,MAAM;IAAE,UAAU;IAAM,YAAY,sBAAsB,KAAK;GAAE,CAAC;EAC9F;EAEA,SAAS,mBAAmB,OAAe;GACzC,IAAI,CAAC,MAAM,cAAc,OAAO;GAChC,MAAM,WAAW,iBAAiB;GAClC,IAAI,CAAC,YAAY,CAAC,MAAM,OAAO,OAAO;GACtC,MAAM,UAAU,EAAE,MAAM,KAAK,CAAC,CAAC,IAAI,KAAK;GACxC,MAAM,UAAU,EAAE,QAAQ,CAAC,CAAC,IAAI,KAAK;GACrC,OAAO,gBAAgB,SAAS,OAAO;EACzC;EAEA,SAA2B;GAAE,IAAI,YAAY,QAAQ,OAAO,EAAE;GAAG;GAAU;GAAe;EAAM,CAAC;;GAnH/F,OAAA,UAAA,GAAA,YA0BS,MAAA,SAAA,GAAA;IAzBP,KAAI;IACJ,KAAI;IACH,UAAM,OAAA,OAAA,OAAA,KAAA,oBAAP,CAAA,GAAe,CAAA,SAAA,CAAA;IACf,KAAI;IACH,MAAM,QAAA,QAAQ;IACd,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,GAAG,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,QAAA,QAAQ,CAAA,CAAA;;IAGzC,SAAA,cAA4F,EAD9F,UAAA,IAAA,GAAA,mBAiBW,UAAA,MAAA,WAhBmE,MAAA,aAAA,CAAa,CAAA,IAAA,EAAhF,MAAM,YAAY,eAAe,OAAO,YAAY,gBAAS;KAChE,OAAA,UAAA,GAAA,mBAAA,UAAA,EAAA,KAAA,UAAS,GAAA,CAEK,CAAA,cAAc,SAAlC,UAAA,GAAA,YAUc,MAAA,iBAAA,GAVd,WAUc;;;KAVmC,GAAA,aAAa,GAAA;MAC5D,SAAA,cAIE,EAJF,UAAA,GAAA,YAIE,wBAHK,IAAI,GAAA;OACR,eAAa,cAAc,MAAA,CAAA,CAAC,CAAC,QAAA,SAAK,CAAA,CAAA,CAAA,CAAQ,IAAI,KAAK;OACnD,wBAAkB,WAAE,kBAAkB,OAAO,MAAM;MAE3C,GAAA,MAAA,GAAA,CAAA,eAAA,qBAAA,CAAA,IAAA,mBAAmB,KAAK,KAAnC,UAAA,GAAA,mBAGM,OAAA;;OAHiC,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,aAAA,CAAA;MACjD,GAAA,CAAA,mBAA8D,QAAA,EAAvD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,aAAA,CAAA,EAAoB,GAAA,gBAAA,QAAA,aAAa,GAAA,CAAA,IACpD,UAAA,GAAA,YAAsD,wBAAtC,sBAAsB,MAAM,KAAK,CAAA,CAAA,EAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,CAAA;;KAIrD,GAAA,IAAA,MAAA,UAAA,GAAA,YAA+B,wBAAR,IAAI,GAAA,EAAA,KAAA,EAAA,CAAA,EAAA,GAAA,EAAA"}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
import { useSlots } from "vue";
|
|
1
|
+
import { cloneVNode, useSlots } from "vue";
|
|
2
2
|
import { extractNormalVNodes } from "@veltra/utils";
|
|
3
3
|
import { o } from "@cat-kit/core";
|
|
4
4
|
//#region src/components/form/use-node-interceptor.ts
|
|
5
|
+
function getVNodeTypeName(node) {
|
|
6
|
+
const type = node.type;
|
|
7
|
+
if (typeof type === "string") return type;
|
|
8
|
+
if (typeof type === "object" && type) return type.name || type.__name || "";
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 从源 vnode 派生一份未挂载副本。
|
|
13
|
+
* Vue 的 cloneVNode 会拷贝 el/component,同一源 vnode 渲两次会把实例从编辑控件抢走。
|
|
14
|
+
*/
|
|
15
|
+
function forkUnmountedVNode(node, extra) {
|
|
16
|
+
const forked = extra ? cloneVNode(node, extra) : cloneVNode(node);
|
|
17
|
+
forked.el = null;
|
|
18
|
+
forked.component = null;
|
|
19
|
+
forked.anchor = null;
|
|
20
|
+
forked.suspense = null;
|
|
21
|
+
return forked;
|
|
22
|
+
}
|
|
5
23
|
/**
|
|
6
24
|
* 虚拟node拦截
|
|
7
25
|
* @returns
|
|
@@ -13,6 +31,7 @@ function useNodeInterceptor() {
|
|
|
13
31
|
if (!nodes?.length) return null;
|
|
14
32
|
const flattedNodes = extractNormalVNodes(nodes);
|
|
15
33
|
const results = [];
|
|
34
|
+
const seen = /* @__PURE__ */ new Map();
|
|
16
35
|
let i = 0;
|
|
17
36
|
while (i < flattedNodes.length) {
|
|
18
37
|
const node = flattedNodes[i];
|
|
@@ -28,12 +47,21 @@ function useNodeInterceptor() {
|
|
|
28
47
|
"readonly",
|
|
29
48
|
"field"
|
|
30
49
|
]);
|
|
50
|
+
let renderKey;
|
|
51
|
+
if (node.key != null) renderKey = node.key;
|
|
52
|
+
else {
|
|
53
|
+
const base = `${getVNodeTypeName(node)}:${field ?? ""}`;
|
|
54
|
+
const n = seen.get(base) ?? 0;
|
|
55
|
+
seen.set(base, n + 1);
|
|
56
|
+
renderKey = n === 0 ? `uform:${base}` : `uform:${base}#${n}`;
|
|
57
|
+
}
|
|
31
58
|
results.push({
|
|
32
59
|
isFormItem,
|
|
33
60
|
formItemProps,
|
|
34
61
|
node,
|
|
35
62
|
field,
|
|
36
|
-
modelValue: props?.["model-value"] ?? props?.modelValue
|
|
63
|
+
modelValue: props?.["model-value"] ?? props?.modelValue,
|
|
64
|
+
renderKey
|
|
37
65
|
});
|
|
38
66
|
}
|
|
39
67
|
return results;
|
|
@@ -41,6 +69,6 @@ function useNodeInterceptor() {
|
|
|
41
69
|
return { getSlotsNodes };
|
|
42
70
|
}
|
|
43
71
|
//#endregion
|
|
44
|
-
export { useNodeInterceptor };
|
|
72
|
+
export { forkUnmountedVNode, useNodeInterceptor };
|
|
45
73
|
|
|
46
74
|
//# sourceMappingURL=use-node-interceptor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-node-interceptor.js","names":[],"sources":["../../../src/components/form/use-node-interceptor.ts"],"sourcesContent":["import { o } from '@cat-kit/core'\nimport { extractNormalVNodes } from '@veltra/utils'\nimport { useSlots, type VNode } from 'vue'\n\nexport type SlotRenderItem = {\n isFormItem: boolean\n formItemProps?: Record<string, any>\n node: VNode\n field?: string\n modelValue?: any\n}\n\n/**\n * 虚拟node拦截\n * @returns\n */\nexport function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] | null } {\n const slots = useSlots()\n\n function getSlotsNodes() {\n const nodes = slots.default?.()\n if (!nodes?.length) return null\n\n const flattedNodes = extractNormalVNodes(nodes)\n\n const results: SlotRenderItem[] = []\n\n let i = 0\n while (i < flattedNodes.length) {\n const node = flattedNodes[i]!\n i++\n\n const { props, type } = node\n const field = props?.field as string | undefined\n const isFormItem = (type as any)?.name === 'UFormItem'\n\n const formItemProps = o((props ?? {}) as Record<string, any>).pick([\n 'label',\n 'rules',\n 'span',\n 'tips',\n 'readonly',\n 'field'\n ]) as Record<string, any>\n\n results.push({\n isFormItem,\n formItemProps,\n node,\n field,\n modelValue: props?.['model-value'] ?? props?.modelValue\n })\n }\n\n return results\n }\n\n return { getSlotsNodes }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-node-interceptor.js","names":[],"sources":["../../../src/components/form/use-node-interceptor.ts"],"sourcesContent":["import { o } from '@cat-kit/core'\nimport { extractNormalVNodes } from '@veltra/utils'\nimport { cloneVNode, useSlots, type VNode } from 'vue'\n\nexport type SlotRenderItem = {\n isFormItem: boolean\n formItemProps?: Record<string, any>\n node: VNode\n field?: string\n modelValue?: any\n /** v-for 身份:调用方 :key 优先,否则 type+field+出现次序 */\n renderKey: string | number | symbol\n}\n\nfunction getVNodeTypeName(node: VNode): string {\n const type = node.type as { name?: string; __name?: string } | string\n if (typeof type === 'string') return type\n if (typeof type === 'object' && type) return type.name || type.__name || ''\n return ''\n}\n\n/**\n * 从源 vnode 派生一份未挂载副本。\n * Vue 的 cloneVNode 会拷贝 el/component,同一源 vnode 渲两次会把实例从编辑控件抢走。\n */\nexport function forkUnmountedVNode(node: VNode, extra?: Record<string, unknown>): VNode {\n const forked = extra ? cloneVNode(node, extra) : cloneVNode(node)\n forked.el = null\n forked.component = null\n forked.anchor = null\n forked.suspense = null\n return forked\n}\n\n/**\n * 虚拟node拦截\n * @returns\n */\nexport function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] | null } {\n const slots = useSlots()\n\n function getSlotsNodes() {\n const nodes = slots.default?.()\n if (!nodes?.length) return null\n\n const flattedNodes = extractNormalVNodes(nodes)\n\n const results: SlotRenderItem[] = []\n const seen = new Map<string, number>()\n\n let i = 0\n while (i < flattedNodes.length) {\n const node = flattedNodes[i]!\n i++\n\n const { props, type } = node\n const field = props?.field as string | undefined\n const isFormItem = (type as any)?.name === 'UFormItem'\n\n const formItemProps = o((props ?? {}) as Record<string, any>).pick([\n 'label',\n 'rules',\n 'span',\n 'tips',\n 'readonly',\n 'field'\n ]) as Record<string, any>\n\n let renderKey: string | number | symbol\n if (node.key != null) {\n renderKey = node.key\n } else {\n const base = `${getVNodeTypeName(node)}:${field ?? ''}`\n const n = seen.get(base) ?? 0\n seen.set(base, n + 1)\n renderKey = n === 0 ? `uform:${base}` : `uform:${base}#${n}`\n }\n\n results.push({\n isFormItem,\n formItemProps,\n node,\n field,\n modelValue: props?.['model-value'] ?? props?.modelValue,\n renderKey\n })\n }\n\n return results\n }\n\n return { getSlotsNodes }\n}\n"],"mappings":";;;;AAcA,SAAS,iBAAiB,MAAqB;CAC7C,MAAM,OAAO,KAAK;CAClB,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,IAAI,OAAO,SAAS,YAAY,MAAM,OAAO,KAAK,QAAQ,KAAK,UAAU;CACzE,OAAO;AACT;;;;;AAMA,SAAgB,mBAAmB,MAAa,OAAwC;CACtF,MAAM,SAAS,QAAQ,WAAW,MAAM,KAAK,IAAI,WAAW,IAAI;CAChE,OAAO,KAAK;CACZ,OAAO,YAAY;CACnB,OAAO,SAAS;CAChB,OAAO,WAAW;CAClB,OAAO;AACT;;;;;AAMA,SAAgB,qBAAuE;CACrF,MAAM,QAAQ,SAAS;CAEvB,SAAS,gBAAgB;EACvB,MAAM,QAAQ,MAAM,UAAU;EAC9B,IAAI,CAAC,OAAO,QAAQ,OAAO;EAE3B,MAAM,eAAe,oBAAoB,KAAK;EAE9C,MAAM,UAA4B,CAAC;EACnC,MAAM,uBAAO,IAAI,IAAoB;EAErC,IAAI,IAAI;EACR,OAAO,IAAI,aAAa,QAAQ;GAC9B,MAAM,OAAO,aAAa;GAC1B;GAEA,MAAM,EAAE,OAAO,SAAS;GACxB,MAAM,QAAQ,OAAO;GACrB,MAAM,aAAc,MAAc,SAAS;GAE3C,MAAM,gBAAgB,EAAG,SAAS,CAAC,CAAyB,CAAC,CAAC,KAAK;IACjE;IACA;IACA;IACA;IACA;IACA;GACF,CAAC;GAED,IAAI;GACJ,IAAI,KAAK,OAAO,MACd,YAAY,KAAK;QACZ;IACL,MAAM,OAAO,GAAG,iBAAiB,IAAI,EAAE,GAAG,SAAS;IACnD,MAAM,IAAI,KAAK,IAAI,IAAI,KAAK;IAC5B,KAAK,IAAI,MAAM,IAAI,CAAC;IACpB,YAAY,MAAM,IAAI,SAAS,SAAS,SAAS,KAAK,GAAG;GAC3D;GAEA,QAAQ,KAAK;IACX;IACA;IACA;IACA;IACA,YAAY,QAAQ,kBAAkB,OAAO;IAC7C;GACF,CAAC;EACH;EAEA,OAAO;CACT;CAEA,OAAO,EAAE,cAAc;AACzB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-item.js","names":[],"sources":["../../../src/components/form-item/form-item.vue"],"sourcesContent":["<template>\n <u-grid-item :span=\"span\" :class=\"className\">\n <label\n v-if=\"props.label || $slots.label\"\n :class=\"[cls.e('label'), bem.is('required', !!rules?.required)]\"\n :style=\"labelStyles\"\n >\n <u-tip v-if=\"tips\" :content=\"tips\" :class=\"cls.e('tips')\" :show-delay=\"500\">\n <span :class=\"cls.e('tip-label')\">\n <slot name=\"label\">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>\n </span>\n </u-tip>\n <template v-else>\n <slot name=\"label\">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>\n </template>\n </label>\n\n <section :class=\"cls.e('wrapper')\">\n <div :class=\"cls.e('content')\">\n <ContentSlot>\n <slot></slot>\n </ContentSlot>\n </div>\n\n <!-- 只有表单控件处于非只读状态时,才显示错误提示 -->\n <section :class=\"cls.e('error')\" v-if=\"!readonly && !formProps?.noTips\">\n <transition name=\"form-item-tips\" mode=\"out-in\">\n <span :class=\"cls.e('error-text')\" v-if=\"!!errorTip\">\n {{ errorTip }}\n </span>\n </transition>\n </section>\n </section>\n </u-grid-item>\n</template>\n\n<script lang=\"tsx\" setup>\nimport { o } from '@cat-kit/core'\nimport { useConfig, useFallbackProps } from '@veltra/compositions'\nimport { bem, extractNormalVNodes, withUnit } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport {\n type CSSProperties,\n type SetupContext,\n type VNode,\n cloneVNode,\n computed,\n onBeforeUnmount,\n shallowRef,\n watch\n} from 'vue'\n\nimport type { FormItemEmits, FormItemProps, ComponentSize } from '../../types'\nimport { UGridItem } from '../grid'\nimport { UTip } from '../tip'\nimport { formItemCls as cls, defineField } from './helper'\nimport { validateField } from './validate'\n\ndefineOptions({ name: 'UFormItem' })\n\nconst props = withDefaults(defineProps<FormItemProps>(), { readonly: undefined })\n\nconst emit = defineEmits<FormItemEmits>()\n\ndefineSlots<{\n /** 标签插槽 */\n label?: () => any\n default?: () => any\n}>()\n\n/** 表单组件上下文 */\nconst {
|
|
1
|
+
{"version":3,"file":"form-item.js","names":[],"sources":["../../../src/components/form-item/form-item.vue"],"sourcesContent":["<template>\n <u-grid-item :span=\"span\" :class=\"className\">\n <label\n v-if=\"props.label || $slots.label\"\n :class=\"[cls.e('label'), bem.is('required', !!rules?.required)]\"\n :style=\"labelStyles\"\n >\n <u-tip v-if=\"tips\" :content=\"tips\" :class=\"cls.e('tips')\" :show-delay=\"500\">\n <span :class=\"cls.e('tip-label')\">\n <slot name=\"label\">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>\n </span>\n </u-tip>\n <template v-else>\n <slot name=\"label\">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>\n </template>\n </label>\n\n <section :class=\"cls.e('wrapper')\">\n <div :class=\"cls.e('content')\">\n <ContentSlot>\n <slot></slot>\n </ContentSlot>\n </div>\n\n <!-- 只有表单控件处于非只读状态时,才显示错误提示 -->\n <section :class=\"cls.e('error')\" v-if=\"!readonly && !formProps?.noTips\">\n <transition name=\"form-item-tips\" mode=\"out-in\">\n <span :class=\"cls.e('error-text')\" v-if=\"!!errorTip\">\n {{ errorTip }}\n </span>\n </transition>\n </section>\n </section>\n </u-grid-item>\n</template>\n\n<script lang=\"tsx\" setup>\nimport { o } from '@cat-kit/core'\nimport { useConfig, useFallbackProps } from '@veltra/compositions'\nimport { bem, extractNormalVNodes, withUnit } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport {\n type CSSProperties,\n type SetupContext,\n type VNode,\n cloneVNode,\n computed,\n onBeforeUnmount,\n shallowRef,\n watch\n} from 'vue'\n\nimport type { FormItemEmits, FormItemProps, ComponentSize } from '../../types'\nimport { UGridItem } from '../grid'\nimport { UTip } from '../tip'\nimport { formItemCls as cls, defineField } from './helper'\nimport { validateField } from './validate'\n\ndefineOptions({ name: 'UFormItem' })\n\nconst props = withDefaults(defineProps<FormItemProps>(), { readonly: undefined })\n\nconst emit = defineEmits<FormItemEmits>()\n\ndefineSlots<{\n /** 标签插槽 */\n label?: () => any\n default?: () => any\n}>()\n\n/** 表单组件上下文 */\nconst { formProps, registerField, unregisterField, shouldValidate, handleFieldUpdate } =\n injectFormContext()\n\nfunction wrapControlChange(node: VNode) {\n if (!node || typeof node.type === 'symbol') return node\n\n return cloneVNode(node, {\n onChange: (...args: any[]) => {\n emit('change', ...args)\n }\n })\n}\n\n/**\n * 承载默认插槽并拦截控件 change。\n * 必须经填充插槽接收内容(而非直接读 UFormItem 的 $slots):\n * 读父级 $slots 时组件自身没有更新路径,父级重渲染后动态 props(disabled、data 等)会冻结在首帧。\n */\nfunction ContentSlot(_: unknown, { slots }: SetupContext) {\n return extractNormalVNodes(slots.default?.() ?? []).map(wrapControlChange)\n}\n\nconst { config } = useConfig()\n\nconst { size, readonly } = useFallbackProps([formProps ?? {}, props], {\n size: 'default' as ComponentSize,\n readonly: false\n})\n\nconst errorTip = shallowRef<string>()\n/** 递增序号,丢弃过期的异步校验结果 */\nlet validateSeq = 0\n/** 最近一次 validate 的 Promise,供过期调用等待最新结果 */\nlet latestValidatePromise: Promise<boolean> = Promise.resolve(true)\n\nconst labelPosition = computed(() => props.labelPosition ?? formProps?.labelPosition ?? 'left')\n\nconst className = computed(() => {\n return [\n cls.b,\n cls.m(size.value),\n bem.is('error', !!errorTip.value),\n bem.is('label-top', labelPosition.value === 'top')\n ].join(' ')\n})\n\n/** label样式 */\nconst labelStyles = computed<CSSProperties>(() => {\n if (labelPosition.value === 'top') return {}\n return {\n width: withUnit(props.labelWidth ?? formProps?.labelWidth ?? config.form.labelWidth, 'px')\n }\n})\n\nconst fieldItem = defineField({\n clearValidate() {\n errorTip.value = ''\n },\n async validate() {\n if (!props.field || !formProps?.model || !props.rules || !shouldValidate?.()) return true\n\n const seq = ++validateSeq\n\n latestValidatePromise = validateField(formProps.model!, props.field!, props.rules!).then(\n (tip) => {\n if (seq !== validateSeq) return latestValidatePromise\n\n errorTip.value = tip\n return !errorTip.value\n }\n )\n return latestValidatePromise\n }\n})\n\nwatch(\n () => props.field,\n (field, oldField) => {\n if (oldField) {\n unregisterField?.(oldField)\n }\n if (field) {\n registerField?.(field, fieldItem)\n }\n },\n { immediate: true }\n)\n\nlet stopWatchFieldValue: (() => void) | undefined\n\nwatch(\n [() => formProps?.model, () => props.field],\n ([model, field]) => {\n stopWatchFieldValue?.()\n\n if (!field || !model) return\n\n stopWatchFieldValue = watch(\n () => o(model).get(field),\n (value) => {\n handleFieldUpdate?.(field, value)\n fieldItem.validate()\n }\n )\n },\n { immediate: true }\n)\n\nonBeforeUnmount(() => {\n props.field && unregisterField?.(props.field)\n})\n</script>\n"],"mappings":""}
|
|
@@ -30,12 +30,11 @@ var form_item_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
|
|
|
30
30
|
const props = __props;
|
|
31
31
|
const emit = __emit;
|
|
32
32
|
/** 表单组件上下文 */
|
|
33
|
-
const { formProps, registerField, unregisterField, shouldValidate, handleFieldUpdate
|
|
33
|
+
const { formProps, registerField, unregisterField, shouldValidate, handleFieldUpdate } = injectFormContext();
|
|
34
34
|
function wrapControlChange(node) {
|
|
35
35
|
if (!node || typeof node.type === "symbol") return node;
|
|
36
36
|
return cloneVNode(node, { onChange: (...args) => {
|
|
37
37
|
emit("change", ...args);
|
|
38
|
-
if (props.field) handleFieldChange?.(props.field, ...args);
|
|
39
38
|
} });
|
|
40
39
|
}
|
|
41
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-item.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/form-item/form-item.vue"],"sourcesContent":["<template>\n <u-grid-item :span=\"span\" :class=\"className\">\n <label\n v-if=\"props.label || $slots.label\"\n :class=\"[cls.e('label'), bem.is('required', !!rules?.required)]\"\n :style=\"labelStyles\"\n >\n <u-tip v-if=\"tips\" :content=\"tips\" :class=\"cls.e('tips')\" :show-delay=\"500\">\n <span :class=\"cls.e('tip-label')\">\n <slot name=\"label\">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>\n </span>\n </u-tip>\n <template v-else>\n <slot name=\"label\">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>\n </template>\n </label>\n\n <section :class=\"cls.e('wrapper')\">\n <div :class=\"cls.e('content')\">\n <ContentSlot>\n <slot></slot>\n </ContentSlot>\n </div>\n\n <!-- 只有表单控件处于非只读状态时,才显示错误提示 -->\n <section :class=\"cls.e('error')\" v-if=\"!readonly && !formProps?.noTips\">\n <transition name=\"form-item-tips\" mode=\"out-in\">\n <span :class=\"cls.e('error-text')\" v-if=\"!!errorTip\">\n {{ errorTip }}\n </span>\n </transition>\n </section>\n </section>\n </u-grid-item>\n</template>\n\n<script lang=\"tsx\" setup>\nimport { o } from '@cat-kit/core'\nimport { useConfig, useFallbackProps } from '@veltra/compositions'\nimport { bem, extractNormalVNodes, withUnit } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport {\n type CSSProperties,\n type SetupContext,\n type VNode,\n cloneVNode,\n computed,\n onBeforeUnmount,\n shallowRef,\n watch\n} from 'vue'\n\nimport type { FormItemEmits, FormItemProps, ComponentSize } from '../../types'\nimport { UGridItem } from '../grid'\nimport { UTip } from '../tip'\nimport { formItemCls as cls, defineField } from './helper'\nimport { validateField } from './validate'\n\ndefineOptions({ name: 'UFormItem' })\n\nconst props = withDefaults(defineProps<FormItemProps>(), { readonly: undefined })\n\nconst emit = defineEmits<FormItemEmits>()\n\ndefineSlots<{\n /** 标签插槽 */\n label?: () => any\n default?: () => any\n}>()\n\n/** 表单组件上下文 */\nconst {
|
|
1
|
+
{"version":3,"file":"form-item.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/form-item/form-item.vue"],"sourcesContent":["<template>\n <u-grid-item :span=\"span\" :class=\"className\">\n <label\n v-if=\"props.label || $slots.label\"\n :class=\"[cls.e('label'), bem.is('required', !!rules?.required)]\"\n :style=\"labelStyles\"\n >\n <u-tip v-if=\"tips\" :content=\"tips\" :class=\"cls.e('tips')\" :show-delay=\"500\">\n <span :class=\"cls.e('tip-label')\">\n <slot name=\"label\">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>\n </span>\n </u-tip>\n <template v-else>\n <slot name=\"label\">{{ label }}{{ labelPosition === 'top' ? '' : ':' }}</slot>\n </template>\n </label>\n\n <section :class=\"cls.e('wrapper')\">\n <div :class=\"cls.e('content')\">\n <ContentSlot>\n <slot></slot>\n </ContentSlot>\n </div>\n\n <!-- 只有表单控件处于非只读状态时,才显示错误提示 -->\n <section :class=\"cls.e('error')\" v-if=\"!readonly && !formProps?.noTips\">\n <transition name=\"form-item-tips\" mode=\"out-in\">\n <span :class=\"cls.e('error-text')\" v-if=\"!!errorTip\">\n {{ errorTip }}\n </span>\n </transition>\n </section>\n </section>\n </u-grid-item>\n</template>\n\n<script lang=\"tsx\" setup>\nimport { o } from '@cat-kit/core'\nimport { useConfig, useFallbackProps } from '@veltra/compositions'\nimport { bem, extractNormalVNodes, withUnit } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport {\n type CSSProperties,\n type SetupContext,\n type VNode,\n cloneVNode,\n computed,\n onBeforeUnmount,\n shallowRef,\n watch\n} from 'vue'\n\nimport type { FormItemEmits, FormItemProps, ComponentSize } from '../../types'\nimport { UGridItem } from '../grid'\nimport { UTip } from '../tip'\nimport { formItemCls as cls, defineField } from './helper'\nimport { validateField } from './validate'\n\ndefineOptions({ name: 'UFormItem' })\n\nconst props = withDefaults(defineProps<FormItemProps>(), { readonly: undefined })\n\nconst emit = defineEmits<FormItemEmits>()\n\ndefineSlots<{\n /** 标签插槽 */\n label?: () => any\n default?: () => any\n}>()\n\n/** 表单组件上下文 */\nconst { formProps, registerField, unregisterField, shouldValidate, handleFieldUpdate } =\n injectFormContext()\n\nfunction wrapControlChange(node: VNode) {\n if (!node || typeof node.type === 'symbol') return node\n\n return cloneVNode(node, {\n onChange: (...args: any[]) => {\n emit('change', ...args)\n }\n })\n}\n\n/**\n * 承载默认插槽并拦截控件 change。\n * 必须经填充插槽接收内容(而非直接读 UFormItem 的 $slots):\n * 读父级 $slots 时组件自身没有更新路径,父级重渲染后动态 props(disabled、data 等)会冻结在首帧。\n */\nfunction ContentSlot(_: unknown, { slots }: SetupContext) {\n return extractNormalVNodes(slots.default?.() ?? []).map(wrapControlChange)\n}\n\nconst { config } = useConfig()\n\nconst { size, readonly } = useFallbackProps([formProps ?? {}, props], {\n size: 'default' as ComponentSize,\n readonly: false\n})\n\nconst errorTip = shallowRef<string>()\n/** 递增序号,丢弃过期的异步校验结果 */\nlet validateSeq = 0\n/** 最近一次 validate 的 Promise,供过期调用等待最新结果 */\nlet latestValidatePromise: Promise<boolean> = Promise.resolve(true)\n\nconst labelPosition = computed(() => props.labelPosition ?? formProps?.labelPosition ?? 'left')\n\nconst className = computed(() => {\n return [\n cls.b,\n cls.m(size.value),\n bem.is('error', !!errorTip.value),\n bem.is('label-top', labelPosition.value === 'top')\n ].join(' ')\n})\n\n/** label样式 */\nconst labelStyles = computed<CSSProperties>(() => {\n if (labelPosition.value === 'top') return {}\n return {\n width: withUnit(props.labelWidth ?? formProps?.labelWidth ?? config.form.labelWidth, 'px')\n }\n})\n\nconst fieldItem = defineField({\n clearValidate() {\n errorTip.value = ''\n },\n async validate() {\n if (!props.field || !formProps?.model || !props.rules || !shouldValidate?.()) return true\n\n const seq = ++validateSeq\n\n latestValidatePromise = validateField(formProps.model!, props.field!, props.rules!).then(\n (tip) => {\n if (seq !== validateSeq) return latestValidatePromise\n\n errorTip.value = tip\n return !errorTip.value\n }\n )\n return latestValidatePromise\n }\n})\n\nwatch(\n () => props.field,\n (field, oldField) => {\n if (oldField) {\n unregisterField?.(oldField)\n }\n if (field) {\n registerField?.(field, fieldItem)\n }\n },\n { immediate: true }\n)\n\nlet stopWatchFieldValue: (() => void) | undefined\n\nwatch(\n [() => formProps?.model, () => props.field],\n ([model, field]) => {\n stopWatchFieldValue?.()\n\n if (!field || !model) return\n\n stopWatchFieldValue = watch(\n () => o(model).get(field),\n (value) => {\n handleFieldUpdate?.(field, value)\n fieldItem.validate()\n }\n )\n },\n { immediate: true }\n)\n\nonBeforeUnmount(() => {\n props.field && unregisterField?.(props.field)\n})\n</script>\n"],"mappings":"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"}
|
|
@@ -99,8 +99,6 @@ interface BatchEditProps extends TableProps {
|
|
|
99
99
|
}
|
|
100
100
|
/** 批量编辑组件定义的事件 */
|
|
101
101
|
interface BatchEditEmits extends TableEmits {
|
|
102
|
-
/** 表单控件 change,透传自 UForm */
|
|
103
|
-
(e: 'field:change', field: string, ...args: any[]): void;
|
|
104
102
|
/** 更新数据 */
|
|
105
103
|
(e: 'update:data', value: Record<string, any>[]): void;
|
|
106
104
|
/** 点击底部「新增一行」 */
|
package/dist/types/form.d.ts
CHANGED
|
@@ -28,8 +28,6 @@ interface FormProps extends ComponentProps {
|
|
|
28
28
|
disabled?: boolean;
|
|
29
29
|
}
|
|
30
30
|
interface FormEmits {
|
|
31
|
-
/** 控件 change 事件,仅用户操作触发 */
|
|
32
|
-
(e: 'field:change', field: string, ...args: any[]): void;
|
|
33
31
|
/** model 字段值更新,含编程写入 */
|
|
34
32
|
(e: 'field:update', field: string, value: any): void;
|
|
35
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/desktop",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.12",
|
|
4
4
|
"homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/cabinet-fe/ultra-ui/issues"
|
|
@@ -80,12 +80,12 @@
|
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"@cat-kit/core": ">=1.2.1",
|
|
82
82
|
"@cat-kit/fe": ">=1.2.1",
|
|
83
|
-
"@veltra/compositions": "^1.7.
|
|
84
|
-
"@veltra/directives": "^1.7.
|
|
83
|
+
"@veltra/compositions": "^1.7.12",
|
|
84
|
+
"@veltra/directives": "^1.7.12",
|
|
85
85
|
"@veltra/icons": "^1.5.0",
|
|
86
86
|
"@veltra/sheet-core": "^2.5.6",
|
|
87
|
-
"@veltra/styles": "^1.7.
|
|
88
|
-
"@veltra/utils": "^1.7.
|
|
87
|
+
"@veltra/styles": "^1.7.12",
|
|
88
|
+
"@veltra/utils": "^1.7.12",
|
|
89
89
|
"vue": ">=3.5.42"
|
|
90
90
|
},
|
|
91
91
|
"peerDependenciesMeta": {
|
|
@@ -30,8 +30,6 @@ function mountBatchEdit() {
|
|
|
30
30
|
apiMethod: undefined as string | undefined
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
const fieldChanges: { field: string; args: unknown[] }[] = []
|
|
34
|
-
|
|
35
33
|
const app = createApp({
|
|
36
34
|
render() {
|
|
37
35
|
return h(
|
|
@@ -41,9 +39,6 @@ function mountBatchEdit() {
|
|
|
41
39
|
'onUpdate:data': (value: any[]) => {
|
|
42
40
|
data.value = value
|
|
43
41
|
},
|
|
44
|
-
'onField:change': (field: string, ...args: unknown[]) => {
|
|
45
|
-
fieldChanges.push({ field, args })
|
|
46
|
-
},
|
|
47
42
|
columns,
|
|
48
43
|
model,
|
|
49
44
|
rowKey: 'id',
|
|
@@ -94,7 +89,6 @@ function mountBatchEdit() {
|
|
|
94
89
|
host,
|
|
95
90
|
data,
|
|
96
91
|
model,
|
|
97
|
-
fieldChanges,
|
|
98
92
|
unmount() {
|
|
99
93
|
app.unmount()
|
|
100
94
|
host.remove()
|
|
@@ -202,136 +196,4 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
|
|
|
202
196
|
|
|
203
197
|
unmount()
|
|
204
198
|
})
|
|
205
|
-
|
|
206
|
-
it('切换行回显不触发 field:change', async () => {
|
|
207
|
-
const { host, fieldChanges, unmount } = mountBatchEdit()
|
|
208
|
-
|
|
209
|
-
fieldChanges.length = 0
|
|
210
|
-
await clickRow(host, 0)
|
|
211
|
-
await clickRow(host, 1)
|
|
212
|
-
|
|
213
|
-
expect(fieldChanges).toHaveLength(0)
|
|
214
|
-
|
|
215
|
-
unmount()
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('用户变更 Select 触发 field:change', async () => {
|
|
219
|
-
const { host, fieldChanges, unmount } = mountBatchEdit()
|
|
220
|
-
|
|
221
|
-
await clickRow(host, 0)
|
|
222
|
-
fieldChanges.length = 0
|
|
223
|
-
|
|
224
|
-
const input = host.querySelector<HTMLInputElement>('.u-select input')
|
|
225
|
-
expect(input).toBeTruthy()
|
|
226
|
-
input!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
227
|
-
await nextTick()
|
|
228
|
-
await nextTick()
|
|
229
|
-
|
|
230
|
-
const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
|
|
231
|
-
(el) => el.textContent === '接口'
|
|
232
|
-
)
|
|
233
|
-
expect(option).toBeTruthy()
|
|
234
|
-
option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
235
|
-
await nextTick()
|
|
236
|
-
await nextTick()
|
|
237
|
-
|
|
238
|
-
expect(fieldChanges).toHaveLength(1)
|
|
239
|
-
expect(fieldChanges[0]).toEqual({ field: 'behavior', args: [{ label: '接口', value: 'api' }] })
|
|
240
|
-
|
|
241
|
-
unmount()
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('field:change 联动改其他字段不循环触发', async () => {
|
|
245
|
-
const host = document.createElement('div')
|
|
246
|
-
document.body.appendChild(host)
|
|
247
|
-
|
|
248
|
-
const columns = defineTableColumns([
|
|
249
|
-
{ key: 'label', name: '名称' },
|
|
250
|
-
{ key: 'behavior', name: '行为' }
|
|
251
|
-
])
|
|
252
|
-
|
|
253
|
-
const data = ref([
|
|
254
|
-
{ id: '1', label: '存草稿', behavior: 'common-resource', submitType: 'DRAFT' },
|
|
255
|
-
{ id: '2', label: '调接口', behavior: 'api', submitType: 'SUBMIT', apiMethod: 'POST' }
|
|
256
|
-
])
|
|
257
|
-
|
|
258
|
-
const model = reactive({
|
|
259
|
-
id: '',
|
|
260
|
-
label: '',
|
|
261
|
-
behavior: 'event',
|
|
262
|
-
submitType: undefined as string | undefined,
|
|
263
|
-
apiMethod: undefined as string | undefined
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
const fieldChanges: string[] = []
|
|
267
|
-
|
|
268
|
-
const app = createApp({
|
|
269
|
-
render() {
|
|
270
|
-
return h(
|
|
271
|
-
UBatchEdit,
|
|
272
|
-
{
|
|
273
|
-
data: data.value,
|
|
274
|
-
'onUpdate:data': (value: any[]) => {
|
|
275
|
-
data.value = value
|
|
276
|
-
},
|
|
277
|
-
'onField:change': (field: string) => {
|
|
278
|
-
fieldChanges.push(field)
|
|
279
|
-
if (field === 'behavior') {
|
|
280
|
-
model.submitType = undefined
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
columns,
|
|
284
|
-
model,
|
|
285
|
-
rowKey: 'id',
|
|
286
|
-
quickEdit: true
|
|
287
|
-
},
|
|
288
|
-
{
|
|
289
|
-
form: () => [
|
|
290
|
-
h(UInput, { field: 'label', label: '名称' }),
|
|
291
|
-
h(USelect, {
|
|
292
|
-
field: 'behavior',
|
|
293
|
-
label: '行为',
|
|
294
|
-
options: [
|
|
295
|
-
{ label: '事件', value: 'event' },
|
|
296
|
-
{ label: '通用资源', value: 'common-resource' },
|
|
297
|
-
{ label: '接口', value: 'api' }
|
|
298
|
-
]
|
|
299
|
-
}),
|
|
300
|
-
h(USelect, {
|
|
301
|
-
field: 'submitType',
|
|
302
|
-
label: '提交类型',
|
|
303
|
-
options: [
|
|
304
|
-
{ label: 'DRAFT', value: 'DRAFT' },
|
|
305
|
-
{ label: 'SUBMIT', value: 'SUBMIT' }
|
|
306
|
-
]
|
|
307
|
-
})
|
|
308
|
-
]
|
|
309
|
-
}
|
|
310
|
-
)
|
|
311
|
-
}
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
app.mount(host)
|
|
315
|
-
|
|
316
|
-
await clickRow(host, 0)
|
|
317
|
-
fieldChanges.length = 0
|
|
318
|
-
|
|
319
|
-
const input = host.querySelector<HTMLInputElement>('.u-select input')
|
|
320
|
-
input!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
321
|
-
await nextTick()
|
|
322
|
-
await nextTick()
|
|
323
|
-
|
|
324
|
-
const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
|
|
325
|
-
(el) => el.textContent === '接口'
|
|
326
|
-
)
|
|
327
|
-
option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
328
|
-
await nextTick()
|
|
329
|
-
await nextTick()
|
|
330
|
-
|
|
331
|
-
expect(fieldChanges).toEqual(['behavior'])
|
|
332
|
-
expect(model.submitType).toBeUndefined()
|
|
333
|
-
|
|
334
|
-
app.unmount()
|
|
335
|
-
host.remove()
|
|
336
|
-
})
|
|
337
199
|
})
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
:readonly="props.readonly"
|
|
30
30
|
:label-width="props.labelWidth"
|
|
31
31
|
@field:update="handleFieldUpdate"
|
|
32
|
-
@field:change="handleFormFieldChange"
|
|
33
32
|
>
|
|
34
33
|
<slot
|
|
35
34
|
v-bind="{
|
|
@@ -92,17 +91,8 @@ defineOptions({ name: 'UBatchEditForm' })
|
|
|
92
91
|
|
|
93
92
|
const batchEditCtx = inject(BatchEditDIKey)!
|
|
94
93
|
|
|
95
|
-
const {
|
|
96
|
-
|
|
97
|
-
props,
|
|
98
|
-
state,
|
|
99
|
-
emit,
|
|
100
|
-
handleClose,
|
|
101
|
-
handleSave,
|
|
102
|
-
staticFeatures,
|
|
103
|
-
dynamicFeatures,
|
|
104
|
-
syncing
|
|
105
|
-
} = batchEditCtx
|
|
94
|
+
const { cls, props, state, handleClose, handleSave, staticFeatures, dynamicFeatures, syncing } =
|
|
95
|
+
batchEditCtx
|
|
106
96
|
|
|
107
97
|
const focused = toRef(batchEditCtx, 'focused')
|
|
108
98
|
|
|
@@ -124,10 +114,6 @@ function handleFieldUpdate(field: string, value: any) {
|
|
|
124
114
|
}
|
|
125
115
|
}
|
|
126
116
|
|
|
127
|
-
function handleFormFieldChange(field: string, ...args: any[]) {
|
|
128
|
-
emit('field:change', field, ...args)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
117
|
const creatable = computed(() => {
|
|
132
118
|
if (state.formActionType !== 'create') return false
|
|
133
119
|
return staticFeatures.value.has('create') || dynamicFeatures.value.create?.(state.row)
|
|
@@ -1,42 +1,22 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createApp, h, nextTick, reactive, ref } from 'vue'
|
|
2
|
+
import { createApp, defineComponent, h, nextTick, reactive, ref } from 'vue'
|
|
3
3
|
|
|
4
4
|
import { UInput } from '../../input'
|
|
5
5
|
import { USelect } from '../../select'
|
|
6
6
|
import { UForm } from '../index'
|
|
7
7
|
|
|
8
|
-
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
9
|
-
|
|
10
8
|
const behaviorOptions = [
|
|
11
9
|
{ label: '事件', value: 'event' },
|
|
12
10
|
{ label: '接口', value: 'api' }
|
|
13
11
|
]
|
|
14
12
|
|
|
15
|
-
async function openSelect(host: HTMLElement) {
|
|
16
|
-
const input = host.querySelector('input')!
|
|
17
|
-
input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
18
|
-
await nextTick()
|
|
19
|
-
await nextTick()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function clickSelectOption(label: string) {
|
|
23
|
-
const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
|
|
24
|
-
(el) => el.textContent === label
|
|
25
|
-
)
|
|
26
|
-
expect(option, `选项「${label}」应存在`).toBeTruthy()
|
|
27
|
-
option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
28
|
-
await nextTick()
|
|
29
|
-
await sleep(50)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
13
|
describe('UForm field events', () => {
|
|
33
|
-
it('field:update
|
|
14
|
+
it('编程写入触发 field:update,不依赖用户操作', async () => {
|
|
34
15
|
const host = document.createElement('div')
|
|
35
16
|
document.body.appendChild(host)
|
|
36
17
|
|
|
37
18
|
const model = reactive({ behavior: 'event' })
|
|
38
19
|
const updates: { field: string; value: unknown }[] = []
|
|
39
|
-
const changes: { field: string; args: unknown[] }[] = []
|
|
40
20
|
|
|
41
21
|
const app = createApp({
|
|
42
22
|
render() {
|
|
@@ -46,9 +26,6 @@ describe('UForm field events', () => {
|
|
|
46
26
|
model,
|
|
47
27
|
'onField:update': (field: string, value: unknown) => {
|
|
48
28
|
updates.push({ field, value })
|
|
49
|
-
},
|
|
50
|
-
'onField:change': (field: string, ...args: unknown[]) => {
|
|
51
|
-
changes.push({ field, args })
|
|
52
29
|
}
|
|
53
30
|
},
|
|
54
31
|
{
|
|
@@ -61,19 +38,11 @@ describe('UForm field events', () => {
|
|
|
61
38
|
|
|
62
39
|
app.mount(host)
|
|
63
40
|
updates.length = 0
|
|
64
|
-
changes.length = 0
|
|
65
41
|
|
|
66
42
|
model.behavior = 'api'
|
|
67
43
|
await nextTick()
|
|
68
44
|
|
|
69
45
|
expect(updates.some((item) => item.field === 'behavior' && item.value === 'api')).toBe(true)
|
|
70
|
-
expect(changes).toHaveLength(0)
|
|
71
|
-
|
|
72
|
-
await openSelect(host)
|
|
73
|
-
await clickSelectOption('事件')
|
|
74
|
-
|
|
75
|
-
expect(changes).toHaveLength(1)
|
|
76
|
-
expect(changes[0]).toEqual({ field: 'behavior', args: [{ label: '事件', value: 'event' }] })
|
|
77
46
|
|
|
78
47
|
app.unmount()
|
|
79
48
|
host.remove()
|
|
@@ -122,3 +91,126 @@ describe('UForm field events', () => {
|
|
|
122
91
|
host.remove()
|
|
123
92
|
})
|
|
124
93
|
})
|
|
94
|
+
|
|
95
|
+
/** 用 setup 期间生成的 id 断言实例身份 */
|
|
96
|
+
let probeSeq = 0
|
|
97
|
+
const Probe = defineComponent({
|
|
98
|
+
name: 'Probe',
|
|
99
|
+
props: { field: String, label: String, modelValue: String },
|
|
100
|
+
setup(props) {
|
|
101
|
+
const id = `p${++probeSeq}`
|
|
102
|
+
return () =>
|
|
103
|
+
h('div', { class: 'probe', 'data-field': props.field, 'data-id': id }, [
|
|
104
|
+
h('span', { class: 'probe-value' }, props.modelValue ?? '')
|
|
105
|
+
])
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('UForm slot identity', () => {
|
|
110
|
+
it('中间字段卸载后不复用相邻控件实例', async () => {
|
|
111
|
+
probeSeq = 0
|
|
112
|
+
const host = document.createElement('div')
|
|
113
|
+
document.body.appendChild(host)
|
|
114
|
+
|
|
115
|
+
const model = reactive({ a: 'A', b: 'B', c: 'C' })
|
|
116
|
+
const showB = ref(true)
|
|
117
|
+
|
|
118
|
+
const app = createApp({
|
|
119
|
+
render() {
|
|
120
|
+
return h(
|
|
121
|
+
UForm,
|
|
122
|
+
{ model },
|
|
123
|
+
{
|
|
124
|
+
default: () => [
|
|
125
|
+
h(Probe, { field: 'a', label: 'A' }),
|
|
126
|
+
showB.value ? h(Probe, { field: 'b', label: 'B' }) : null,
|
|
127
|
+
h(Probe, { field: 'c', label: 'C' })
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
app.mount(host)
|
|
135
|
+
|
|
136
|
+
const idOf = (field: string) =>
|
|
137
|
+
host.querySelector(`[data-field="${field}"]`)?.getAttribute('data-id')
|
|
138
|
+
|
|
139
|
+
const cId = idOf('c')
|
|
140
|
+
expect(cId).toBe('p3')
|
|
141
|
+
|
|
142
|
+
showB.value = false
|
|
143
|
+
await nextTick()
|
|
144
|
+
|
|
145
|
+
expect(host.querySelector('[data-field="b"]')).toBeNull()
|
|
146
|
+
expect(idOf('c')).toBe(cId)
|
|
147
|
+
expect(host.querySelector('[data-field="c"] .probe-value')!.textContent).toBe('C')
|
|
148
|
+
|
|
149
|
+
app.unmount()
|
|
150
|
+
host.remove()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('重复 field 各自保有独立实例', async () => {
|
|
154
|
+
probeSeq = 0
|
|
155
|
+
const host = document.createElement('div')
|
|
156
|
+
document.body.appendChild(host)
|
|
157
|
+
|
|
158
|
+
const model = reactive({ name: '同名' })
|
|
159
|
+
|
|
160
|
+
const app = createApp({
|
|
161
|
+
render() {
|
|
162
|
+
return h(
|
|
163
|
+
UForm,
|
|
164
|
+
{ model },
|
|
165
|
+
{
|
|
166
|
+
default: () => [
|
|
167
|
+
h(Probe, { field: 'name', label: '左' }),
|
|
168
|
+
h(Probe, { field: 'name', label: '右' })
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
app.mount(host)
|
|
176
|
+
|
|
177
|
+
const probes = [...host.querySelectorAll('.probe')]
|
|
178
|
+
expect(probes).toHaveLength(2)
|
|
179
|
+
expect(probes[0]!.getAttribute('data-id')).toBe('p1')
|
|
180
|
+
expect(probes[1]!.getAttribute('data-id')).toBe('p2')
|
|
181
|
+
expect(probes[0]!.querySelector('.probe-value')!.textContent).toBe('同名')
|
|
182
|
+
expect(probes[1]!.querySelector('.probe-value')!.textContent).toBe('同名')
|
|
183
|
+
|
|
184
|
+
app.unmount()
|
|
185
|
+
host.remove()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('showModified 变更前副本不抢走编辑控件', async () => {
|
|
189
|
+
const host = document.createElement('div')
|
|
190
|
+
document.body.appendChild(host)
|
|
191
|
+
|
|
192
|
+
const model = reactive({ name: '新值' })
|
|
193
|
+
const initialModel = { name: '旧值' }
|
|
194
|
+
|
|
195
|
+
const app = createApp({
|
|
196
|
+
render() {
|
|
197
|
+
return h(
|
|
198
|
+
UForm,
|
|
199
|
+
{ model, initialModel, showModified: true },
|
|
200
|
+
{ default: () => h(UInput, { field: 'name', label: '名称' }) }
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
app.mount(host)
|
|
206
|
+
await nextTick()
|
|
207
|
+
|
|
208
|
+
const live = host.querySelector('input') as HTMLInputElement | null
|
|
209
|
+
expect(live).toBeTruthy()
|
|
210
|
+
expect(live!.value).toBe('新值')
|
|
211
|
+
expect(host.querySelector('.u-form__data-before')?.textContent).toContain('旧值')
|
|
212
|
+
|
|
213
|
+
app.unmount()
|
|
214
|
+
host.remove()
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
:class="[cls.b, bem.is('readonly', readonly)]"
|
|
9
9
|
>
|
|
10
10
|
<template
|
|
11
|
-
v-for="{ node, isFormItem, formItemProps, field, modelValue } of getSlotsNodes()"
|
|
12
|
-
:key="
|
|
11
|
+
v-for="{ node, isFormItem, formItemProps, field, modelValue, renderKey } of getSlotsNodes()"
|
|
12
|
+
:key="renderKey"
|
|
13
13
|
>
|
|
14
14
|
<u-form-item v-if="!isFormItem && field" v-bind="formItemProps">
|
|
15
15
|
<component
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
/>
|
|
20
20
|
<div v-if="shouldShowModified(field)" :class="cls.e('data-before')">
|
|
21
21
|
<span :class="cls.e('changed-tag')">{{ modifiedLabel }}</span>
|
|
22
|
-
<component :is="node
|
|
22
|
+
<component :is="createModifiedPreview(node, field)" />
|
|
23
23
|
</div>
|
|
24
24
|
</u-form-item>
|
|
25
25
|
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
import { o } from '@cat-kit/core'
|
|
33
33
|
import { bem } from '@veltra/utils'
|
|
34
34
|
import { provideFormContext } from '@veltra/utils'
|
|
35
|
-
import { nextTick, toRef, useTemplateRef } from 'vue'
|
|
35
|
+
import { nextTick, toRef, useTemplateRef, type VNode } from 'vue'
|
|
36
36
|
|
|
37
37
|
import type { BreakCols, FormProps, _FormExposed, FormEmits } from '../../types'
|
|
38
38
|
import { UFormItem } from '../form-item'
|
|
39
39
|
import { UGrid } from '../grid'
|
|
40
40
|
import { isFieldModified } from './is-field-modified'
|
|
41
41
|
import { useFormFields } from './use-form-fields'
|
|
42
|
-
import { useNodeInterceptor } from './use-node-interceptor'
|
|
42
|
+
import { forkUnmountedVNode, useNodeInterceptor } from './use-node-interceptor'
|
|
43
43
|
|
|
44
44
|
defineOptions({ name: 'UForm' })
|
|
45
45
|
|
|
@@ -81,18 +81,13 @@ function handleFieldUpdate(field: string, value: any) {
|
|
|
81
81
|
emit('field:update', field, value)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function handleFieldChange(field: string, ...args: any[]) {
|
|
85
|
-
emit('field:change', field, ...args)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
provideFormContext({
|
|
89
85
|
formProps: props,
|
|
90
86
|
registerField,
|
|
91
87
|
unregisterField,
|
|
92
88
|
validateFields: validate,
|
|
93
89
|
shouldValidate,
|
|
94
|
-
handleFieldUpdate
|
|
95
|
-
handleFieldChange
|
|
90
|
+
handleFieldUpdate
|
|
96
91
|
})
|
|
97
92
|
|
|
98
93
|
const { getSlotsNodes } = useNodeInterceptor()
|
|
@@ -106,6 +101,10 @@ function getBaselineFieldValue(field: string) {
|
|
|
106
101
|
return o(getBaselineModel() ?? {}).get(field)
|
|
107
102
|
}
|
|
108
103
|
|
|
104
|
+
function createModifiedPreview(node: VNode, field: string) {
|
|
105
|
+
return forkUnmountedVNode(node, { readonly: true, modelValue: getBaselineFieldValue(field) })
|
|
106
|
+
}
|
|
107
|
+
|
|
109
108
|
function shouldShowModified(field: string) {
|
|
110
109
|
if (!props.showModified) return false
|
|
111
110
|
const baseline = getBaselineModel()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { o } from '@cat-kit/core'
|
|
2
2
|
import { extractNormalVNodes } from '@veltra/utils'
|
|
3
|
-
import { useSlots, type VNode } from 'vue'
|
|
3
|
+
import { cloneVNode, useSlots, type VNode } from 'vue'
|
|
4
4
|
|
|
5
5
|
export type SlotRenderItem = {
|
|
6
6
|
isFormItem: boolean
|
|
@@ -8,6 +8,28 @@ export type SlotRenderItem = {
|
|
|
8
8
|
node: VNode
|
|
9
9
|
field?: string
|
|
10
10
|
modelValue?: any
|
|
11
|
+
/** v-for 身份:调用方 :key 优先,否则 type+field+出现次序 */
|
|
12
|
+
renderKey: string | number | symbol
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getVNodeTypeName(node: VNode): string {
|
|
16
|
+
const type = node.type as { name?: string; __name?: string } | string
|
|
17
|
+
if (typeof type === 'string') return type
|
|
18
|
+
if (typeof type === 'object' && type) return type.name || type.__name || ''
|
|
19
|
+
return ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 从源 vnode 派生一份未挂载副本。
|
|
24
|
+
* Vue 的 cloneVNode 会拷贝 el/component,同一源 vnode 渲两次会把实例从编辑控件抢走。
|
|
25
|
+
*/
|
|
26
|
+
export function forkUnmountedVNode(node: VNode, extra?: Record<string, unknown>): VNode {
|
|
27
|
+
const forked = extra ? cloneVNode(node, extra) : cloneVNode(node)
|
|
28
|
+
forked.el = null
|
|
29
|
+
forked.component = null
|
|
30
|
+
forked.anchor = null
|
|
31
|
+
forked.suspense = null
|
|
32
|
+
return forked
|
|
11
33
|
}
|
|
12
34
|
|
|
13
35
|
/**
|
|
@@ -24,6 +46,7 @@ export function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] |
|
|
|
24
46
|
const flattedNodes = extractNormalVNodes(nodes)
|
|
25
47
|
|
|
26
48
|
const results: SlotRenderItem[] = []
|
|
49
|
+
const seen = new Map<string, number>()
|
|
27
50
|
|
|
28
51
|
let i = 0
|
|
29
52
|
while (i < flattedNodes.length) {
|
|
@@ -43,12 +66,23 @@ export function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] |
|
|
|
43
66
|
'field'
|
|
44
67
|
]) as Record<string, any>
|
|
45
68
|
|
|
69
|
+
let renderKey: string | number | symbol
|
|
70
|
+
if (node.key != null) {
|
|
71
|
+
renderKey = node.key
|
|
72
|
+
} else {
|
|
73
|
+
const base = `${getVNodeTypeName(node)}:${field ?? ''}`
|
|
74
|
+
const n = seen.get(base) ?? 0
|
|
75
|
+
seen.set(base, n + 1)
|
|
76
|
+
renderKey = n === 0 ? `uform:${base}` : `uform:${base}#${n}`
|
|
77
|
+
}
|
|
78
|
+
|
|
46
79
|
results.push({
|
|
47
80
|
isFormItem,
|
|
48
81
|
formItemProps,
|
|
49
82
|
node,
|
|
50
83
|
field,
|
|
51
|
-
modelValue: props?.['model-value'] ?? props?.modelValue
|
|
84
|
+
modelValue: props?.['model-value'] ?? props?.modelValue,
|
|
85
|
+
renderKey
|
|
52
86
|
})
|
|
53
87
|
}
|
|
54
88
|
|
|
@@ -37,18 +37,12 @@ describe('UFormItem change', () => {
|
|
|
37
37
|
|
|
38
38
|
const model = reactive({ behavior: 'event' })
|
|
39
39
|
const itemChanges: unknown[][] = []
|
|
40
|
-
const formChanges: { field: string; args: unknown[] }[] = []
|
|
41
40
|
|
|
42
41
|
const app = createApp({
|
|
43
42
|
render() {
|
|
44
43
|
return h(
|
|
45
44
|
UForm,
|
|
46
|
-
{
|
|
47
|
-
model,
|
|
48
|
-
'onField:change': (field: string, ...args: unknown[]) => {
|
|
49
|
-
formChanges.push({ field, args })
|
|
50
|
-
}
|
|
51
|
-
},
|
|
45
|
+
{ model },
|
|
52
46
|
{
|
|
53
47
|
default: () =>
|
|
54
48
|
h(
|
|
@@ -82,9 +76,6 @@ describe('UFormItem change', () => {
|
|
|
82
76
|
expect(itemChanges).toHaveLength(1)
|
|
83
77
|
expect(itemChanges[0]![0]).toEqual({ label: '接口', value: 'api' })
|
|
84
78
|
|
|
85
|
-
expect(formChanges).toHaveLength(1)
|
|
86
|
-
expect(formChanges[0]).toEqual({ field: 'behavior', args: [{ label: '接口', value: 'api' }] })
|
|
87
|
-
|
|
88
79
|
app.unmount()
|
|
89
80
|
host.remove()
|
|
90
81
|
})
|
|
@@ -69,14 +69,8 @@ defineSlots<{
|
|
|
69
69
|
}>()
|
|
70
70
|
|
|
71
71
|
/** 表单组件上下文 */
|
|
72
|
-
const {
|
|
73
|
-
|
|
74
|
-
registerField,
|
|
75
|
-
unregisterField,
|
|
76
|
-
shouldValidate,
|
|
77
|
-
handleFieldUpdate,
|
|
78
|
-
handleFieldChange
|
|
79
|
-
} = injectFormContext()
|
|
72
|
+
const { formProps, registerField, unregisterField, shouldValidate, handleFieldUpdate } =
|
|
73
|
+
injectFormContext()
|
|
80
74
|
|
|
81
75
|
function wrapControlChange(node: VNode) {
|
|
82
76
|
if (!node || typeof node.type === 'symbol') return node
|
|
@@ -84,7 +78,6 @@ function wrapControlChange(node: VNode) {
|
|
|
84
78
|
return cloneVNode(node, {
|
|
85
79
|
onChange: (...args: any[]) => {
|
|
86
80
|
emit('change', ...args)
|
|
87
|
-
if (props.field) handleFieldChange?.(props.field, ...args)
|
|
88
81
|
}
|
|
89
82
|
})
|
|
90
83
|
}
|
package/src/types/batch-edit.ts
CHANGED
|
@@ -115,8 +115,6 @@ export interface BatchEditProps extends TableProps {
|
|
|
115
115
|
|
|
116
116
|
/** 批量编辑组件定义的事件 */
|
|
117
117
|
export interface BatchEditEmits extends TableEmits {
|
|
118
|
-
/** 表单控件 change,透传自 UForm */
|
|
119
|
-
(e: 'field:change', field: string, ...args: any[]): void
|
|
120
118
|
/** 更新数据 */
|
|
121
119
|
(e: 'update:data', value: Record<string, any>[]): void
|
|
122
120
|
/** 点击底部「新增一行」 */
|
package/src/types/form.ts
CHANGED
|
@@ -29,8 +29,6 @@ export interface FormProps extends ComponentProps {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface FormEmits {
|
|
32
|
-
/** 控件 change 事件,仅用户操作触发 */
|
|
33
|
-
(e: 'field:change', field: string, ...args: any[]): void
|
|
34
32
|
/** model 字段值更新,含编程写入 */
|
|
35
33
|
(e: 'field:update', field: string, value: any): void
|
|
36
34
|
}
|