Prechádzať zdrojové kódy

chore: (form) 组件升级

gemercheung 3 rokov pred
rodič
commit
32d2f2933f

+ 69 - 69
package.json

@@ -30,120 +30,120 @@
   },
   "dependencies": {
     "@ant-design/colors": "^6.0.0",
-    "@ant-design/icons-vue": "^6.0.1",
-    "@iconify/iconify": "^2.1.0",
+    "@ant-design/icons-vue": "^6.1.0",
+    "@iconify/iconify": "^2.2.1",
     "@logicflow/core": "^0.7.16",
     "@logicflow/extension": "^0.7.16",
-    "@vue/runtime-core": "^3.2.26",
-    "@vue/shared": "^3.2.26",
-    "@vueuse/core": "^7.4.1",
-    "@vueuse/shared": "^7.4.1",
+    "@vue/runtime-core": "^3.2.39",
+    "@vue/shared": "^3.2.39",
+    "@vueuse/core": "^7.7.1",
+    "@vueuse/shared": "^7.7.1",
     "@zxcvbn-ts/core": "^1.2.0",
-    "ant-design-vue": "^3.2.0",
+    "ant-design-vue": "^3.2.12",
     "axios": "^0.24.0",
-    "codemirror": "^5.65.0",
+    "codemirror": "^5.65.8",
     "cropperjs": "^1.5.12",
     "crypto-js": "^4.1.1",
-    "dayjs": "^1.10.7",
-    "echarts": "^5.2.2",
+    "dayjs": "^1.11.5",
+    "echarts": "^5.3.3",
     "intro.js": "^4.3.0",
     "js-base64": "^3.7.2",
     "lodash-es": "^4.17.21",
     "mockjs": "^1.1.0",
-    "moment": "^2.29.1",
+    "moment": "^2.29.4",
     "nprogress": "^0.2.0",
-    "path-to-regexp": "^6.2.0",
+    "path-to-regexp": "^6.2.1",
     "pinia": "2.0.9",
     "print-js": "^1.6.0",
-    "qrcode": "^1.5.0",
-    "qs": "^6.10.2",
+    "qrcode": "^1.5.1",
+    "qs": "^6.11.0",
     "resize-observer-polyfill": "^1.5.1",
     "showdown": "^1.9.1",
-    "sortablejs": "^1.14.0",
-    "tinymce": "^5.10.2",
-    "vditor": "^3.8.10",
-    "vue": "^3.2.26",
-    "vue-i18n": "^9.1.9",
-    "vue-json-pretty": "^1.8.2",
-    "vue-router": "^4.0.12",
-    "vue-types": "^4.1.1",
-    "xlsx": "^0.17.4"
+    "sortablejs": "^1.15.0",
+    "tinymce": "^5.10.5",
+    "vditor": "^3.8.17",
+    "vue": "^3.2.39",
+    "vue-i18n": "^9.2.2",
+    "vue-json-pretty": "^1.9.2",
+    "vue-router": "^4.1.5",
+    "vue-types": "^4.2.1",
+    "xlsx": "^0.17.5"
   },
   "devDependencies": {
-    "@commitlint/cli": "^16.0.1",
-    "@commitlint/config-conventional": "^16.0.0",
-    "@iconify/json": "^2.0.16",
+    "@commitlint/cli": "^16.3.0",
+    "@commitlint/config-conventional": "^16.2.4",
+    "@iconify/json": "^2.1.104",
     "@purge-icons/generated": "^0.7.0",
     "@types/codemirror": "^5.60.5",
-    "@types/crypto-js": "^4.0.2",
+    "@types/crypto-js": "^4.1.1",
     "@types/fs-extra": "^9.0.13",
-    "@types/inquirer": "^8.1.3",
+    "@types/inquirer": "^8.2.3",
     "@types/intro.js": "^3.0.2",
-    "@types/jest": "^27.0.3",
-    "@types/lodash-es": "^4.17.5",
-    "@types/mockjs": "^1.0.4",
-    "@types/node": "^17.0.5",
+    "@types/jest": "^27.5.2",
+    "@types/lodash-es": "^4.17.6",
+    "@types/mockjs": "^1.0.6",
+    "@types/node": "^17.0.45",
     "@types/nprogress": "^0.2.0",
-    "@types/qrcode": "^1.4.2",
+    "@types/qrcode": "^1.5.0",
     "@types/qs": "^6.9.7",
     "@types/showdown": "^1.9.4",
-    "@types/sortablejs": "^1.10.7",
-    "@typescript-eslint/eslint-plugin": "^5.8.1",
-    "@typescript-eslint/parser": "^5.8.1",
-    "@vitejs/plugin-legacy": "^1.6.4",
-    "@vitejs/plugin-vue": "^2.0.1",
-    "@vitejs/plugin-vue-jsx": "^1.3.3",
+    "@types/sortablejs": "^1.13.0",
+    "@typescript-eslint/eslint-plugin": "^5.36.2",
+    "@typescript-eslint/parser": "^5.36.2",
+    "@vitejs/plugin-legacy": "^1.8.2",
+    "@vitejs/plugin-vue": "^2.3.4",
+    "@vitejs/plugin-vue-jsx": "^1.3.10",
     "@vue/compiler-sfc": "3.2.26",
-    "@vue/test-utils": "^2.0.0-rc.18",
-    "autoprefixer": "^10.4.0",
-    "commitizen": "^4.2.4",
+    "@vue/test-utils": "^2.0.2",
+    "autoprefixer": "^10.4.8",
+    "commitizen": "^4.2.5",
     "conventional-changelog-cli": "^2.2.2",
     "cross-env": "^7.0.3",
     "dotenv": "^10.0.0",
-    "eslint": "^8.5.0",
-    "eslint-config-prettier": "^8.3.0",
-    "eslint-define-config": "^1.2.1",
-    "eslint-plugin-jest": "^25.3.2",
-    "eslint-plugin-prettier": "^4.0.0",
-    "eslint-plugin-vue": "^8.2.0",
+    "eslint": "^8.23.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-define-config": "^1.7.0",
+    "eslint-plugin-jest": "^25.7.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-vue": "^8.7.1",
     "esno": "^0.13.0",
-    "fs-extra": "^10.0.0",
+    "fs-extra": "^10.1.0",
     "husky": "^7.0.4",
-    "inquirer": "^8.2.0",
-    "jest": "^27.4.5",
-    "less": "^4.1.2",
+    "inquirer": "^8.2.4",
+    "jest": "^27.5.1",
+    "less": "^4.1.3",
     "lint-staged": "12.1.4",
     "npm-run-all": "^4.1.5",
-    "postcss": "^8.4.5",
-    "postcss-html": "^1.3.0",
+    "postcss": "^8.4.16",
+    "postcss-html": "^1.5.0",
     "postcss-less": "^5.0.0",
-    "prettier": "^2.5.1",
+    "prettier": "^2.7.1",
     "rimraf": "^3.0.2",
-    "rollup-plugin-visualizer": "^5.5.2",
-    "stylelint": "^14.2.0",
-    "stylelint-config-html": "^1.0.0",
+    "rollup-plugin-visualizer": "^5.8.1",
+    "stylelint": "^14.11.0",
+    "stylelint-config-html": "^1.1.0",
     "stylelint-config-prettier": "^9.0.3",
     "stylelint-config-recommended": "^6.0.0",
     "stylelint-config-standard": "^24.0.0",
     "stylelint-order": "^5.0.0",
-    "ts-jest": "^27.1.2",
-    "ts-node": "^10.4.0",
-    "typescript": "^4.5.4",
-    "vite": "^2.8.5",
+    "ts-jest": "^27.1.5",
+    "ts-node": "^10.9.1",
+    "typescript": "^4.8.2",
+    "vite": "^2.9.15",
     "vite-plugin-compression": "^0.4.0",
     "vite-plugin-html": "^2.1.2",
-    "vite-plugin-imagemin": "^0.5.1",
+    "vite-plugin-imagemin": "^0.5.3",
     "vite-plugin-mock": "^2.9.6",
     "vite-plugin-purge-icons": "^0.7.0",
-    "vite-plugin-pwa": "^0.11.12",
+    "vite-plugin-pwa": "^0.11.13",
     "vite-plugin-rewrite-all": "^0.1.2",
     "vite-plugin-style-import": "^1.4.1",
-    "vite-plugin-svg-icons": "^1.0.5",
-    "vite-plugin-theme": "^0.8.1",
+    "vite-plugin-svg-icons": "^1.1.0",
+    "vite-plugin-theme": "^0.8.6",
     "vite-plugin-vue-setup-extend": "^0.3.0",
-    "vite-plugin-windicss": "^1.6.1",
-    "vue-eslint-parser": "^8.0.1",
-    "vue-tsc": "^0.30.1"
+    "vite-plugin-windicss": "^1.8.7",
+    "vue-eslint-parser": "^8.3.0",
+    "vue-tsc": "^0.30.6"
   },
   "resolutions": {
     "bin-wrapper": "npm:bin-wrapper-china",

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 3354 - 3253
pnpm-lock.yaml


+ 2 - 0
src/components/Form/index.ts

@@ -9,7 +9,9 @@ export { useForm } from './src/hooks/useForm';
 export { default as ApiSelect } from './src/components/ApiSelect.vue';
 export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
 export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
+export { default as ApiTree } from './src/components/ApiTree.vue';
 export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
 export { default as ApiCascader } from './src/components/ApiCascader.vue';
+export { default as ApiTransfer } from './src/components/ApiTransfer.vue';
 
 export { BasicForm };

+ 11 - 10
src/components/Form/src/BasicForm.vue

@@ -62,12 +62,13 @@
 
   import { basicProps } from './props';
   import { useDesign } from '/@/hooks/web/useDesign';
+  import { cloneDeep } from 'lodash-es';
 
   export default defineComponent({
     name: 'BasicForm',
     components: { FormItem, Form, Row, FormAction },
     props: basicProps,
-    emits: ['advanced-change', 'reset', 'submit', 'register'],
+    emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
     setup(props, { emit, attrs }) {
       const formModel = reactive<Recordable>({});
       const modalFn = useModalContext();
@@ -132,9 +133,11 @@
           }
         }
         if (unref(getProps).showAdvancedButton) {
-          return schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[];
+          return cloneDeep(
+            schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[],
+          );
         } else {
-          return schemas as FormSchema[];
+          return cloneDeep(schemas as FormSchema[]);
         }
       });
 
@@ -240,13 +243,11 @@
 
       function setFormModel(key: string, value: any) {
         formModel[key] = value;
-        // const { validateTrigger } = unref(getBindValue);
-        // console.log('key', key, schemas[3]);
-
-        // console.log('validateTrigger', validateTrigger, formModel);
-        // if (!validateTrigger || validateTrigger === 'change') {
-        //   validateFields([key]).catch((_) => {});
-        // }
+        const { validateTrigger } = unref(getBindValue);
+        if (!validateTrigger || validateTrigger === 'change') {
+          validateFields([key]).catch((_) => {});
+        }
+        emit('field-value-change', key, value);
       }
 
       function handleEnterPress(e: KeyboardEvent) {

+ 4 - 0
src/components/Form/src/componentMap.ts

@@ -24,8 +24,10 @@ import {
 import ApiRadioGroup from './components/ApiRadioGroup.vue';
 import RadioButtonGroup from './components/RadioButtonGroup.vue';
 import ApiSelect from './components/ApiSelect.vue';
+import ApiTree from './components/ApiTree.vue';
 import ApiTreeSelect from './components/ApiTreeSelect.vue';
 import ApiCascader from './components/ApiCascader.vue';
+import ApiTransfer from './components/ApiTransfer.vue';
 import { BasicUpload } from '/@/components/Upload';
 import { StrengthMeter } from '/@/components/StrengthMeter';
 import { IconPicker } from '/@/components/Icon';
@@ -43,6 +45,7 @@ componentMap.set('AutoComplete', AutoComplete);
 
 componentMap.set('Select', Select);
 componentMap.set('ApiSelect', ApiSelect);
+componentMap.set('ApiTree', ApiTree);
 componentMap.set('TreeSelect', TreeSelect);
 componentMap.set('ApiTreeSelect', ApiTreeSelect);
 componentMap.set('ApiRadioGroup', ApiRadioGroup);
@@ -55,6 +58,7 @@ componentMap.set('ApiCascader', ApiCascader);
 componentMap.set('Cascader', Cascader);
 componentMap.set('Slider', Slider);
 componentMap.set('Rate', Rate);
+componentMap.set('ApiTransfer', ApiTransfer);
 
 componentMap.set('DatePicker', DatePicker);
 componentMap.set('MonthPicker', DatePicker.MonthPicker);

+ 3 - 2
src/components/Form/src/components/ApiCascader.vue

@@ -26,7 +26,7 @@
   import { get, omit } from 'lodash-es';
   import { useRuleFormItem } from '/@/hooks/component/useFormItem';
   import { LoadingOutlined } from '@ant-design/icons-vue';
-
+  import { useI18n } from '/@/hooks/web/useI18n';
   interface Option {
     value: string;
     label: string;
@@ -76,7 +76,7 @@
       const loading = ref<boolean>(false);
       const emitData = ref<any[]>([]);
       const isFirstLoad = ref(true);
-
+      const { t } = useI18n();
       // Embedded in the form, just use the hook binding to perform form verification
       const [state] = useRuleFormItem(props, 'value', 'change', emitData);
 
@@ -188,6 +188,7 @@
         state,
         options,
         loading,
+        t,
         handleChange,
         loadData,
         handleRenderDisplay,

+ 4 - 11
src/components/Form/src/components/ApiSelect.vue

@@ -2,9 +2,7 @@
   <Select
     @dropdown-visible-change="handleFetch"
     v-bind="$attrs"
-    show-search
     @change="handleChange"
-    :filter-option="filterOption"
     :options="getOptions"
     v-model:value="state"
   >
@@ -69,18 +67,13 @@
       const emitData = ref<any[]>([]);
       const attrs = useAttrs();
       const { t } = useI18n();
-      const filterOption = (input, option) => {
-        try {
-          return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
-        } catch (error) {
-          return false;
-        }
-      };
+
       // Embedded in the form, just use the hook binding to perform form verification
       const [state] = useRuleFormItem(props, 'value', 'change', emitData);
 
       const getOptions = computed(() => {
         const { labelField, valueField, numberToString } = props;
+
         return unref(options).reduce((prev, next: Recordable) => {
           if (next) {
             const value = next[valueField];
@@ -123,7 +116,7 @@
           }
           emitChange();
         } catch (error) {
-          console.warn('error', error);
+          console.warn(error);
         } finally {
           loading.value = false;
         }
@@ -148,7 +141,7 @@
         emitData.value = args;
       }
 
-      return { state, attrs, getOptions, loading, t, handleFetch, handleChange, filterOption };
+      return { state, attrs, getOptions, loading, t, handleFetch, handleChange };
     },
   });
 </script>

+ 135 - 0
src/components/Form/src/components/ApiTransfer.vue

@@ -0,0 +1,135 @@
+<template>
+  <Transfer
+    :data-source="getdataSource"
+    show-search
+    :filter-option="filterOption"
+    :render="(item) => item.title"
+    :showSelectAll="showSelectAll"
+    :selectedKeys="selectedKeys"
+    :targetKeys="getTargetKeys"
+    :showSearch="showSearch"
+    @change="handleChange"
+  />
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, watch, ref, unref, watchEffect } from 'vue';
+  import { Transfer } from 'ant-design-vue';
+  import { isFunction } from '/@/utils/is';
+  import { get, omit } from 'lodash-es';
+  import { propTypes } from '/@/utils/propTypes';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer';
+  export default defineComponent({
+    name: 'ApiTransfer',
+    components: { Transfer },
+    props: {
+      value: { type: Array<string> },
+      api: {
+        type: Function as PropType<(arg?: Recordable) => Promise<TransferItem[]>>,
+        default: null,
+      },
+      params: { type: Object },
+      dataSource: { type: Array<TransferItem> },
+      immediate: propTypes.bool.def(true),
+      alwaysLoad: propTypes.bool.def(false),
+      afterFetch: { type: Function as PropType<Fn> },
+      resultField: propTypes.string.def(''),
+      labelField: propTypes.string.def('title'),
+      valueField: propTypes.string.def('key'),
+      showSearch: { type: Boolean, default: false },
+      disabled: { type: Boolean, default: false },
+      filterOption: {
+        type: Function as PropType<(inputValue: string, item: TransferItem) => boolean>,
+      },
+      selectedKeys: { type: Array<string> },
+      showSelectAll: { type: Boolean, default: false },
+      targetKeys: { type: Array<string> },
+    },
+    emits: ['options-change', 'change'],
+    setup(props, { attrs, emit }) {
+      const _dataSource = ref<TransferItem[]>([]);
+      const _targetKeys = ref<string[]>([]);
+      const { t } = useI18n();
+
+      const getAttrs = computed(() => {
+        return {
+          ...(!props.api ? { dataSource: unref(_dataSource) } : {}),
+          ...attrs,
+        };
+      });
+      const getdataSource = computed(() => {
+        const { labelField, valueField } = props;
+
+        return unref(_dataSource).reduce((prev, next: Recordable) => {
+          if (next) {
+            prev.push({
+              ...omit(next, [labelField, valueField]),
+              title: next[labelField],
+              key: next[valueField],
+            });
+          }
+          return prev;
+        }, [] as TransferItem[]);
+      });
+      const getTargetKeys = computed<string[]>(() => {
+        if (unref(_targetKeys).length > 0) {
+          return unref(_targetKeys);
+        }
+        if (Array.isArray(props.value)) {
+          return props.value;
+        }
+        return [];
+      });
+
+      function handleChange(keys: string[], direction: TransferDirection, moveKeys: string[]) {
+        _targetKeys.value = keys;
+        console.log(direction);
+        console.log(moveKeys);
+        emit('change', keys);
+      }
+
+      watchEffect(() => {
+        props.immediate && !props.alwaysLoad && fetch();
+      });
+
+      watch(
+        () => props.params,
+        () => {
+          fetch();
+        },
+        { deep: true },
+      );
+
+      async function fetch() {
+        const api = props.api;
+        if (!api || !isFunction(api)) {
+          if (Array.isArray(props.dataSource)) {
+            _dataSource.value = props.dataSource;
+          }
+          return;
+        }
+        _dataSource.value = [];
+        try {
+          const res = await api(props.params);
+          if (Array.isArray(res)) {
+            _dataSource.value = res;
+            emitChange();
+            return;
+          }
+          if (props.resultField) {
+            _dataSource.value = get(res, props.resultField) || [];
+          }
+          emitChange();
+        } catch (error) {
+          console.warn(error);
+        } finally {
+        }
+      }
+      function emitChange() {
+        emit('options-change', unref(getdataSource));
+      }
+      return { getTargetKeys, getdataSource, t, getAttrs, handleChange };
+    },
+  });
+</script>

+ 90 - 0
src/components/Form/src/components/ApiTree.vue

@@ -0,0 +1,90 @@
+<template>
+  <a-tree v-bind="getAttrs" @change="handleChange">
+    <template #[item]="data" v-for="item in Object.keys($slots)">
+      <slot :name="item" v-bind="data || {}"></slot>
+    </template>
+    <template #suffixIcon v-if="loading">
+      <LoadingOutlined spin />
+    </template>
+  </a-tree>
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
+  import { Tree } from 'ant-design-vue';
+  import { isArray, isFunction } from '/@/utils/is';
+  import { get } from 'lodash-es';
+  import { propTypes } from '/@/utils/propTypes';
+  import { LoadingOutlined } from '@ant-design/icons-vue';
+  export default defineComponent({
+    name: 'ApiTree',
+    components: { ATree: Tree, LoadingOutlined },
+    props: {
+      api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
+      params: { type: Object },
+      immediate: { type: Boolean, default: true },
+      resultField: propTypes.string.def(''),
+      afterFetch: { type: Function as PropType<Fn> },
+    },
+    emits: ['options-change', 'change'],
+    setup(props, { attrs, emit }) {
+      const treeData = ref<Recordable[]>([]);
+      const isFirstLoaded = ref<Boolean>(false);
+      const loading = ref(false);
+      const getAttrs = computed(() => {
+        return {
+          ...(props.api ? { treeData: unref(treeData) } : {}),
+          ...attrs,
+        };
+      });
+
+      function handleChange(...args) {
+        emit('change', ...args);
+      }
+
+      watch(
+        () => props.params,
+        () => {
+          !unref(isFirstLoaded) && fetch();
+        },
+        { deep: true },
+      );
+
+      watch(
+        () => props.immediate,
+        (v) => {
+          v && !isFirstLoaded.value && fetch();
+        },
+      );
+
+      onMounted(() => {
+        props.immediate && fetch();
+      });
+
+      async function fetch() {
+        const { api, afterFetch } = props;
+        if (!api || !isFunction(api)) return;
+        loading.value = true;
+        treeData.value = [];
+        let result;
+        try {
+          result = await api(props.params);
+        } catch (e) {
+          console.error(e);
+        }
+        if (afterFetch && isFunction(afterFetch)) {
+          result = afterFetch(result);
+        }
+        loading.value = false;
+        if (!result) return;
+        if (!isArray(result)) {
+          result = get(result, props.resultField);
+        }
+        treeData.value = (result as Recordable[]) || [];
+        isFirstLoaded.value = true;
+        emit('options-change', treeData.value);
+      }
+      return { getAttrs, loading, handleChange };
+    },
+  });
+</script>

+ 19 - 7
src/components/Form/src/components/FormItem.vue

@@ -1,17 +1,16 @@
 <script lang="tsx">
   import type { PropType, Ref } from 'vue';
-  import type { FormActionType, FormProps } from '../types/form';
-  import type { FormSchema } from '../types/form';
+  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 { defineComponent, computed, unref, toRefs } from 'vue';
-  import { Form, Col, Divider } from 'ant-design-vue';
+  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 { createPlaceholderMessage, setComponentRuleType } from '../helper';
-  import { upperFirst, cloneDeep } from 'lodash-es';
+  import { cloneDeep, upperFirst } from 'lodash-es';
   import { useItemLabelWidth } from '../hooks/useLabelWidth';
   import { useI18n } from '/@/hooks/web/useI18n';
 
@@ -178,8 +177,21 @@
 
         const getRequired = isFunction(required) ? required(unref(getValues)) : required;
 
-        if ((!rules || rules.length === 0) && getRequired) {
-          rules = [{ required: getRequired, validator }];
+        /*
+         * 1、若设置了required属性,又没有其他的rules,就创建一个验证规则;
+         * 2、若设置了required属性,又存在其他的rules,则只rules中不存在required属性时,才添加验证required的规则
+         *     也就是说rules中的required,优先级大于required
+         */
+        if (getRequired) {
+          if (!rules || rules.length === 0) {
+            rules = [{ required: getRequired, validator }];
+          } else {
+            const requiredIndex: number = rules.findIndex((rule) => Reflect.has(rule, 'required'));
+
+            if (requiredIndex === -1) {
+              rules.push({ required: getRequired, validator });
+            }
+          }
         }
 
         const requiredRuleIndex: number = rules.findIndex(

+ 6 - 1
src/components/Form/src/hooks/useAdvanced.ts

@@ -1,6 +1,6 @@
 import type { ColEx } from '../types';
 import type { AdvanceState } from '../types/hooks';
-import type { ComputedRef, Ref } from 'vue';
+import { ComputedRef, getCurrentInstance, Ref } from 'vue';
 import type { FormProps, FormSchema } from '../types/form';
 import { computed, unref, watch } from 'vue';
 import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is';
@@ -26,6 +26,8 @@ export default function ({
   formModel,
   defaultValueRef,
 }: UseAdvancedContext) {
+  const vm = getCurrentInstance();
+
   const { realWidthRef, screenEnum, screenRef } = useBreakpoint();
 
   const getEmptySpan = computed((): number => {
@@ -150,6 +152,9 @@ export default function ({
       }
     }
 
+    // 确保页面发送更新
+    vm?.proxy?.$forceUpdate();
+
     advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan);
 
     getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true);

+ 52 - 2
src/components/Form/src/hooks/useFormEvents.ts

@@ -2,7 +2,7 @@ import type { ComputedRef, Ref } from 'vue';
 import type { FormProps, FormSchema, FormActionType } from '../types/form';
 import type { NamePath } from 'ant-design-vue/lib/form/interface';
 import { unref, toRaw, nextTick } from 'vue';
-import { isArray, isFunction, isObject, isString } from '/@/utils/is';
+import { isArray, isFunction, isObject, isString, isDef, isNullOrUnDef } from '/@/utils/is';
 import { deepMerge } from '/@/utils';
 import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper';
 import { dateUtil } from '/@/utils/dateUtil';
@@ -39,7 +39,8 @@ export function useFormEvents({
     Object.keys(formModel).forEach((key) => {
       const schema = unref(getSchema).find((item) => item.field === key);
       const isInput = schema?.component && defaultValueComponents.includes(schema.component);
-      formModel[key] = isInput ? defaultValueRef.value[key] || '' : defaultValueRef.value[key];
+      const defaultValue = cloneDeep(defaultValueRef.value[key]);
+      formModel[key] = isInput ? defaultValue || '' : defaultValue;
     });
     nextTick(() => clearValidate());
 
@@ -55,6 +56,10 @@ export function useFormEvents({
       .map((item) => item.field)
       .filter(Boolean);
 
+    // key 支持 a.b.c 的嵌套写法
+    const delimiter = '.';
+    const nestKeyArray = fields.filter((item) => item.indexOf(delimiter) >= 0);
+
     const validKeys: string[] = [];
     Object.keys(values).forEach((key) => {
       const schema = unref(getSchema).find((item) => item.field === key);
@@ -85,6 +90,21 @@ export function useFormEvents({
           formModel[key] = value;
         }
         validKeys.push(key);
+      } else {
+        nestKeyArray.forEach((nestKey: string) => {
+          try {
+            const value = eval('values' + delimiter + nestKey);
+            if (isDef(value)) {
+              formModel[nestKey] = value;
+              validKeys.push(nestKey);
+            }
+          } catch (e) {
+            // key not exist
+            if (isDef(defaultValueRef.value[nestKey])) {
+              formModel[nestKey] = cloneDeep(defaultValueRef.value[nestKey]);
+            }
+          }
+        });
       }
     });
     validateFields(validKeys).catch((_) => {});
@@ -132,11 +152,14 @@ export function useFormEvents({
     if (!prefixField || index === -1 || first) {
       first ? schemaList.unshift(schema) : schemaList.push(schema);
       schemaRef.value = schemaList;
+      _setDefaultValue(schema);
       return;
     }
     if (index !== -1) {
       schemaList.splice(index + 1, 0, schema);
     }
+    _setDefaultValue(schema);
+
     schemaRef.value = schemaList;
   }
 
@@ -192,9 +215,36 @@ export function useFormEvents({
         }
       });
     });
+    _setDefaultValue(schema);
+
     schemaRef.value = uniqBy(schema, 'field');
   }
 
+  function _setDefaultValue(data: FormSchema | FormSchema[]) {
+    let schemas: FormSchema[] = [];
+    if (isObject(data)) {
+      schemas.push(data as FormSchema);
+    }
+    if (isArray(data)) {
+      schemas = [...data];
+    }
+
+    const obj: Recordable = {};
+    const currentFieldsValue = getFieldsValue();
+    schemas.forEach((item) => {
+      if (
+        item.component != 'Divider' &&
+        Reflect.has(item, 'field') &&
+        item.field &&
+        !isNullOrUnDef(item.defaultValue) &&
+        !(item.field in currentFieldsValue)
+      ) {
+        obj[item.field] = item.defaultValue;
+      }
+    });
+    setFieldsValue(obj);
+  }
+
   function getFieldsValue(): Recordable {
     const formEl = unref(formElRef);
     if (!formEl) return {};

+ 47 - 4
src/components/Form/src/hooks/useFormValues.ts

@@ -3,7 +3,7 @@ import { dateUtil } from '/@/utils/dateUtil';
 import { unref } from 'vue';
 import type { Ref, ComputedRef } from 'vue';
 import type { FormProps, FormSchema } from '../types/form';
-import { set } from 'lodash-es';
+import { cloneDeep, set } from 'lodash-es';
 
 interface UseFormValuesContext {
   defaultValueRef: Ref<any>;
@@ -11,6 +11,43 @@ interface UseFormValuesContext {
   getProps: ComputedRef<FormProps>;
   formModel: Recordable;
 }
+
+/**
+ * @desription deconstruct array-link key. This method will mutate the target.
+ */
+function tryDeconstructArray(key: string, value: any, target: Recordable) {
+  const pattern = /^\[(.+)\]$/;
+  if (pattern.test(key)) {
+    const match = key.match(pattern);
+    if (match && match[1]) {
+      const keys = match[1].split(',');
+      value = Array.isArray(value) ? value : [value];
+      keys.forEach((k, index) => {
+        set(target, k.trim(), value[index]);
+      });
+      return true;
+    }
+  }
+}
+
+/**
+ * @desription deconstruct object-link key. This method will mutate the target.
+ */
+function tryDeconstructObject(key: string, value: any, target: Recordable) {
+  const pattern = /^\{(.+)\}$/;
+  if (pattern.test(key)) {
+    const match = key.match(pattern);
+    if (match && match[1]) {
+      const keys = match[1].split(',');
+      value = isObject(value) ? value : {};
+      keys.forEach((k) => {
+        set(target, k.trim(), value[k.trim()]);
+      });
+      return true;
+    }
+  }
+}
+
 export function useFormValues({
   defaultValueRef,
   getSchema,
@@ -41,7 +78,10 @@ export function useFormValues({
       if (isString(value)) {
         value = value.trim();
       }
-      set(res, key, value);
+      if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) {
+        // 没有解构成功的,按原样赋值
+        set(res, key, value);
+      }
     }
     return handleRangeTimeValue(res);
   }
@@ -78,10 +118,13 @@ export function useFormValues({
       const { defaultValue } = item;
       if (!isNullOrUnDef(defaultValue)) {
         obj[item.field] = defaultValue;
-        formModel[item.field] = defaultValue;
+
+        if (formModel[item.field] === undefined) {
+          formModel[item.field] = defaultValue;
+        }
       }
     });
-    defaultValueRef.value = obj;
+    defaultValueRef.value = cloneDeep(obj);
   }
 
   return { handleFormValues, initDefault };

+ 2 - 1
src/components/Form/src/types/form.ts

@@ -10,7 +10,7 @@ import type { RowProps } from 'ant-design-vue/lib/grid/Row';
 export type FieldMapToTime = [string, [string, string], string?][];
 
 export type Rule = RuleObject & {
-  trigger?: 'blur' | 'change' | 'blur' | ['change', 'blur', 'blur'];
+  trigger?: 'blur' | 'change' | ['change', 'blur'];
 };
 
 export interface RenderCallbackParams {
@@ -49,6 +49,7 @@ export type RegisterFn = (formInstance: FormActionType) => void;
 export type UseFormReturnType = [RegisterFn, FormActionType];
 
 export interface FormProps {
+  name?: string;
   layout?: 'vertical' | 'inline' | 'horizontal';
   // Form value
   model?: Recordable;

+ 3 - 2
src/components/Form/src/types/index.ts

@@ -91,6 +91,7 @@ export type ComponentType =
   | 'Select'
   | 'ApiSelect'
   | 'TreeSelect'
+  | 'ApiTree'
   | 'ApiTreeSelect'
   | 'ApiRadioGroup'
   | 'RadioButtonGroup'
@@ -112,5 +113,5 @@ export type ComponentType =
   | 'Render'
   | 'Slider'
   | 'Rate'
-  | 'Time'
-  | 'Divider';
+  | 'Divider'
+  | 'ApiTransfer';

+ 9 - 9
tests/server/package.json

@@ -10,7 +10,7 @@
     "stop": "npx pm2 stop ecosystem.config.js"
   },
   "dependencies": {
-    "fs-extra": "^10.0.0",
+    "fs-extra": "^10.1.0",
     "koa": "^2.13.4",
     "koa-body": "^4.2.0",
     "koa-bodyparser": "^4.3.0",
@@ -21,16 +21,16 @@
     "koa2-cors": "^2.0.6"
   },
   "devDependencies": {
-    "@types/koa": "^2.13.4",
+    "@types/koa": "^2.13.5",
     "@types/koa-bodyparser": "^5.0.2",
     "@types/koa-router": "^7.4.4",
-    "@types/node": "^16.11.6",
-    "nodemon": "^2.0.14",
-    "pm2": "^5.1.2",
+    "@types/node": "^16.11.58",
+    "nodemon": "^2.0.19",
+    "pm2": "^5.2.0",
     "rimraf": "^3.0.2",
-    "ts-node": "^10.4.0",
-    "tsconfig-paths": "^3.11.0",
-    "tsup": "^5.5.0",
-    "typescript": "^4.4.4"
+    "ts-node": "^10.9.1",
+    "tsconfig-paths": "^3.14.1",
+    "tsup": "^5.12.9",
+    "typescript": "^4.8.2"
   }
 }

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2695 - 0
tests/server/pnpm-lock.yaml