فهرست منبع

fix: fix lint and add new form-ui features

feat(form-ui): 在 dependencies 里提供访问extendApi的能力
allen 2 ماه پیش
والد
کامیت
33e2582f60

+ 2 - 153
apps/web-naive/src/views/demos/form/basic.vue

@@ -1,19 +1,7 @@
 <script lang="ts" setup>
-import type { CollapsibleParamSchema } from '@vben-core/shadcn-ui';
+import { Page, useVbenModal } from '@vben/common-ui';
 
-import { ref } from 'vue';
-
-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 { NButton, NCard, useMessage } from 'naive-ui';
 
 import { useVbenForm } from '#/adapter/form';
 import { getAllMenusApi } from '#/api';
@@ -22,111 +10,6 @@ import modalDemo from './modal.vue';
 
 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({
   commonConfig: {
     // 所有表单项
@@ -260,17 +143,6 @@ const [Form, formApi] = useVbenForm({
       },
       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',
     checkbox: ['A', 'C'],
     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({
   connectedComponent: modalDemo,
 });
-
-function onLayoutChange(layout: string) {
-  formApi.setState({
-    layout,
-  });
-}
 </script>
 <template>
   <Page
@@ -311,14 +168,6 @@ function onLayoutChange(layout: string) {
   >
     <NCard title="基础表单" header-extra-class="gap-4">
       <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="modalApi.open()" class="ml-2">
           打开弹窗

+ 21 - 8
packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts

@@ -1,16 +1,18 @@
 import type {
+  ExtendedFormApi,
   FormItemDependencies,
   FormSchemaRuleType,
   MaybeComponentProps,
 } 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 { useFormValues } from 'vee-validate';
 
 import { resolveFieldNamePath } from '../field-name';
+import { injectFormProps } from '../use-form-context';
 import { injectRenderFormProps } from './context';
 
 /**
@@ -37,6 +39,13 @@ export default function useDependencies(
   const values = useFormValues();
 
   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;
 
   if (!formApi) {
@@ -92,7 +101,7 @@ export default function useDependencies(
       const formValues = values.value;
 
       if (isFunction(whenIf)) {
-        isIf.value = !!(await whenIf(formValues, formApi));
+        isIf.value = !!(await whenIf(formValues, formApi, controller));
         // 不渲染
         if (!isIf.value) return;
       } else if (isBoolean(whenIf)) {
@@ -102,31 +111,35 @@ export default function useDependencies(
 
       // 2. 判断show,如果show为false,则隐藏
       if (isFunction(show)) {
-        isShow.value = !!(await show(formValues, formApi));
+        isShow.value = !!(await show(formValues, formApi, controller));
       } else if (isBoolean(show)) {
         isShow.value = show;
       }
 
       if (isFunction(componentProps)) {
-        dynamicComponentProps.value = await componentProps(formValues, formApi);
+        dynamicComponentProps.value = await componentProps(
+          formValues,
+          formApi,
+          controller,
+        );
       }
 
       if (isFunction(rules)) {
-        dynamicRules.value = await rules(formValues, formApi);
+        dynamicRules.value = await rules(formValues, formApi, controller);
       }
 
       if (isFunction(disabled)) {
-        isDisabled.value = !!(await disabled(formValues, formApi));
+        isDisabled.value = !!(await disabled(formValues, formApi, controller));
       } else if (isBoolean(disabled)) {
         isDisabled.value = disabled;
       }
 
       if (isFunction(required)) {
-        isRequired.value = !!(await required(formValues, formApi));
+        isRequired.value = !!(await required(formValues, formApi, controller));
       }
 
       if (isFunction(trigger)) {
-        trigger(formValues, formApi);
+        trigger(formValues, formApi, controller);
       }
     },
     { deep: true, immediate: true },

+ 1 - 0
packages/@core/ui-kit/form-ui/src/index.ts

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

+ 4 - 0
packages/@core/ui-kit/form-ui/src/types.ts

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

+ 23 - 2
packages/@core/ui-kit/shadcn-ui/src/components/collapsible/collapsible-params-item.vue

@@ -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() {
   modelValue.value = props.data.defaultValue;
 }
@@ -78,8 +99,8 @@ defineExpose({
         />
       </div>
       <div class="flex items-center flex-none text-muted-foreground pl-2 gap-2">
-        <span v-if="data.option.min && data.option.max">
-          [{{ data.option.min }},{{ data.option.max }}]
+        <span v-if="limitDisplay">
+          {{ limitDisplay }}
         </span>
         <span v-if="data.option.step && data.option.step !== 1">
           step:{{ data.option.step }}

+ 31 - 8
packages/@core/ui-kit/shadcn-ui/src/components/collapsible/collapsible-params.vue

@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import type { Recordable } from '@vben-core/typings';
+
 import type { CollapsibleParamSchema } from './type';
 
 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 modelValue = defineModel('value');
+const modelValue = defineModel('value', {
+  default: {} as Recordable<CollapsibleParamSchema['defaultValue']>,
+});
+
 const visibleRefs = useTemplateRef('visibleRefs');
 const collapsibleRefs = useTemplateRef('collapsibleRefs');
 
@@ -59,11 +64,13 @@ const bodyStyle = computed(() => {
 });
 
 function init(force = false) {
-  const nextValue = { ...modelValue.value };
+  const nextValue: Recordable<CollapsibleParamSchema['defaultValue']> = {
+    ...modelValue.value,
+  };
 
   for (const param of props.params) {
     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;
 }
 
-async function onParamValueChange(value: any, key: string) {
+async function onParamValueChange(_: any, key: string) {
   await nextTick();
   emits('update:value', modelValue.value, key);
 }
 
-function resetValue() {
+function resetValues() {
   if (visibleRefs.value)
     for (const rowRef of visibleRefs.value) {
-      rowRef.reset();
+      rowRef?.reset();
     }
 
   if (collapsibleRefs.value)
     for (const rowRef of collapsibleRefs.value) {
-      rowRef.reset();
+      rowRef?.reset();
     }
 
   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(
   () => props.params,
   () => init(),
@@ -101,7 +123,8 @@ watch(
 
 defineExpose({
   toggleCollapsed,
-  resetValue,
+  resetValues,
+  updateValues,
 });
 </script>
 

+ 1 - 1
playground/src/locales/langs/en-US/examples.json

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

+ 109 - 17
playground/src/views/examples/form/collapsible.vue

@@ -1,4 +1,6 @@
 <script lang="ts" setup>
+import type { FormLayout } from '@vben/common-ui';
+
 import type { CollapsibleParamSchema } from '@vben-core/shadcn-ui';
 
 import { ref } from 'vue';
@@ -13,22 +15,32 @@ import { useVbenForm, z } from '#/adapter/form';
 
 import DocButton from '../doc-button.vue';
 
-const layouts = [
+const layouts: { label: string; value: FormLayout }[] = [
   { label: 'Vertical', value: 'vertical' },
   { 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({
     required_error: `${key} 值不能为空`,
     invalid_type_error: `${key} 值只能为数字`,
   });
 
+  // validator.default(null);
+
   if (limit) {
-    validator = validator
-      .min(limit[0], { message: `${key} 值不在区间范围内` })
-      .max(limit[1], { message: `${key} 值不在区间范围内` });
+    if (limit[0] !== undefined) {
+      validator = validator.min(limit[0], {
+        message: `${key} 值不能小于${limit[0]}`,
+      });
+    }
+    if (limit[1] !== undefined) {
+      validator = validator.max(limit[1], {
+        message: `${key} 值不能大于${limit[0]}`,
+      });
+    }
   }
 
   return validator.default(null);
@@ -107,12 +119,13 @@ const paramsValidator = z.object({
   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]),
+  max_length: getNumberValidator('max_length', [500, 131_072]),
   warmup_ratio: getNumberValidator('warmup_ratio', [0, 1]),
   save_steps: getNumberValidator('save_steps', [1, 2_147_483_647]),
 });
 
 const [BaseForm, baseFormApi] = useVbenForm({
+  showDefaultActions: false,
   // 所有表单项共用,可单独在表单内覆盖
   commonConfig: {
     // 在label后显示一个冒号
@@ -129,6 +142,17 @@ const [BaseForm, baseFormApi] = useVbenForm({
   // 水平布局,label和input在同一行
   layout: 'vertical',
   schema: [
+    {
+      component: 'Switch',
+      fieldName: 'qat',
+      componentProps: {
+        checkedChildren: '开',
+        unCheckedChildren: '关',
+        class: 'w-auto',
+      },
+      label: 'QAT',
+      defaultValue: false,
+    },
     {
       component: VbenCollapsibleParams,
       componentProps: {
@@ -138,20 +162,70 @@ const [BaseForm, baseFormApi] = useVbenForm({
       modelPropName: 'value',
       fieldName: 'params',
       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,
+      // defaultValue: {
+      //   micro_batch_size: 24,
+      // },
     },
     {
       component: 'RichEditor',
       fieldName: 'richEditor',
       label: '富文本',
-      formItemClass: 'col-span-3 items-baseline',
+      formItemClass: 'col-span-12 items-baseline',
       collapsible: true,
       defaultCollapsed: false, // 默认false
     },
   ],
   // 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
-  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
+  wrapperClass: 'grid-cols-12',
 });
 
 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() {
   baseFormApi.setFieldValue('params', {
     micro_batch_size: 8,
@@ -172,10 +252,16 @@ function handleSetFormValue() {
   });
 }
 
-function onLayoutChange(layout: string) {
-  baseFormApi.setState({
-    layout,
-  });
+function handleResetFormValue() {
+  baseFormApi.resetForm(undefined, { force: true });
+}
+
+async function handleSubmitFormValue() {
+  const { valid } = await baseFormApi.validate();
+
+  if (valid) {
+    baseFormApi.submitForm();
+  }
 }
 </script>
 
@@ -201,10 +287,16 @@ function onLayoutChange(layout: string) {
             option-type="button"
             v-model:value="layout"
             @update:value="onLayoutChange"
-          >
+          />
+          <Button type="primary" @click="handleSetFormValue">
             设置表单值
-          </RadioGroup>
-          <Button type="primary" @click="handleSetFormValue">设置表单值</Button>
+          </Button>
+          <Button type="primary" @click="handleSubmitFormValue">
+            提交表单
+          </Button>
+          <Button type="primary" @click="handleResetFormValue">
+            重置表单
+          </Button>
         </div>
       </template>
       <div class="w-full overflow-hidden">