Ben Lin
2024-10-20 d6749f95c526c0e71ec946bd3bb777bc42b2c34a
src/components/Form/src/components/FormItem.vue
@@ -2,22 +2,29 @@
  import { type Recordable, type Nullable } from '@vben/types';
  import type { PropType, Ref } from 'vue';
  import { computed, defineComponent, toRefs, unref } from 'vue';
  import type { FormActionType, FormProps, FormSchema } from '../types/form';
  import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
  import type { TableActionType } from '/@/components/Table';
  import {
    isComponentFormSchema,
    type FormActionType,
    type FormProps,
    type FormSchemaInner as FormSchema,
  } from '../types/form';
  import type { Rule as ValidationRule } from 'ant-design-vue/lib/form/interface';
  import type { TableActionType } from '@/components/Table';
  import { Col, Divider, Form } from 'ant-design-vue';
  import { componentMap } from '../componentMap';
  import { BasicHelp } from '/@/components/Basic';
  import { isBoolean, isFunction, isNull } from '/@/utils/is';
  import { getSlot } from '/@/utils/helper/tsxHelper';
  import { BasicHelp, BasicTitle } from '@/components/Basic';
  import { isBoolean, isFunction, isNull } from '@/utils/is';
  import { getSlot } from '@/utils/helper/tsxHelper';
  import {
    createPlaceholderMessage,
    isIncludeSimpleComponents,
    NO_AUTO_LINK_COMPONENTS,
    setComponentRuleType,
  } from '../helper';
  import { cloneDeep, upperFirst } from 'lodash-es';
  import { useItemLabelWidth } from '../hooks/useLabelWidth';
  import { useI18n } from '/@/hooks/web/useI18n';
  import { useI18n } from '@/hooks/web/useI18n';
  export default defineComponent({
    name: 'BasicFormItem',
@@ -61,6 +68,12 @@
        formProps: Ref<FormProps>;
      };
      // 组件 CropperAvatar 的 size 属性类型为 number
      // 此处补充一个兼容
      if (schema.value.component === 'CropperAvatar' && typeof formProps.value.size === 'string') {
        formProps.value.size = undefined;
      }
      const itemLabelWidthProp = useItemLabelWidth(schema, formProps);
      const getValues = computed(() => {
@@ -84,7 +97,7 @@
        if (isFunction(componentProps)) {
          componentProps = componentProps({ schema, tableAction, formModel, formActionType }) ?? {};
        }
        if (schema.component === 'Divider') {
        if (isIncludeSimpleComponents(schema.component)) {
          componentProps = Object.assign(
            { type: 'horizontal' },
            {
@@ -109,6 +122,21 @@
          disabled = dynamicDisabled(unref(getValues));
        }
        return disabled;
      });
      const getReadonly = computed(() => {
        const { readonly: globReadonly } = props.formProps;
        const { dynamicReadonly } = props.schema;
        const { readonly: itemReadonly = false } = unref(getComponentsProps);
        let readonly = globReadonly || itemReadonly;
        if (isBoolean(dynamicReadonly)) {
          readonly = dynamicReadonly;
        }
        if (isFunction(dynamicReadonly)) {
          readonly = dynamicReadonly(unref(getValues));
        }
        return readonly;
      });
      function getShow(): { isShow: boolean; isIfShow: boolean } {
@@ -138,7 +166,6 @@
        isShow = isShow && itemIsAdvanced;
        return { isShow, isIfShow };
      }
      function handleRules(): ValidationRule[] {
        const {
          rules: defRules = [],
@@ -148,7 +175,6 @@
          dynamicRules,
          required,
        } = props.schema;
        if (isFunction(dynamicRules)) {
          return dynamicRules(unref(getValues)) as ValidationRule[];
        }
@@ -159,7 +185,10 @@
        const joinLabel = Reflect.has(props.schema, 'rulesMessageJoinLabel')
          ? rulesMessageJoinLabel
          : globalRulesMessageJoinLabel;
        const defaultMsg = createPlaceholderMessage(component) + `${joinLabel ? label : ''}`;
        const assertLabel = joinLabel ? (isFunction(label) ? '' : label) : '';
        const defaultMsg = component
          ? createPlaceholderMessage(component) + assertLabel
          : assertLabel;
        function validator(rule: any, value: any) {
          const msg = rule.message || defaultMsg;
@@ -186,7 +215,6 @@
          }
          return Promise.resolve();
        }
        const getRequired = isFunction(required) ? required(unref(getValues)) : required;
        /*
@@ -196,7 +224,10 @@
         */
        if (getRequired) {
          if (!rules || rules.length === 0) {
            rules = [{ required: getRequired, validator }];
            const trigger = NO_AUTO_LINK_COMPONENTS.includes(component || 'Input')
              ? 'blur'
              : 'change';
            rules = [{ required: getRequired, validator, trigger }];
          } else {
            const requiredIndex: number = rules.findIndex((rule) => Reflect.has(rule, 'required'));
@@ -217,10 +248,6 @@
            rule.required = false;
          }
          if (component) {
            if (!Reflect.has(rule, 'type')) {
              rule.type = component === 'InputNumber' ? 'number' : 'string';
            }
            rule.message = rule.message || defaultMsg;
            if (component.includes('Input') || component.includes('Textarea')) {
@@ -242,12 +269,16 @@
      }
      function renderComponent() {
        if (!isComponentFormSchema(props.schema)) {
          return null;
        }
        const {
          renderComponentContent,
          component,
          field,
          changeEvent = 'change',
          valueField,
          valueFormat,
        } = props.schema;
        const isCheck = component && ['Switch', 'Checkbox'].includes(component);
@@ -257,12 +288,17 @@
        const on = {
          [eventKey]: (...args: Nullable<Recordable<any>>[]) => {
            const [e] = args;
            const target = e ? e.target : null;
            let value = target ? (isCheck ? target.checked : target.value) : e;
            if(isFunction(valueFormat)){
              value = valueFormat({...unref(getValues),value});
            }
            props.setFormModel(field, value, props.schema);
            if (propsData[eventKey]) {
              propsData[eventKey](...args);
            }
            const target = e ? e.target : null;
            const value = target ? (isCheck ? target.checked : target.value) : e;
            props.setFormModel(field, value, props.schema);
          },
        };
        const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>;
@@ -270,10 +306,10 @@
        const { autoSetPlaceHolder, size } = props.formProps;
        const propsData: Recordable<any> = {
          allowClear: true,
          getPopupContainer: (trigger: Element) => trigger.parentNode,
          size,
          ...unref(getComponentsProps),
          disabled: unref(getDisable),
          readonly: unref(getReadonly),
        };
        const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
@@ -299,7 +335,12 @@
          return <Comp {...compAttr} />;
        }
        const compSlot = isFunction(renderComponentContent)
          ? { ...renderComponentContent(unref(getValues)) }
          ? {
              ...renderComponentContent(unref(getValues), {
                disabled: unref(getDisable),
                readonly: unref(getReadonly),
              }),
            }
          : {
              default: () => renderComponentContent,
            };
@@ -308,12 +349,13 @@
      function renderLabelHelpMessage() {
        const { label, helpMessage, helpComponentProps, subLabel } = props.schema;
        const getLabel = isFunction(label) ? label(unref(getValues)) : label;
        const renderLabel = subLabel ? (
          <span>
            {label} <span class="text-secondary">{subLabel}</span>
            {getLabel} <span class="text-secondary">{subLabel}</span>
          </span>
        ) : (
          label
          getLabel
        );
        const getHelpMessage = isFunction(helpMessage)
          ? helpMessage(unref(getValues))
@@ -330,30 +372,46 @@
      }
      function renderItem() {
        const { itemProps, slot, render, field, suffix, component } = props.schema;
        const { itemProps, slot, render, field, suffix, component, prefix } = props.schema;
        const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
        const { colon } = props.formProps;
        const opts = { disabled: unref(getDisable), readonly: unref(getReadonly) };
        if (component === 'Divider') {
          return (
            <Col span={24}>
              <Divider {...unref(getComponentsProps)}>{renderLabelHelpMessage()}</Divider>
            </Col>
          );
        } else if (component === 'BasicTitle') {
          return (
            <Form.Item
              labelCol={labelCol}
              wrapperCol={wrapperCol}
              name={field}
              class={{
                'suffix-item': !!suffix,
                'prefix-item': !!prefix,
              }}
            >
              <BasicTitle {...unref(getComponentsProps)}>{renderLabelHelpMessage()}</BasicTitle>
            </Form.Item>
          );
        } else {
          const getContent = () => {
            return slot
              ? getSlot(slots, slot, unref(getValues))
              ? getSlot(slots, slot, unref(getValues), opts)
              : render
              ? render(unref(getValues))
              : renderComponent();
                ? render(unref(getValues), opts)
                : renderComponent();
          };
          const showSuffix = !!suffix;
          const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix;
          const showPrefix = !!prefix;
          const getPrefix = isFunction(prefix) ? prefix(unref(getValues)) : prefix;
          // TODO 自定义组件验证会出现问题,因此这里框架默认将自定义组件设置手动触发验证,如果其他组件还有此问题请手动设置autoLink=false
          if (NO_AUTO_LINK_COMPONENTS.includes(component)) {
          if (component && NO_AUTO_LINK_COMPONENTS.includes(component)) {
            props.schema &&
              (props.schema.itemProps! = {
                autoLink: false,
@@ -365,7 +423,10 @@
            <Form.Item
              name={field}
              colon={colon}
              class={{ 'suffix-item': showSuffix }}
              class={{
                'suffix-item': showSuffix,
                'prefix-item': showPrefix,
              }}
              {...(itemProps as Recordable<any>)}
              label={renderLabelHelpMessage()}
              rules={handleRules()}
@@ -373,6 +434,7 @@
              wrapperCol={wrapperCol}
            >
              <div style="display:flex">
                {showPrefix && <span class="prefix">{getPrefix}</span>}
                <div style="flex:1;">{getContent()}</div>
                {showSuffix && <span class="suffix">{getSuffix}</span>}
              </div>
@@ -382,8 +444,8 @@
      }
      return () => {
        const { colProps = {}, colSlot, renderColContent, component } = props.schema;
        if (!componentMap.has(component)) {
        const { colProps = {}, colSlot, renderColContent, component, slot } = props.schema;
        if (!((component && componentMap.has(component)) || slot)) {
          return null;
        }
@@ -391,13 +453,14 @@
        const realColProps = { ...baseColProps, ...colProps };
        const { isIfShow, isShow } = getShow();
        const values = unref(getValues);
        const opts = { disabled: unref(getDisable), readonly: unref(getReadonly) };
        const getContent = () => {
          return colSlot
            ? getSlot(slots, colSlot, values)
            ? getSlot(slots, colSlot, values, opts)
            : renderColContent
            ? renderColContent(values)
            : renderItem();
              ? renderColContent(values, opts)
              : renderItem();
        };
        return (