tmagic-editor/packages/form/src/FormDialog.vue
roymondchen b7c04edcd5 feat(form): 新增 validateOnInit,默认关闭初始化时自动校验
属性面板仍开启初始化校验,避免打开普通表单时立即展示错误态。
2026-07-28 16:45:42 +08:00

221 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<TMagicDialog
v-model="dialogVisible"
:class="['m-form-dialog', effectiveTheme ? `m-theme--${effectiveTheme}` : '']"
top="20px"
append-to-body
:title="title"
:width="width"
:zIndex="zIndex"
:fullscreen="fullscreen"
:close-on-click-modal="closeOnClickModal"
:close-on-press-escape="closeOnPressEscape"
:destroy-on-close="destroyOnClose"
:show-close="showClose"
@close="closeHandler"
>
<div
v-if="dialogVisible"
class="m-dialog-body"
:style="`max-height: ${bodyHeight}; overflow-y: auto; overflow-x: hidden;`"
>
<Form
v-model="stepActive"
ref="form"
:size="size"
:disabled="disabled"
:config="config"
:init-values="values"
:parent-values="parentValues"
:label-width="labelWidth"
:label-position="labelPosition"
:inline="inline"
:prevent-submit-default="preventSubmitDefault"
:use-field-text-in-error="useFieldTextInError"
:type-match-valid="typeMatchValid"
:validate-on-init="validateOnInit"
:extend-state="extendState"
:theme="effectiveTheme"
@change="changeHandler"
></Form>
<slot></slot>
</div>
<template #footer>
<TMagicRow class="dialog-footer">
<TMagicCol :span="12" style="text-align: left">
<div style="min-height: 1px">
<slot name="left"></slot>
</div>
</TMagicCol>
<TMagicCol :span="12">
<slot name="footer">
<TMagicButton v-if="showCancel" @click="cancel" size="small"> </TMagicButton>
<TMagicButton v-if="hasStep && stepActive > 1" type="info" size="small" @click="preStep"
>上一步</TMagicButton
>
<TMagicButton v-if="hasStep && stepCount > stepActive" type="info" size="small" @click="nextStep"
>下一步</TMagicButton
>
<TMagicButton v-else type="primary" size="small" :disabled="disabled" :loading="saveFetch" @click="save">{{
confirmText
}}</TMagicButton>
</slot>
</TMagicCol>
</TMagicRow>
</template>
</TMagicDialog>
</template>
<script setup lang="ts">
import { computed, inject, provide, ref } from 'vue';
import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDialog, TMagicRow } from '@tmagic/design';
import Form from './Form.vue';
import { ContainerChangeEventData, FormConfig, FormState, FormValue, StepConfig } from './schema';
defineOptions({
name: 'MFormDialog',
});
const props = withDefaults(
defineProps<{
config?: FormConfig;
values?: Object;
parentValues?: Object;
width?: string | number;
labelWidth?: string;
/** 是否开启类型匹配校验 */
typeMatchValid?: boolean;
/** 透传给内部 `MForm`,初始化完成后是否立即校验(默认 `false` */
validateOnInit?: boolean;
fullscreen?: boolean;
disabled?: boolean;
title?: string;
inline?: boolean;
labelPosition?: string;
zIndex?: number;
size?: 'small' | 'default' | 'large';
confirmText?: string;
preventSubmitDefault?: boolean;
closeOnClickModal?: boolean;
closeOnPressEscape?: boolean;
destroyOnClose?: boolean;
showClose?: boolean;
showCancel?: boolean;
/** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
useFieldTextInError?: boolean;
/** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 主题名。优先级:传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串。
* 计算结果会再次 `provide` 出去,使得 Dialog 被 Teleport 到 body 后,内部子树
* (包括 `MForm` 以及更深的 popover / dropdown仍能拿到主题。
*/
theme?: string;
}>(),
{
config: () => [],
values: () => ({}),
confirmText: '确定',
closeOnClickModal: false,
closeOnPressEscape: false,
destroyOnClose: false,
showClose: true,
showCancel: true,
useFieldTextInError: true,
validateOnInit: false,
},
);
const ancestorTheme = inject(M_THEME_KEY, null);
const effectiveTheme = computed(() => props.theme || ancestorTheme?.value || '');
provide(M_THEME_KEY, effectiveTheme);
const emit = defineEmits(['close', 'submit', 'error', 'change']);
const form = ref<InstanceType<typeof Form>>();
const dialogVisible = ref(false);
const saveFetch = ref(false);
const stepActive = ref(1);
const bodyHeight = ref(`${document.body.clientHeight - 194}px`);
const stepCount = computed(() => {
if (!Array.isArray(props.config)) {
return 0;
}
for (const item of props.config) {
if ('type' in item && item.type === 'step') {
return (item as StepConfig).items.length;
}
}
return 0;
});
const hasStep = computed(() => {
if (!Array.isArray(props.config)) {
return false;
}
for (const item of props.config) {
if ('type' in item && item.type === 'step') {
return true;
}
}
return false;
});
const closeHandler = () => {
stepActive.value = 1;
emit('close');
};
const save = async () => {
try {
const changeRecords = [...(form.value?.changeRecords || [])];
const values = await form.value?.submitForm();
emit('submit', values, { changeRecords });
} catch (e) {
emit('error', e);
}
};
const preStep = () => {
stepActive.value -= 1;
};
const nextStep = () => {
stepActive.value += 1;
};
const changeHandler = (value: FormValue, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);
};
const show = () => {
dialogVisible.value = true;
};
const hide = () => {
dialogVisible.value = false;
};
const cancel = () => {
hide();
};
defineExpose({
form,
saveFetch,
dialogVisible,
cancel,
save,
show,
hide,
});
</script>