fix: fix lint and add new form-ui features

feat(form-ui): 在 dependencies 里提供访问extendApi的能力
pull/348/MERGE
allen 2026-04-15 14:21:39 +08:00
parent 991408b451
commit 33e2582f60
8 changed files with 192 additions and 189 deletions

View File

@ -1,19 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CollapsibleParamSchema } from '@vben-core/shadcn-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { ref } from 'vue'; import { NButton, NCard, useMessage } from 'naive-ui';
import { Page, useVbenModal, z } from '@vben/common-ui';
import { VbenCollapsibleParams } from '@vben-core/shadcn-ui';
import {
NButton,
NCard,
NRadioButton,
NRadioGroup,
useMessage,
} from 'naive-ui';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { getAllMenusApi } from '#/api'; import { getAllMenusApi } from '#/api';
@ -22,111 +10,6 @@ import modalDemo from './modal.vue';
const message = useMessage(); const message = useMessage();
const layouts = [
{ label: 'Vertical', value: 'vertical' },
{ label: 'Horizontal', value: 'horizontal' },
{ label: 'Inline', value: 'inline' },
];
const layout = ref(layouts[0].value);
function getNumberValidator(key: string, limit?: [number, number]) {
let validator = z.number({
required_error: `${key} 值不能为空`,
invalid_type_error: `${key} 值只能为数字`,
});
if (limit) {
validator = validator
.min(limit[0], { message: `${key} 值不在区间范围内` })
.max(limit[1], { message: `${key} 值不在区间范围内` });
}
return validator.default(null);
}
const paramsSchema = [
{
key: 'micro_batch_size',
description: `批次大小,代表模型训练过程中,模型更新模型参数的数据步长,可理解为模型每看多少数据即更新一次模型参数,
一般建议的批次大小为16/32表示模型每看16或32条数据即更新一次参数`,
// defaultValue: 8,
option: {
min: 8,
max: 1024,
step: 8,
},
},
{
key: 'learning_rate',
description:
'学习率,代表每次更新数据的增量参数权重,学习率数值越大参数变化越大,对模型影响越大',
// defaultValue: 1e-5,
option: {
step: 1e-4,
type: 'exponential',
},
},
{
key: 'eval_steps',
description:
'验证步数,训练阶段针模型的验证间隔步长,用于阶段性评估模型训练准确率、训练损失',
// defaultValue: 50,
option: {
min: 1,
max: 2_147_483_647,
},
},
{
key: 'num_train_epochs',
description:
'循环次数代表模型训练过程中模型学习数据集的次数可理解为看几遍数据一般建议的范围是1-3遍即可可依据需求进行调整',
// defaultValue: 3,
option: {
min: 1,
max: 200,
},
},
{
key: 'max_length',
description: `序列长度,单个训练数据样本的最大长度,超出配置长度将丢弃`,
// defaultValue: 32_768,
option: {
min: 500,
max: 131_072,
},
},
{
key: 'warmup_ratio',
description: '学习率预热比例,学习率预热阶段占总训练步数的比例',
// defaultValue: 0.05,
option: {
min: 0,
max: 1,
precision: 2,
step: 0.01,
},
},
{
key: 'save_steps',
description: 'Checkpoint保存间隔',
// defaultValue: 50,
option: {
min: 1,
max: 2_147_483_647,
},
},
] as CollapsibleParamSchema[];
const paramsValidator = z.object({
micro_batch_size: getNumberValidator('micro_batch_size', [8, 1024]),
learning_rate: getNumberValidator('learning_rate'),
eval_steps: getNumberValidator('eval_steps', [1, 2_147_483_647]),
num_train_epochs: getNumberValidator('num_train_epochs', [1, 200]),
max_length: getNumberValidator('max_length', [1, 131_072]),
warmup_ratio: getNumberValidator('warmup_ratio', [0, 1]),
save_steps: getNumberValidator('save_steps', [1, 2_147_483_647]),
});
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
commonConfig: { commonConfig: {
// //
@ -260,17 +143,6 @@ const [Form, formApi] = useVbenForm({
}, },
collapsible: true, collapsible: true,
}, },
{
component: VbenCollapsibleParams,
componentProps: {
params: paramsSchema,
},
modelPropName: 'value',
fieldName: 'params',
label: '参数配置',
formItemClass: 'col-span-2',
rules: paramsValidator,
},
], ],
}); });
@ -282,27 +154,12 @@ function setFormValues() {
radioButton: 'C', radioButton: 'C',
checkbox: ['A', 'C'], checkbox: ['A', 'C'],
date: Date.now(), date: Date.now(),
params: {
micro_batch_size: 8,
learning_rate: 1e-5,
eval_steps: 50,
num_train_epochs: 3,
max_length: 32_768,
warmup_ratio: 0.05,
save_steps: 50,
},
}); });
} }
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
connectedComponent: modalDemo, connectedComponent: modalDemo,
}); });
function onLayoutChange(layout: string) {
formApi.setState({
layout,
});
}
</script> </script>
<template> <template>
<Page <Page
@ -311,14 +168,6 @@ function onLayoutChange(layout: string) {
> >
<NCard title="基础表单" header-extra-class="gap-4"> <NCard title="基础表单" header-extra-class="gap-4">
<template #header-extra> <template #header-extra>
<NRadioGroup v-model:value="layout" @update:value="onLayoutChange">
<NRadioButton
v-for="layoutItem in layouts"
:key="layoutItem.value"
:value="layoutItem.value"
:label="layoutItem.label"
/>
</NRadioGroup>
<NButton type="primary" @click="setFormValues"></NButton> <NButton type="primary" @click="setFormValues"></NButton>
<NButton type="primary" @click="modalApi.open()" class="ml-2"> <NButton type="primary" @click="modalApi.open()" class="ml-2">
打开弹窗 打开弹窗

View File

@ -1,16 +1,18 @@
import type { import type {
ExtendedFormApi,
FormItemDependencies, FormItemDependencies,
FormSchemaRuleType, FormSchemaRuleType,
MaybeComponentProps, MaybeComponentProps,
} from '../types'; } from '../types';
import { computed, ref, watch } from 'vue'; import { computed, isRef, ref, watch } from 'vue';
import { get, isBoolean, isFunction } from '@vben-core/shared/utils'; import { get, isBoolean, isFunction } from '@vben-core/shared/utils';
import { useFormValues } from 'vee-validate'; import { useFormValues } from 'vee-validate';
import { resolveFieldNamePath } from '../field-name'; import { resolveFieldNamePath } from '../field-name';
import { injectFormProps } from '../use-form-context';
import { injectRenderFormProps } from './context'; import { injectRenderFormProps } from './context';
/** /**
@ -37,6 +39,13 @@ export default function useDependencies(
const values = useFormValues(); const values = useFormValues();
const formRenderProps = injectRenderFormProps(); const formRenderProps = injectRenderFormProps();
const [extendApi] = injectFormProps();
// 在 dependencies 里提供访问extendApi的能力
const controller: ExtendedFormApi = isRef(extendApi)
? (extendApi.value.formApi as ExtendedFormApi)
: (extendApi.formApi as ExtendedFormApi);
const formApi = formRenderProps.form; const formApi = formRenderProps.form;
if (!formApi) { if (!formApi) {
@ -92,7 +101,7 @@ export default function useDependencies(
const formValues = values.value; const formValues = values.value;
if (isFunction(whenIf)) { if (isFunction(whenIf)) {
isIf.value = !!(await whenIf(formValues, formApi)); isIf.value = !!(await whenIf(formValues, formApi, controller));
// 不渲染 // 不渲染
if (!isIf.value) return; if (!isIf.value) return;
} else if (isBoolean(whenIf)) { } else if (isBoolean(whenIf)) {
@ -102,31 +111,35 @@ export default function useDependencies(
// 2. 判断show如果show为false则隐藏 // 2. 判断show如果show为false则隐藏
if (isFunction(show)) { if (isFunction(show)) {
isShow.value = !!(await show(formValues, formApi)); isShow.value = !!(await show(formValues, formApi, controller));
} else if (isBoolean(show)) { } else if (isBoolean(show)) {
isShow.value = show; isShow.value = show;
} }
if (isFunction(componentProps)) { if (isFunction(componentProps)) {
dynamicComponentProps.value = await componentProps(formValues, formApi); dynamicComponentProps.value = await componentProps(
formValues,
formApi,
controller,
);
} }
if (isFunction(rules)) { if (isFunction(rules)) {
dynamicRules.value = await rules(formValues, formApi); dynamicRules.value = await rules(formValues, formApi, controller);
} }
if (isFunction(disabled)) { if (isFunction(disabled)) {
isDisabled.value = !!(await disabled(formValues, formApi)); isDisabled.value = !!(await disabled(formValues, formApi, controller));
} else if (isBoolean(disabled)) { } else if (isBoolean(disabled)) {
isDisabled.value = disabled; isDisabled.value = disabled;
} }
if (isFunction(required)) { if (isFunction(required)) {
isRequired.value = !!(await required(formValues, formApi)); isRequired.value = !!(await required(formValues, formApi, controller));
} }
if (isFunction(trigger)) { if (isFunction(trigger)) {
trigger(formValues, formApi); trigger(formValues, formApi, controller);
} }
}, },
{ deep: true, immediate: true }, { deep: true, immediate: true },

View File

@ -3,6 +3,7 @@ export { setupVbenForm } from './config';
export type { export type {
BaseFormComponentType, BaseFormComponentType,
ExtendedFormApi, ExtendedFormApi,
FormLayout,
VbenFormProps, VbenFormProps,
FormSchema as VbenFormSchema, FormSchema as VbenFormSchema,
} from './types'; } from './types';

View File

@ -85,16 +85,19 @@ export type FormSchemaRuleType =
type FormItemDependenciesCondition<T = boolean | PromiseLike<boolean>> = ( type FormItemDependenciesCondition<T = boolean | PromiseLike<boolean>> = (
value: Partial<Record<string, any>>, value: Partial<Record<string, any>>,
actions: FormActions, actions: FormActions,
controller: ExtendedFormApi, // 在 dependencies 里提供访问extendApi的能力
) => T; ) => T;
type FormItemDependenciesConditionWithRules = ( type FormItemDependenciesConditionWithRules = (
value: Partial<Record<string, any>>, value: Partial<Record<string, any>>,
actions: FormActions, actions: FormActions,
controller: ExtendedFormApi, // 在 dependencies 里提供访问extendApi的能力
) => FormSchemaRuleType | PromiseLike<FormSchemaRuleType>; ) => FormSchemaRuleType | PromiseLike<FormSchemaRuleType>;
type FormItemDependenciesConditionWithProps = ( type FormItemDependenciesConditionWithProps = (
value: Partial<Record<string, any>>, value: Partial<Record<string, any>>,
actions: FormActions, actions: FormActions,
controller: ExtendedFormApi, // 在 dependencies 里提供访问extendApi的能力
) => MaybeComponentProps | PromiseLike<MaybeComponentProps>; ) => MaybeComponentProps | PromiseLike<MaybeComponentProps>;
export interface FormItemDependencies { export interface FormItemDependencies {
@ -147,6 +150,7 @@ type ComponentProps =
export interface FormCommonConfig { export interface FormCommonConfig {
/** /**
* *
* @default false
*/ */
collapsible?: boolean; collapsible?: boolean;
/** /**

View File

@ -49,6 +49,27 @@ const FieldComponent = computed(() => {
} }
}); });
const limitDisplay = computed(() => {
if (
props.data.option.min !== null &&
props.data.option.min !== undefined &&
props.data.option.max !== null &&
props.data.option.max !== undefined
) {
return `[${props.data.option.min},${props.data.option.max}]`;
}
if (props.data.option.min !== null && props.data.option.min !== undefined) {
return `min:${props.data.option.min}`;
}
if (props.data.option.max !== null && props.data.option.max !== undefined) {
return `max:${props.data.option.max}`;
}
return '';
});
function reset() { function reset() {
modelValue.value = props.data.defaultValue; modelValue.value = props.data.defaultValue;
} }
@ -78,8 +99,8 @@ defineExpose({
/> />
</div> </div>
<div class="flex items-center flex-none text-muted-foreground pl-2 gap-2"> <div class="flex items-center flex-none text-muted-foreground pl-2 gap-2">
<span v-if="data.option.min && data.option.max"> <span v-if="limitDisplay">
[{{ data.option.min }},{{ data.option.max }}] {{ limitDisplay }}
</span> </span>
<span v-if="data.option.step && data.option.step !== 1"> <span v-if="data.option.step && data.option.step !== 1">
step:{{ data.option.step }} step:{{ data.option.step }}

View File

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Recordable } from '@vben-core/typings';
import type { CollapsibleParamSchema } from './type'; import type { CollapsibleParamSchema } from './type';
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'; import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
@ -29,7 +31,10 @@ const props = withDefaults(defineProps<Props>(), {
const emits = defineEmits<{ 'update:value': [any, string] }>(); const emits = defineEmits<{ 'update:value': [any, string] }>();
const modelValue = defineModel('value'); const modelValue = defineModel('value', {
default: {} as Recordable<CollapsibleParamSchema['defaultValue']>,
});
const visibleRefs = useTemplateRef('visibleRefs'); const visibleRefs = useTemplateRef('visibleRefs');
const collapsibleRefs = useTemplateRef('collapsibleRefs'); const collapsibleRefs = useTemplateRef('collapsibleRefs');
@ -59,11 +64,13 @@ const bodyStyle = computed(() => {
}); });
function init(force = false) { function init(force = false) {
const nextValue = { ...modelValue.value }; const nextValue: Recordable<CollapsibleParamSchema['defaultValue']> = {
...modelValue.value,
};
for (const param of props.params) { for (const param of props.params) {
if (force || nextValue[param.key] === undefined) { if (force || nextValue[param.key] === undefined) {
nextValue[param.key] = param.defaultValue ?? null; nextValue[param.key] = param.defaultValue ?? undefined;
} }
} }
@ -74,25 +81,40 @@ function toggleCollapsed() {
open.value = !open.value; open.value = !open.value;
} }
async function onParamValueChange(value: any, key: string) { async function onParamValueChange(_: any, key: string) {
await nextTick(); await nextTick();
emits('update:value', modelValue.value, key); emits('update:value', modelValue.value, key);
} }
function resetValue() { function resetValues() {
if (visibleRefs.value) if (visibleRefs.value)
for (const rowRef of visibleRefs.value) { for (const rowRef of visibleRefs.value) {
rowRef.reset(); rowRef?.reset();
} }
if (collapsibleRefs.value) if (collapsibleRefs.value)
for (const rowRef of collapsibleRefs.value) { for (const rowRef of collapsibleRefs.value) {
rowRef.reset(); rowRef?.reset();
} }
init(true); init(true);
} }
function updateValues(
values: Recordable<CollapsibleParamSchema['defaultValue']>,
) {
const newValue = {} as Recordable<CollapsibleParamSchema['defaultValue']>;
for (const key in values) {
if (!Object.hasOwn(values, key)) continue;
if (!Object.hasOwn(modelValue.value, key)) continue;
newValue[key] = values[key];
modelValue.value = { ...modelValue.value, ...newValue };
}
}
watch( watch(
() => props.params, () => props.params,
() => init(), () => init(),
@ -101,7 +123,8 @@ watch(
defineExpose({ defineExpose({
toggleCollapsed, toggleCollapsed,
resetValue, resetValues,
updateValues,
}); });
</script> </script>

View File

@ -26,7 +26,7 @@
"file": "file", "file": "file",
"crop-image": "Crop image", "crop-image": "Crop image",
"upload-image": "Click to upload image", "upload-image": "Click to upload image",
"collapsible": "Collapsible FormItem Content" "collapsible": "Collapsible Form Field"
}, },
"vxeTable": { "vxeTable": {
"title": "Vxe Table", "title": "Vxe Table",

View File

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { FormLayout } from '@vben/common-ui';
import type { CollapsibleParamSchema } from '@vben-core/shadcn-ui'; import type { CollapsibleParamSchema } from '@vben-core/shadcn-ui';
import { ref } from 'vue'; import { ref } from 'vue';
@ -13,22 +15,32 @@ import { useVbenForm, z } from '#/adapter/form';
import DocButton from '../doc-button.vue'; import DocButton from '../doc-button.vue';
const layouts = [ const layouts: { label: string; value: FormLayout }[] = [
{ label: 'Vertical', value: 'vertical' }, { label: 'Vertical', value: 'vertical' },
{ label: 'Horizontal', value: 'horizontal' }, { label: 'Horizontal', value: 'horizontal' },
]; ];
const layout = ref(layouts[0].value);
function getNumberValidator(key: string, limit?: [number, number]) { const layout = ref(layouts[0]?.value ?? 'vertical');
function getNumberValidator(key: string, limit?: [number?, number?]) {
let validator = z.number({ let validator = z.number({
required_error: `${key} 值不能为空`, required_error: `${key} 值不能为空`,
invalid_type_error: `${key} 值只能为数字`, invalid_type_error: `${key} 值只能为数字`,
}); });
// validator.default(null);
if (limit) { if (limit) {
validator = validator if (limit[0] !== undefined) {
.min(limit[0], { message: `${key} 值不在区间范围内` }) validator = validator.min(limit[0], {
.max(limit[1], { message: `${key} 值不在区间范围内` }); message: `${key} 值不能小于${limit[0]}`,
});
}
if (limit[1] !== undefined) {
validator = validator.max(limit[1], {
message: `${key} 值不能大于${limit[0]}`,
});
}
} }
return validator.default(null); return validator.default(null);
@ -107,12 +119,13 @@ const paramsValidator = z.object({
learning_rate: getNumberValidator('learning_rate'), learning_rate: getNumberValidator('learning_rate'),
eval_steps: getNumberValidator('eval_steps', [1, 2_147_483_647]), eval_steps: getNumberValidator('eval_steps', [1, 2_147_483_647]),
num_train_epochs: getNumberValidator('num_train_epochs', [1, 200]), num_train_epochs: getNumberValidator('num_train_epochs', [1, 200]),
max_length: getNumberValidator('max_length', [1, 131_072]), max_length: getNumberValidator('max_length', [500, 131_072]),
warmup_ratio: getNumberValidator('warmup_ratio', [0, 1]), warmup_ratio: getNumberValidator('warmup_ratio', [0, 1]),
save_steps: getNumberValidator('save_steps', [1, 2_147_483_647]), save_steps: getNumberValidator('save_steps', [1, 2_147_483_647]),
}); });
const [BaseForm, baseFormApi] = useVbenForm({ const [BaseForm, baseFormApi] = useVbenForm({
showDefaultActions: false,
// //
commonConfig: { commonConfig: {
// label // label
@ -129,6 +142,17 @@ const [BaseForm, baseFormApi] = useVbenForm({
// labelinput // labelinput
layout: 'vertical', layout: 'vertical',
schema: [ schema: [
{
component: 'Switch',
fieldName: 'qat',
componentProps: {
checkedChildren: '开',
unCheckedChildren: '关',
class: 'w-auto',
},
label: 'QAT',
defaultValue: false,
},
{ {
component: VbenCollapsibleParams, component: VbenCollapsibleParams,
componentProps: { componentProps: {
@ -138,20 +162,70 @@ const [BaseForm, baseFormApi] = useVbenForm({
modelPropName: 'value', modelPropName: 'value',
fieldName: 'params', fieldName: 'params',
label: '参数配置', label: '参数配置',
formItemClass: 'col-span-2 items-baseline', formItemClass: 'col-span-8 items-baseline col-start-1',
dependencies: {
triggerFields: ['qat'],
componentProps(values) {
return {
params: values.qat
? [
{
key: 'calib_steps',
description: `校准步数;校准的数据集大小 = 校准步数 * 训练的batch_size`,
option: {
min: 1,
},
},
...paramsSchema,
]
: paramsSchema,
};
},
trigger(values, __, controller) {
const paramsRef =
controller.getFieldComponentRef<typeof VbenCollapsibleParams>(
'params',
);
if (values.qat) {
paramsRef?.updateValues?.({
calib_steps: 10,
micro_batch_size: 32,
learning_rate: 4e-5,
eval_steps: 80,
num_train_epochs: 3,
max_length: 32_768,
warmup_ratio: 0.1,
save_steps: 80,
});
} else {
paramsRef?.updateValues?.({ calib_steps: null });
}
},
rules(values) {
if (values.qat) {
return paramsValidator.extend({
calib_steps: getNumberValidator('calib_steps', [1]),
});
}
return paramsValidator;
},
},
rules: paramsValidator, rules: paramsValidator,
// defaultValue: {
// micro_batch_size: 24,
// },
}, },
{ {
component: 'RichEditor', component: 'RichEditor',
fieldName: 'richEditor', fieldName: 'richEditor',
label: '富文本', label: '富文本',
formItemClass: 'col-span-3 items-baseline', formItemClass: 'col-span-12 items-baseline',
collapsible: true, collapsible: true,
defaultCollapsed: false, // false defaultCollapsed: false, // false
}, },
], ],
// 321 // 321
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', wrapperClass: 'grid-cols-12',
}); });
function onSubmit(values: Record<string, any>) { function onSubmit(values: Record<string, any>) {
@ -160,6 +234,12 @@ function onSubmit(values: Record<string, any>) {
}); });
} }
function onLayoutChange(layout: FormLayout) {
baseFormApi.setState({
layout,
});
}
function handleSetFormValue() { function handleSetFormValue() {
baseFormApi.setFieldValue('params', { baseFormApi.setFieldValue('params', {
micro_batch_size: 8, micro_batch_size: 8,
@ -172,10 +252,16 @@ function handleSetFormValue() {
}); });
} }
function onLayoutChange(layout: string) { function handleResetFormValue() {
baseFormApi.setState({ baseFormApi.resetForm(undefined, { force: true });
layout, }
});
async function handleSubmitFormValue() {
const { valid } = await baseFormApi.validate();
if (valid) {
baseFormApi.submitForm();
}
} }
</script> </script>
@ -201,10 +287,16 @@ function onLayoutChange(layout: string) {
option-type="button" option-type="button"
v-model:value="layout" v-model:value="layout"
@update:value="onLayoutChange" @update:value="onLayoutChange"
> />
<Button type="primary" @click="handleSetFormValue">
设置表单值 设置表单值
</RadioGroup> </Button>
<Button type="primary" @click="handleSetFormValue"></Button> <Button type="primary" @click="handleSubmitFormValue">
提交表单
</Button>
<Button type="primary" @click="handleResetFormValue">
重置表单
</Button>
</div> </div>
</template> </template>
<div class="w-full overflow-hidden"> <div class="w-full overflow-hidden">