Bläddra i källkod

表单组件库修改

快乐的梦鱼 2 månader sedan
förälder
incheckning
5413c7c151

+ 17 - 16
package-lock.json

@@ -31,7 +31,8 @@
         "pinia": "^3.0.1",
         "tslib": "^2.8.1",
         "vue": "3.5.22",
-        "vue-i18n": "9.14.5"
+        "vue-i18n": "9.14.5",
+        "vue-router": "^4.6.3"
       },
       "devDependencies": {
         "@dcloudio/types": "3.4.25",
@@ -2356,21 +2357,6 @@
         }
       }
     },
-    "node_modules/@dcloudio/uni-h5/node_modules/vue-router": {
-      "version": "4.6.3",
-      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.3.tgz",
-      "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/devtools-api": "^6.6.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/posva"
-      },
-      "peerDependencies": {
-        "vue": "^3.5.0"
-      }
-    },
     "node_modules/@dcloudio/uni-i18n": {
       "version": "3.0.0-4070620250821001",
       "resolved": "https://registry.npmmirror.com/@dcloudio/uni-i18n/-/uni-i18n-3.0.0-4070620250821001.tgz",
@@ -11673,6 +11659,21 @@
         "url": "https://github.com/sponsors/kazupon"
       }
     },
+    "node_modules/vue-router": {
+      "version": "4.6.3",
+      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.3.tgz",
+      "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
     "node_modules/vue-template-compiler": {
       "version": "2.7.16",
       "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",

+ 2 - 1
package.json

@@ -58,7 +58,8 @@
     "pinia": "^3.0.1",
     "tslib": "^2.8.1",
     "vue": "3.5.22",
-    "vue-i18n": "9.14.5"
+    "vue-i18n": "9.14.5",
+    "vue-router": "^4.6.3"
   },
   "devDependencies": {
     "@dcloudio/types": "3.4.25",

+ 4 - 2
src/common/components/dynamicf/ComponentConfigs.ts

@@ -1,5 +1,7 @@
 import MapApi from "@/api/map/MapApi";
-import type { FormItemComponentAdditionalDefine } from "@/components/dynamic";
+import type { IDynamicFormComponentAdditionalDefine } from "@/components/dynamic";
+
+//添加自定义组件默认配置
 
 export default [
   {
@@ -10,4 +12,4 @@ export default [
       loadDistrictInfo: (latlon: [number, number]) => MapApi.regeo(latlon[0], latlon[1]),
     },
   },
-] as FormItemComponentAdditionalDefine[];
+] as IDynamicFormComponentAdditionalDefine[];

+ 61 - 0
src/common/components/dynamicf/ComponentRender.vue

@@ -0,0 +1,61 @@
+<template>
+  <!-- 在下方添加自定义组件 -->
+  <!-- 业务代码开始 -->
+  <template v-if="formDefineItem.type === 'richtext'">
+    <RichTextEditor
+      ref="itemRef"
+      :modelValue="modelValue"
+      @update:modelValue="onValueChanged"
+      v-bind="params"
+    />
+  </template>
+  <template v-else-if="formDefineItem.type === 'recorder'">
+    <Recorder
+      ref="itemRef"
+      :modelValue="modelValue"
+      @update:modelValue="onValueChanged"
+      v-bind="params"
+    />
+  </template>
+  <!-- 业务代码结束 -->
+  <template v-else>
+    <text>Fallback: unknow form type {{ formDefineItem.type }}</text>
+  </template>
+</template>
+
+<script setup lang="ts">
+import { ref, type PropType } from 'vue';
+import type { IDynamicFormItem } from '@/components/dynamic';
+import RichTextEditor from '@/common/components/form/RichTextEditor.vue';
+import Recorder from '@/common/components/form/Recorder.vue';
+
+const props = defineProps({	
+  modelValue: {
+    type: null
+  },
+  formDefineItem: {
+    type: Object as PropType<IDynamicFormItem>,
+    default: () => ({})
+  },
+  isLast: {
+    type: Boolean,
+    default: false,
+  },
+  params: {
+    type: Object,
+    default: () => ({})
+  },
+});
+const emit = defineEmits(['update:modelValue']);
+
+function onValueChanged(v: any) {
+  emit('update:modelValue', v);
+}
+
+const itemRef = ref();
+
+defineExpose({
+  getItemRef: () => itemRef.value,
+})
+
+</script>

+ 316 - 0
src/components/dynamic/DynamicForm.ts

@@ -0,0 +1,316 @@
+import type { Rule, RuleItem, Rules } from "async-validator";
+import type { FormInstance, FormProps } from "../form/Form.vue";
+import type { FieldProps } from "../form/Field.vue";
+import type { ColProps } from "../layout/grid/Col.vue";
+import type { RowProps } from "../layout/grid/Row.vue";
+
+export interface IDynamicFormOptions {
+  /**
+   * 表单额外属性
+   */
+  formAdditionaProps?: FormProps;  
+  /**
+   * 表单校验规则
+   */
+  formRules?: Rules;
+  /**
+   * 子条目定义
+   */
+  formItems: IDynamicFormItem[];
+  /**
+   * 是否在根级别抑制错误提示
+   */
+  suppressRootError?: boolean;
+  /**
+   * 空文本
+   */
+  emptyText?: string;
+  /**
+   * 是否禁用表单
+   */
+  disabled?: boolean;
+  /**
+   * 是否屏蔽所有子条目空错误。默认否
+   * @default false
+   */
+  suppressEmptyError?: boolean,
+  /**
+   * 当显示嵌套的表单对象条目时是否在前部显示缩进,
+   * 缩进大小可使用 CSS 变量 `--dynamic-form-item-nest-margin` 自定义。默认是 20px
+   * @default true
+   */
+  nestObjectMargin?: boolean,
+}
+
+/**
+ * 表单动态属性定义
+ */
+export declare type IDynamicFormItemCallback<T> = {
+  /**
+   * 预留,暂未使用
+   */
+  type?: string;
+  /**
+   * @param model 当前表单条目的值
+   * @param rawModel 整个 form 的值 (最常用,当两个关联组件距离较远时,可以从顶层的 rawModel 里获取)
+   * @param parentModel 父表单元素的值 (上一级的值,只在列表场景的使用,例如列表某个元素的父级就是整个 item)
+   * @param item 当前表单条目信息
+   */
+  callback: (model: any, rawModel: any, parentModel: any, params: { 
+    /**
+     * 当前表单条目
+     */
+    item: IDynamicFormItem, 
+    /**
+     * 当前表单条目的父级,为空时表示当前条目为根级别。
+     */
+    parent?: IDynamicFormItem,
+    /**
+     * 当前表单组件实例
+     */
+    form: IDynamicFormRef,
+    /**
+     * 全局参数。由表单组件顶层添加额外的参数。
+     */
+    formGlobalParams: IDynamicFormObject, 
+    /**
+     * 当前条目校验数据
+     */
+    formRules?: Record<string, Rule>,
+    /**
+     * 是否是所在层级的第一个表单项
+     */
+    isFirst: boolean,
+    /**
+     * 是否是所在层级的最后一个表单项
+     */
+    isLast: boolean,
+  }) => T;
+};
+export type IDynamicFormItemCallbackAdditionalProps<T> = { [P in keyof T]?: T[P]|IDynamicFormItemCallback<T[P]> }
+
+export type DynamicFormNestNameGenerateType = 'dot'|'array'
+
+/**
+ * 表单事件中心消息名称
+ */
+/**
+ * 重新加载表单项目。由条目自由处理。
+ */
+export const MESSAGE_RELOAD = 'reload';
+
+export type IDynamicFormObject = Record<string, any>
+
+/**
+ * 表单动态组件属性定义
+ */
+export interface IDynamicFormComponentAdditionalDefine {
+  /**
+   * 组件名称
+   */
+  name: string,
+  /**
+   * 是否需要显示右侧箭头
+   */
+  needArrow?: boolean,
+  /**
+   * 传递给表单条目的参数
+   */
+  itemProps?: FieldProps,
+  /**
+   * 传递给组件的参数
+   */
+  props?: Record<string, unknown>|unknown,
+}
+
+export interface IDynamicFormItem {
+  /**
+   * 表单项显示标签
+   */
+  label?: string|IDynamicFormItemCallback<string>;
+  /**
+   * 属性名称
+   */
+  name: string;
+  /**
+   * 表单项组件类型
+   */
+  type?: string;
+  /**
+   * 传递给条目组件的参数。(允许动态回调)
+   */
+  additionalProps?: Record<string, unknown|IDynamicFormItemCallback<unknown>>|unknown;
+  /**
+   * 传递给FormItem组件的参数
+   */
+  formProps?: FieldProps;
+  /**
+   * 默认值,用于默认数据生成
+   */
+  defaultValue?: any;
+  /**
+   * 是否屏蔽空错误。默认否
+   * @default false
+   */
+  suppressEmptyError?: boolean,
+  /**
+   * 当前条目的校验规则
+   */
+  rules?: RuleItem[],
+  /**
+   * 子条目,在对象中为对象子属性,在数组中为数组条目(单条目按单项控制,多条目按对象看待控制)
+   */
+  children?: IDynamicFormItem[],
+
+  /**
+   * 是否显示。当为undefined时,默认显示。
+   */
+  show?: boolean|IDynamicFormItemCallback<boolean>|undefined,
+  /**
+   * 是否禁用当前表单项
+   */
+  disabled?: boolean|IDynamicFormItemCallback<boolean>;
+
+  /**
+   * 当前条目组件加载时发生事件
+   * @param nowModel 当前表单条目的值
+   * @param rawModel 顶层数据对象
+   * @param ref 组件实例
+   * @returns 
+   */
+  mounted?: (nowModel: any, rawModel: any, ref: any) => void;
+  /**
+   * 当前条目组件卸载时发生事件
+   * @param nowModel 当前表单条目的值
+   * @param rawModel 顶层数据对象
+   * @param ref 组件实例
+   * @returns 
+   */
+  beforeUnmount?: (nowModel: any, rawModel: any, ref: any) => void;
+  /**
+   * 当前条目数据更改时发生事件
+   * @param oldValue 旧值
+   * @param newValue 新值
+   * @param topModel 顶层数据对象
+   * @param ref 组件实例
+   * @returns 
+   */
+  watch?: (oldValue: any, newValue: any, rawModel: any, ref: any) => void;
+  /**
+   * 监听从外部或者其他表单发送过来的消息事件
+   * @param messageName 消息名称
+   * @param data 消息数据
+   * @param getComponentRef 当前组件的实例
+   * @returns 
+   */
+  message?: (messageName: string, data: unknown, getComponentRef: () => unknown) => void,
+  /**
+   * 当子对象为数组时,可设置这个自定义回调。用于添加按钮新建一个对象,如果这个函数为空,则没有添加按钮。
+   */
+  newChildrenObject?: (arrayNow: unknown[]) => unknown;
+  /**
+   * 当子对象为数组时,可设置这个自定义回调。删除按钮回调,可选,不提供时默认操作为将 item 从 array 中移除。
+   */
+  deleteChildrenCallback?: (arrayNow: unknown[], deleteObject: unknown) => unknown;
+  /**
+   * 子条目的 Col 配置属性(应用到当前条目的所有子条目上)。仅在 object 或者其他容器条目中有效。
+   */
+  childrenColProps?: ColProps,
+  /**
+   * 当前条目的 Col 配置属性(应用到当前条目上)。仅在 object 或者其他容器条目中有效。
+   */
+  colProps?: ColProps,
+  /**
+   * 当前条目的 Row 配置属性(应用到当前条目上)。仅在 object 或者其他容器条目中有效。
+   */
+  rowProps?: RowProps,
+  /**
+   * 当显示嵌套的表单对象条目时是否在前部显示缩进。默认是
+   */
+  nestObjectMargin?: boolean,
+
+  
+  //内部使用,不建议外部调用
+  fullName?: string;
+}
+export interface IDynamicFormRef {
+  /**
+   * 获取表单组件的 Ref
+   * @returns 
+   */
+  getFormRef: () => FormInstance;
+  /**
+   * 获取指定表单项组件的 Ref
+   * @returns 
+   */
+  getFormItemControlRef: <T>(key: string) => T;
+  /**
+   * 触发提交。同 getFormRef().submit() 。
+   * @returns 
+   */
+  submit: () => void;
+  /**
+   * 验证当前表单数据是否有效。同 getFormRef().validate() 。
+   * @returns 
+   */
+  validate: () => Promise<void>;
+  /**
+   * 外部修改指定单个 field 的数据
+   * @param path 路径
+   * @param value 值
+   * @returns 
+   */
+  setValueByPath: (path: string|string[], value: unknown) => void,
+  /**
+   * 外部获取指定单个 field 的数据
+   * @param path 路径
+   * @returns 
+   */
+  getValueByPath: (path: string|string[]) => unknown,
+  /**
+   * 向所有或者指定的子组件分发消息事件。
+   * @param messageName 消息名称。
+   * @param data 可选参数。
+   * @param receiveFilter 可选名称筛选正则,此正则通过名称的子组件会接受事件,其他则不会。
+   * @returns 
+   */
+  dispatchMessage: (messageName: string, data?: unknown, receiveFilter?: RegExp) => void;
+  /**
+   * 向所有子组件分发重新加载消息事件。
+   * @returns 
+   */
+  dispatchReload: () => void;
+  /**
+   * 获取当前表单中可见的所有字段名
+   */
+  getVisibleFormNames: () => string[];
+  /**
+   * 初始化表单默认值到模型中,对于已有数据非空的字段,不会覆盖已有的值。
+   * @returns 
+   */
+  initDefaultValuesToModel: () => void;
+}
+
+/**
+ * 默认的动态表单属性
+ */
+export let defaultDynamicFormOptions = {} as IDynamicFormOptions;
+
+/**
+ * 配置默认的动态表单属性,配置后将会对所有动态表单生效。
+ * @param options 参数
+ */
+export function configDefaultDynamicFormOptions(options: Omit<IDynamicFormOptions, 'formItems'>) {
+  defaultDynamicFormOptions = {
+    ...defaultDynamicFormOptions,
+    ...options,
+  };
+}
+
+export type IEvaluateCallback = <T>(val: T | IDynamicFormItemCallback<T>) => T;
+export type IDynamicFormMessageCenterCallback = (messageName: string, data: unknown) => void;
+
+export interface IDynamicFormMessageCenter {
+  addInstance: (name: string, fn: IDynamicFormMessageCenterCallback) => void,
+  removeInstance: (name: string) => void,
+}

+ 157 - 129
src/components/dynamic/DynamicForm.vue

@@ -1,166 +1,194 @@
 <template>
   <Form
-    ref="formRef"
-    v-bind="formProps"
-    :model="formModel"
-    :rules="formRules"
+    ref="formEditor"
+    :name="name"
+    v-bind="finalOptions.formAdditionaProps"
+    :model="model"
+    :rules="finalOptions.formRules"
+    @submit="(e) => emit('submit', e)"
+    @submitFailed="() => emit('finishFailed')"
   >
-    <DynamicFormCate
-      v-if="formModel"
-      :formModel="formModel"
-      :formDefine="formDefine"
-      :formDefineParentKey="''"
-      :formDefineParentLabel="''"
+    <DynamicFormRoot 
+      :options="finalOptions"
+      :model="model.value"
+      :name="name"
     />
   </Form>
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, provide, reactive, ref, toRef, watch, type PropType } from 'vue';
-import { waitTimeOut } from '@imengyu/imengyu-utils';
-import DynamicFormCate from './DynamicFormCate.vue';
-import Form, { type FormInstance, type FormProps } from '../form/Form.vue';
-import type { FormDefine, FormDefineItem, FormExport } from '.';
-import type { Rules } from 'async-validator';
+import { computed, onMounted, provide, ref, toRef, toRefs, type PropType } from 'vue';
+import Form, { type FormInstance } from '../form/Form.vue';
+import { 
+  type IDynamicFormOptions, type IDynamicFormItem, type IDynamicFormRef, 
+  type IDynamicFormObject, defaultDynamicFormOptions, 
+  type IDynamicFormMessageCenter,
+  type IDynamicFormMessageCenterCallback,
+  MESSAGE_RELOAD
+} from '.';
+import DynamicFormRoot from './nest/DynamicFormRoot.vue';
 
 const props = defineProps({	
-  formDefine: {
-    type: Object as PropType<FormDefine>,
-    default: () => ({})
+  /**
+   * 动态表单选项
+   */
+  options: {
+    type: Object as PropType<IDynamicFormOptions>,
+    default: null
   },
-  formProps: {
-    type: Object as PropType<Omit<FormProps, 'model' | 'rules'>>,
-    default: () => ({}) 
-  },
-  formGlobalParams: {
+  /**
+   * 表单数据模型
+   */
+  model: {
     type: Object,
-    default: () => ({})
+    default: null
+  },
+  /**
+   * 表单名称, 设置到表单组件上。
+   */
+  name: {
+    type: String,
+    default: ''
+  },
+  /**
+   * 全局参数。用于向每个表单项的参数中添加额外的参数,可以在回调中的 formGlobalParams 中访问。
+   */
+  globalParams: {
+    type: Object as PropType<IDynamicFormObject>,
+    default: null
   },
 });
+const emit = defineEmits(['ready', 'submit', 'finish', 'finishFailed']);
 
-const formRef = ref<FormInstance>();
-const formModel = ref<any>(null);
-const formRules = computed(() => {
-  const rules: Rules = {};
-  function loop(prevKey: string, arr: FormDefineItem[]) {
-    if (!arr || !(arr instanceof Array))
-     return;
-    for (const item of arr) {
-      const key = prevKey ? `${prevKey}.${item.name}` : item.name;
-      if (key && item.rules) 
-        rules[key] = item.rules;
-      if (item.children) {
-        loop(
-          item.children.propNestType === 'flat' ? key : prevKey, 
-          item.children.items
-        );
-      }
-    }
-  }
-  loop('', props.formDefine.items);
-  return rules;
-});
-const formGlobalParams = toRef(props.formGlobalParams);
-
-provide('formTopModel', formModel);
-provide('formGlobalParams', formGlobalParams);
+const { options, model, name } = toRefs(props);
+const finalOptions = computed<IDynamicFormOptions>(() => ({
+  ...defaultDynamicFormOptions,
+  ...options.value,
+}));
 
-watch(() => props.formDefine, (v) => {
-  reloadFormData();
-});
+provide('rawModel', model);
+provide('globalParams', toRef(props, 'globalParams'));
+provide('finalOptions', finalOptions);
 
-let isErrorState = false;
-let initCb : () => any = () => {
-  return {};
-};
+const formEditor = ref<FormInstance>();
+const widgetsRefMap = ref<Record<string,() => unknown>>({});
+const messageCenterMap = new Map<string, IDynamicFormMessageCenterCallback>();
+  
+provide('widgetsRefMap', widgetsRefMap.value);
+provide('messageCenter', {
+  addInstance: (name: string, fn: IDynamicFormMessageCenterCallback) => messageCenterMap.set(name, fn),
+  removeInstance: (name: string) => messageCenterMap.delete(name),
+} as IDynamicFormMessageCenter);
 
-function initFormData(data: () => any) {
-  initCb = data;
+//获取组件引用
+function getFormItemControlRef(key: string) {
+  return widgetsRefMap.value[key]?.();
 }
-function loadFormData(value?: Record<string, any>) {
-  const obj = reactive(initCb());
-
-  function loop(prevKey: string, arr: FormDefineItem[]) {
-    if (!arr || !(arr instanceof Array))
-     return;
-    for (let index = 0; index < arr.length; index++) {
-      const item = arr[index];
-      const key = prevKey ? `${prevKey}.${item.name}` : item.name;
-      if (key) {
-        const valueProvided = value?.[key] ;
-        obj[key] = valueProvided == null || valueProvided == undefined ? 
-          (typeof item.defaultValue === 'function' ? item.defaultValue() : item.defaultValue)  
-          : valueProvided ?? null;
-        item.fullName = key;
-      } else {
-        item.fullName = '';
+
+//通过路径访问
+function accessFormModel(keyName: string, isSet: boolean, setValue: unknown) : unknown {
+  const keys = keyName.split('.');
+  let ret : unknown = undefined;
+  let obj = model.value as Record<string, unknown>;
+  let keyIndex = 0;
+  let key = keys[keyIndex];
+  while (obj) {
+    const leftIndex = key.indexOf('[');
+    if (leftIndex > 0 && key.endsWith(']')) {
+      const arr = obj[key.substring(0, leftIndex)] as Record<string, unknown>[];
+      const index = parseInt(key.substring(leftIndex + 1, key.length - 1))    
+      obj = arr[index];
+      if (keyIndex >= keys.length - 1) {
+        ret = obj;
+        if (isSet) arr[index] = setValue as Record<string, unknown>;
       }
-      if (item.children)
-        loop(
-          item.children.propNestType === 'flat' ? key : prevKey, 
-          item.children.items
-        );
+    } else {
+      const newObj = obj[key] as Record<string, unknown>;
+      if (keyIndex >= keys.length - 1) {
+        ret = newObj;
+        if (isSet)
+          obj[key] = setValue as Record<string, unknown>;
+      }
+      obj = newObj;
     }
+    if (keyIndex < keys.length - 1)
+      key = keys[++keyIndex];
+    else
+      break;
   }
+  return ret;
+} 
 
-  loop('', props.formDefine.items);
-  formModel.value = obj;
+//发送通知消息
+function dispatchMessage(messageName: string, data?: unknown, receiveFilter?: RegExp) {
+  for (const iterator of messageCenterMap) {
+    if (!receiveFilter || receiveFilter.test(iterator[0]))
+      iterator[1](messageName, data);
+  }
 }
-async function submitForm<T = Record<string, any>>() : Promise<T|null> {
-  if (!formRef.value)
-    throw new Error('表单实例不存在');
 
-  await formRef.value.clearValidate();
-  await waitTimeOut(50);
-  
-  try {
-    await formRef.value.validate();
-  } catch (e) {
-    if (isErrorState)
-      uni.showToast({
-        title: '表单中有未填写项,请检查',
-        icon: 'none'
-      });
-    console.log(e);
-    isErrorState = true;
-    return null;
-  }
-  isErrorState = false;
-  return formModel.value;
+//发送重新加载消息
+function dispatchReload() {
+  dispatchMessage(MESSAGE_RELOAD);
 }
-function resetForm() {
-  loadFormData();
+
+//初始化默认值到模型
+function initDefaultValuesToModel() {
+  function loopItems(key: string, items: IDynamicFormItem[]) {
+    for (const item of items) {
+      const currentKey = (key ? key + '.' : '') + item.name;
+      if (item.children)
+        loopItems(currentKey, item.children);
+      if (item.defaultValue !== undefined) {
+        const oldValue = accessFormModel(currentKey, false, undefined);
+        if (oldValue !== undefined && oldValue !== null)
+          continue;
+        accessFormModel(currentKey, true, item.defaultValue);
+      }
+    }
+  }
+  loopItems('', finalOptions.value.formItems);
 }
 
-function reloadFormData() {
-  if (!formModel.value)
-    loadFormData();
+//获取当前表单中可见的所有字段名
+function getVisibleFormNames() {
+  return Array.from(messageCenterMap.keys());
 }
 
 onMounted(() => {
-  setTimeout(() => reloadFormData(), 300);
+  setTimeout(() => {
+    emit('ready');
+  }, 400);
 });
 
-defineExpose<FormExport>({
-  getFormModel: () => formModel.value,
-  getFormRef: () => {
-    if (!formRef.value)
-      throw new Error('表单实例不存在');
-    return formRef.value
+const formRef : IDynamicFormRef = {
+  initDefaultValuesToModel,
+  getVisibleFormNames,
+  getFormRef() {
+    if (!formEditor.value)
+      throw new Error('Form instance is not create.');
+    return formEditor.value
+  },
+  getFormItemControlRef: getFormItemControlRef as any,
+  submit() { return this.getFormRef().validate(); },
+  validate() { return this.getFormRef().validate(); },
+  setValueByPath: (path: string|string[], value: unknown) => {
+    if (Array.isArray(path))
+      path = path.join('.');
+    return accessFormModel(path, true, value);
   },
-  getFormData: () => formModel.value,
-  getFormItemRef: (path: string) => {
-    if (!formRef.value)
-      throw new Error('表单实例不存在');
-    const filed = formRef.value.getField(path);
-    if (!filed)
-      throw new Error(`表单项${path}不存在`);
-    return filed.getExpectedRef();
+  getValueByPath: (path: string|string[]) => {
+    if (Array.isArray(path))
+      path = path.join('.');
+    return accessFormModel(path, false, undefined);
   },
-  initFormData,
-  loadFormData,
-  submitForm,
-  resetForm,
-})
+  dispatchMessage,
+  dispatchReload,
+};
+
+provide('formRef', formRef);
+provide('formName', name.value || 'unnamed');
+
+defineExpose(formRef);
 
 </script>

+ 0 - 90
src/components/dynamic/DynamicFormCate.vue

@@ -1,90 +0,0 @@
-<template>
-  <view 
-    v-if="formDefine.type === 'group'" 
-    :class="`form-group ${formDefine.props.type}`"
-  >
-    <text class="form-group-title" v-if="formDefineParentLabel">
-      {{ formDefineParentLabel }}
-    </text>
-    <DynamicFormCateInner
-      :formDefine="formDefine"
-      :formModel="formModel" 
-      :groupType="formDefine.props.type"
-    />
-  </view>
-  <DynamicFormCateInner
-    v-else
-    :formDefine="formDefine"
-    :formModel="formModel" 
-  />
-</template>
-
-<script setup lang="ts">
-import type { PropType } from 'vue';
-import type { FormDefine } from '.';
-import DynamicFormCateInner from './DynamicFormCateInner.vue';
-
-export interface FormGroupProps {
-  type: 'row' | 'column' | 'block';
-}
-
-const props = defineProps({
-  formDefineParentLabel: {
-    type: null,
-    default: '' 
-  },
-  formDefineParentKey: {
-    type: String,
-    default: '' 
-  },
-  formModel: {
-    type: Object,
-    default: () => ({})
-  },
-  formDefine: {
-    type: Object as PropType<FormDefine>,
-    default: () => ({})
-  },
-})
-</script>
-
-<style lang="scss">
-.form-group {
-  display: flex;
-  flex-direction: column;
-
-  &.block {
-    margin-bottom: 32rpx;
-    padding: 24rpx 0;
-    background: #fff;
-    border-radius: 10rpx;
-
-    .form-group-title {
-      display: block;
-      font-size: 28rpx;
-      color: #333;
-      margin-bottom: 16rpx;
-    }
-  }
-
-  .form-group-title {
-    display: block;
-    flex-shrink: 0;
-    font-size: 28rpx;
-    color: #333;
-    margin-bottom: 16rpx;
-    margin-left: 26rpx;
-  }
-
-  &.row {
-    flex-direction: row;
-    justify-content: space-between;
-    align-items: center;
-
-    .form-group-title {
-      display: inline-block;
-      margin-left: 30rpx;
-    }
-  }
-}
-</style>

+ 0 - 85
src/components/dynamic/DynamicFormCateInner.vue

@@ -1,85 +0,0 @@
-<template>
-  <view 
-    v-for="(item, key) in formDefine.items"
-    :key="key"
-    :class="[
-      'form-cate-inner',
-      groupType
-    ]"
-  >
-    <DynamicFormCate
-      v-if="item.children"
-      :formDefine="item.children"
-      :formModel="children" 
-      :formDefineParentKey="item.name"
-      :formDefineParentLabel="item.label"
-      :parentModel="formModel"
-      :topModel="topModel"
-    />
-    <DynamicFormControl
-      v-else
-      :modelValue="formModel[item.name] ?? null"
-      :formDefineItem="item"
-      :parentModel="formModel"
-      :topModel="topModel"
-      :isLast="key === formDefine.items.length - 1"
-      @update:modelValue="(v: any) => formModel[item.name] = v"
-    />
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed, type PropType } from 'vue';
-import type { FormDefine } from '.';
-import DynamicFormControl from './DynamicFormControl.vue';
-import DynamicFormCate from './DynamicFormCate.vue';
-
-const props = defineProps({	
-  topModel: {
-    type: Object,
-    default: () => ({})
-  },
-  parentModel: {
-    type: null,
-  },
-  formModel: {
-    type: Object,
-    default: () => ({})
-  },
-  formDefineParentKey: {
-    type: String,
-    default: ''
-  },
-  formDefine: {
-    type: Object as PropType<FormDefine>,
-    default: () => ({})
-  },
-  groupType: {
-    type: String,
-    default: '' 
-  }
-})
-
-const children = computed(() => {
-  if (props.formDefineParentKey && props.formDefine.propNestType == 'nest')
-    return props.formModel[props.formDefineParentKey];
-  return props.formModel;
-});
-
-</script>
-
-<style lang="scss">
-.form-cate-inner {
-  display: flex;
-  flex-direction: column;
-
-  &.row {
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-  }
-}
-.form-static-text {
-  margin: 0 10rpx 20rpx 10px;
-}
-</style>

+ 287 - 222
src/components/dynamic/DynamicFormControl.vue

@@ -1,210 +1,223 @@
 <template>
-  <template v-if="show">
-    <text
-      v-if="formDefineItem.type === 'static-text' "
-      class="form-static-text"
-      :style="(params.style as any)"
-      :class="(params.class as any)"
-    >
-      {{ params?.text ?? modelValue ?? null }}
-    </text>
-    <Field
-      ref="formItemRef"
-      v-else-if="formDefineItem.type === 'text'"
-      :label="label"
-      :name="formDefineItem.fullName"
-      :modelValue="modelValue"
-      @update:modelValue="onValueChanged"
-      :maxlength="260"
-      :showBottomBorder="!isLast"
-      :required="Boolean(formDefineItem.rules?.length)"
-      v-bind="params"
-    />
-    <Field
-      ref="formItemRef"
-      v-else-if="formDefineItem.type === 'textarea'"
-      multiline
-      :label="label"
-      :name="formDefineItem.fullName"
-      :modelValue="modelValue"
-      :showBottomBorder="!isLast"
-      :required="Boolean(formDefineItem.rules?.length)"
-      @update:modelValue="onValueChanged"
-      v-bind="params"
-    />
-    <Field
-      v-else
-      ref="formItemRef"
-      :label="label"
-      :name="formDefineItem.fullName"
-      :required="Boolean(formDefineItem.rules?.length)"
-      :showRightArrow="extraDefine?.needArrow"
-      :showBottomBorder="!isLast"
-      :requireChildRef="() => itemRef"
-      v-bind="{ 
-        ...extraDefine?.itemProps || {},
-        ...formDefineItem.itemParams,
-      }"
-    >
-      <!-- <text>fullName: {{formDefineItem.fullName}}</text> -->
-      <template v-if="formDefineItem.type === 'number'">
-        <Stepper
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="params"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'switch'">
-        <Switch
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="params"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'radio-value'">
-        <RadioValue
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="(params as any as RadioValueProps)"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'radio-id'">
-        <RadioIdField
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="(params as any as RadioIdFieldProps)"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'select'">
-        <view>
-          <NaPickerField 
-            ref="itemRef"
-            :modelValue="modelValue"
-            @update:modelValue="onValueChanged"
-            v-bind="(params as any as PickerFieldProps)"
-          />
-        </view>
-      </template>
-      <template v-else-if="formDefineItem.type === 'uploader'">
-        <UploaderField
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="(params as any as UploaderFieldProps)"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'select-id'">
-        <PickerIdField 
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="(params as any as PickerIdFieldProps)"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'select-city'">
-        <PickerCityField
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="(params as any)"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'check-box'">
-        <CheckBox
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="params"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'check-box-list'"> 
-        <CheckBoxList
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="(params)"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'check-box-int'"> 
-        <CheckBoxToInt
+  <text
+    v-if="item.type === 'static-text' "
+    class="form-static-text"
+    :style="(params.style as any)"
+    :class="(params.class as any)"
+  >
+    {{ params?.text ?? model ?? null }}
+  </text>
+  <Field
+    ref="formItemRef"
+    v-else-if="item.type === 'text'"
+    :label="label"
+    :name="item.fullName"
+    :modelValue="model"
+    @update:modelValue="onValueChanged"
+    :maxlength="260"
+    :showBottomBorder="!isLast"
+    :required="Boolean(item.rules?.length)"
+    v-bind="{ 
+      ...params,
+      ...extraDefine?.itemProps || {},
+      ...item.formProps,
+    }"
+  />
+  <Field
+    ref="formItemRef"
+    v-else-if="item.type === 'textarea'"
+    multiline
+    :label="label"
+    :name="item.fullName"
+    :modelValue="model"
+    :showBottomBorder="!isLast"
+    :required="Boolean(item.rules?.length)"
+    @update:modelValue="onValueChanged"
+    v-bind="{ 
+      ...params,
+      ...extraDefine?.itemProps || {},
+      ...item.formProps,
+    }"
+  />
+  <Field
+    v-else
+    ref="formItemRef"
+    :label="label"
+    :name="item.fullName"
+    :required="Boolean(item.rules?.length)"
+    :showRightArrow="extraDefine?.needArrow"
+    :showBottomBorder="!isLast"
+    :requireChildRef="() => itemRef"
+    v-bind="{ 
+      ...extraDefine?.itemProps || {},
+      ...item.formProps,
+    }"
+  >
+    <!-- <text>fullName: {{item.fullName}}</text> -->
+    <template v-if="item.type === 'custom'">
+      <slot name="formCeil"
+        :name="item.fullName"
+        :item="item"
+        :model="model"
+        :onModelUpdate="onValueChanged"
+        :rawModel="rawModel"
+        :parentModel="parentModel"
+        :parent="parent"
+        :rules="item.rules"
+        :disabled="disabled"
+      />
+    </template>
+    <template v-else-if="item.type === 'number'">
+      <Stepper
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="params"
+      />
+    </template>
+    <template v-else-if="item.type === 'switch'">
+      <Switch
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="params"
+      />
+    </template>
+    <template v-else-if="item.type === 'radio-value'">
+      <RadioValue
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="(params as any as RadioValueProps)"
+      />
+    </template>
+    <template v-else-if="item.type === 'radio-id'">
+      <RadioIdField
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="(params as any as RadioIdFieldProps)"
+      />
+    </template>
+    <template v-else-if="item.type === 'select'">
+      <view>
+        <NaPickerField 
           ref="itemRef"
-          :modelValue="modelValue"
+          :modelValue="model"
           @update:modelValue="onValueChanged"
-          v-bind="params"
+          v-bind="(params as any as PickerFieldProps)"
         />
-      </template>
+      </view>
+    </template>
+    <template v-else-if="item.type === 'rate'">
+      <Rate
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="(params as any as RateProps)"
+      />
+    </template>
+    <template v-else-if="item.type === 'uploader'">
+      <UploaderField
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="(params as any as UploaderFieldProps)"
+      />
+    </template>
+    <template v-else-if="item.type === 'select-id'">
+      <PickerIdField 
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="(params as any as PickerIdFieldProps)"
+      />
+    </template>
+    <template v-else-if="item.type === 'select-city'">
+      <PickerCityField
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="(params as any)"
+      />
+    </template>
+    <template v-else-if="item.type === 'select-lonlat'">
+      <PickerLonlat
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="(v:any) => onValueChanged(v)"
+        v-bind="params"
+      />
+    </template>
+    <template v-else-if="item.type === 'check-box'">
+      <CheckBox
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="params"
+      />
+    </template>
+    <template v-else-if="item.type === 'check-box-list'"> 
+      <CheckBoxList
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="(params)"
+      />
+    </template>
+    <template v-else-if="item.type === 'check-box-int'"> 
+      <CheckBoxToInt
+        ref="itemRef"
+        :modelValue="model"
+        @update:modelValue="onValueChanged"
+        v-bind="params"
+      />
+    </template>
 
-      <template v-else-if="formDefineItem.type === 'select-lonlat'">
-        <PickerLonlat
+    <template v-else-if="item.type === 'datetime'">
+      <view>
+        <DateTimePickerField
           ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="(v:any) => onValueChanged(v)"
+          :value="model"
           v-bind="params"
+          @update:modelValue="(e: any) => onValueChanged(e)"
         />
-      </template>
-      <template v-else-if="formDefineItem.type === 'picker-datetime'">
-        <view>
-          <DateTimePickerField
-            ref="itemRef"
-            :value="modelValue"
-            v-bind="params"
-            @update:modelValue="(e: any) => onValueChanged(e)"
-          />
-        </view>
-      </template>
-      <template v-else-if="formDefineItem.type === 'picker-time'">
-        <view>
-          <TimePickerField
-            ref="itemRef"
-            :value="modelValue"
-            v-bind="params"
-            @update:modelValue="(e: any) => onValueChanged(e)"
-          />
-        </view>
-      </template>
-      <template v-else-if="formDefineItem.type === 'picker-date'">
-        <view>
-          <DatePickerField
-            ref="itemRef"
-            :value="modelValue"
-            v-bind="params"
-            @update:modelValue="(e: any) => onValueChanged(e)"
-          />
-        </view>
-      </template>
-      <!-- 在下方添加自定义组件 -->
-      <!-- 业务代码开始 -->
-      <template v-else-if="formDefineItem.type === 'richtext'">
-        <RichTextEditor
+      </view>
+    </template>
+    <template v-else-if="item.type === 'time'">
+      <view>
+        <TimePickerField
           ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
+          :value="model"
           v-bind="params"
+          @update:modelValue="(e: any) => onValueChanged(e)"
         />
-      </template>
-      <template v-else-if="formDefineItem.type === 'recorder'">
-        <Recorder
+      </view>
+    </template>
+    <template v-else-if="item.type === 'date'">
+      <view>
+        <DatePickerField
           ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
+          :value="model"
           v-bind="params"
+          @update:modelValue="(e: any) => onValueChanged(e)"
         />
-      </template>
-      <!-- 业务代码结束 -->
-      <template v-else>
-        <text>Fallback: unknow form type {{ formDefineItem.type }}</text>
-      </template>
-    </Field>
-  </template>
+      </view>
+    </template>
+    <ComponentRender v-else
+      ref="itemRef"
+      :modelValue="model"
+      @update:modelValue="onValueChanged"
+      :params="params"
+      :item="item"
+      :isLast="isLast"
+    />
+  </Field>
 </template>
 
 <script setup lang="ts">
-import { computed, inject, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue';
-import type { FormDefineItem, IFormItemCallback } from '.';
+import { computed, inject, onBeforeUnmount, onMounted, ref, watch, type PropType, type Ref } from 'vue';
+import type { IDynamicFormItem, IDynamicFormItemCallback, IDynamicFormObject, IDynamicFormOptions, IDynamicFormRef } from '.';
 import Field from '../form/Field.vue';
 import Stepper from '../form/Stepper.vue';
 import NaPickerField, { type PickerFieldProps } from '../form/PickerField.vue';
@@ -221,52 +234,100 @@ import PickerLonlat from './wrappers/PickerLonlat.vue';
 import DateTimePickerField from '../form/DateTimePickerField.vue';
 import TimePickerField from '../form/TimePickerField.vue';
 import DatePickerField from '../form/DatePickerField.vue';
-import RichTextEditor from '@/common/components/form/RichTextEditor.vue';
 import UploaderField, { type UploaderFieldProps } from '../form/UploaderField.vue';
 import RadioIdField from './wrappers/RadioIdField.vue';
 import type { RadioIdFieldProps } from './wrappers/RadioIdField';
-
-//自定义额外的组件默认配置,修改为自定义文件路径
-//业务代码开始
+import Rate, { type RateProps } from '../form/Rate.vue';
 import ComponentConfigs from '@/common/components/dynamicf/ComponentConfigs';
-import Recorder from '@/common/components/form/Recorder.vue';
-//业务代码结束
+import ComponentRender from '@/common/component/dynamicf/ComponentRender.vue';
+import type { Rules } from 'async-validator';
+
+export interface FormCeilProps {
+  value: unknown,
+  rawModel: unknown,
+  parent?: IDynamicFormItem,
+  parentModel: unknown,
+  'onUpdate:value': (v: unknown) => void,
+  item: IDynamicFormItem,
+  name: string,
+  disabled: boolean,
+  additionalProps: Record<string, unknown>,
+}
 
 const props = defineProps({	
-  parentModel: {
-    type: null, //TODO: parentModel
+  item: {
+    type: Object as PropType<IDynamicFormItem>,
+    required: true,
+  },
+  name: {
+    type: String,
+    default: ''
+  },
+  parent: {
+    type: Object as PropType<IDynamicFormItem>,
+    default: null
   },
-  modelValue: {
+  disabled: {
+    type: Boolean,
+    default: false
+  },
+  model: {
+    type: null
+  },
+  parentModel: {
     type: null
   },
-  formDefineItem: {
-    type: Object as PropType<FormDefineItem>,
-    default: () => ({})
+  rawModel: {
+    type: Object as PropType<Record<string, unknown>>,
+    default: null
+  },
+  noLabel: {
+    type: Boolean,
+    default: false
+  },
+  formWrapperColDefault: {
+    type: Object,
+    default: null
+  },
+  formLabelColDefault: {
+    type: Object,
+    default: null
+  },
+  isFirst: {
+    type: Boolean,
+    default: false,
   },
   isLast: {
     type: Boolean,
     default: false,
-  }
+  },
 });
 
 const formItemRef = ref();
-const topModel = inject<any>('formTopModel', {});
-const formGlobalParams = inject<any>('formGlobalParams', {});
+const finalOptions = inject<Ref<IDynamicFormOptions>>('finalOptions'); 
+const globalParams = inject<Ref<IDynamicFormObject>>('globalParams');
+const formRef = inject<IDynamicFormRef>('formRef');
+const formName = inject('formName', '');
 
-function evaluateCallback(val: unknown|IFormItemCallback<unknown>) {
-  if (typeof val === 'object' && typeof (val as IFormItemCallback<unknown>).callback === 'function')
-    return (val as IFormItemCallback<unknown>).callback(
-      props.modelValue, 
-      topModel.value, 
+function evaluateCallback(val: unknown|IDynamicFormItemCallback<unknown>) {
+  if (typeof val === 'object' && typeof (val as IDynamicFormItemCallback<unknown>).callback === 'function')
+    return (val as IDynamicFormItemCallback<unknown>).callback(
+      props.model, 
+      props.rawModel, 
       props.parentModel, 
       {
-        formGlobalParams: formGlobalParams.value,
-        item: props.formDefineItem,
+        item: props.item, 
+        parent: props.parent,
+        form: formRef!,
+        formGlobalParams: globalParams?.value || {},
+        formRules: (finalOptions?.value.formRules ?? {}) as Record<string, Rules>,
+        isFirst: props.isFirst,
+        isLast: props.isLast,
       }
     );
   return val as unknown;
 }
-function evaluateCallbackObj(val: Record<string, unknown|IFormItemCallback<unknown>>) {
+function evaluateCallbackObj(val: Record<string, unknown|IDynamicFormItemCallback<unknown>>) {
   const newObj = {} as Record<string, unknown>;
   for (const key in val) {
     if (Object.prototype.hasOwnProperty.call(val, key))
@@ -275,29 +336,33 @@ function evaluateCallbackObj(val: Record<string, unknown|IFormItemCallback<unkno
   return newObj;
 }
 
-const extraDefine = computed(() => ComponentConfigs.find((item) => item.name === props.formDefineItem.type))
+const extraDefine = computed(() => ComponentConfigs.find((item) => item.name === props.item.type))
 const params = computed(() => {
   return {
     ...extraDefine.value?.props || {},
-    ...evaluateCallbackObj(props.formDefineItem.params as any)
+    ...evaluateCallbackObj(props.item.additionalProps as any)
   } as Record<string, unknown>
 })
-const label = computed(() => evaluateCallback(props.formDefineItem.label) as string)
-const show = computed(() => props.formDefineItem.show === undefined || evaluateCallback(props.formDefineItem.show))
+const label = computed(() => evaluateCallback(props.item.label) as string)
 
 const itemRef = ref();
-const emit = defineEmits([ 'update:modelValue' ]);
+const emit = defineEmits([ 'update:model' ]);
  
 function onValueChanged(v: any) {
-  props.formDefineItem.onChange?.(props.modelValue, v, topModel.value, itemRef.value);
-  emit('update:modelValue', v);
+  props.item.watch?.(props.model, v, props.rawModel, getComponentRef());
+  emit('update:model', v);
+}
+function getComponentRef() {
+  if (typeof itemRef.value.getItemRef === 'function')
+    return itemRef.value.getItemRef();
+  return itemRef.value;
 }
 
 onMounted(() => {
-  props.formDefineItem.onMounted?.(topModel.value, itemRef.value);
+  props.item.mounted?.(props.model, props.rawModel, getComponentRef());
 })
 onBeforeUnmount(() => {
-  props.formDefineItem.onBeforeUnMount?.(topModel.value, itemRef.value); 
+  props.item.beforeUnmount?.(props.model, props.rawModel, getComponentRef()); 
 })
 
 </script>

+ 26 - 0
src/components/dynamic/group/FormArrayGroup.ts

@@ -0,0 +1,26 @@
+
+/**
+ * 动态表单类型是 arrag-object 时的 additionalProps
+ */
+export interface FormArrayGroupProps {
+  /**
+   * 是否显示添加按钮,默认是
+   */
+  showAddButton?: boolean,
+  /**
+   * 是否显示删除按钮,默认是
+   */
+  showDeleteButton?: boolean,
+  /**
+   * 是否显示上移下移按钮,默认是
+   */
+  showUpDownButton?: boolean,
+  /**
+   * 删除按钮回调,可选,不提供时默认操作为将 item 从 array 中移除。
+   */
+  deleteCallback?: (array: unknown[], item: unknown) => void,
+  /**
+   * 添加按钮回调,必填,否则用户无法添加数据
+   */
+  addCallback: (array: unknown[]) => void,
+}

+ 232 - 0
src/components/dynamic/group/FormArrayGroup.vue

@@ -0,0 +1,232 @@
+<template>
+  <div class="dynamic-form-array-group">
+    <!--列表-->
+    <div :class="['list', direction ]">
+      <!--循环数据条目-->
+      <!--对象类型-->
+      <template v-if="isObject">
+        <div v-for="(childData, key) in model"
+          :key="key" 
+          class="item-container"
+        >
+          <FormArrayGroupItem 
+            :item="item"
+            :parent="parent"
+            :name="name"
+            :childData="childData"
+            :keyName="`[${key}]`"
+            :isObject="true"
+            :showAddButton="showAddButton"
+            :showDeleteButton="showDeleteButton"
+            :showUpDownButton="showUpDownButton"
+            @delete="handleRemove"
+            @down="handleDown"
+            @up="handleUp"
+          >
+            <template #child="values">
+              <slot name="child" v-bind="values" />
+            </template>
+            <template #itemButton="values">
+              <slot name="itemButton" v-bind="values" />
+            </template>
+          </FormArrayGroupItem>
+        </div>
+      </template>
+      <!--普通变量类型-->
+      <template v-else>
+        <div v-for="(childData, key) in model"
+          :key="key" 
+          class="item-container single"
+        >
+          <FormArrayGroupItem 
+            :item="item"
+            :parent="parent"
+            :name="name"
+            :isObject="false"
+            :childData="childData"
+            :keyName="`[${key}]`"
+            :showAddButton="showAddButton"
+            :showDeleteButton="showDeleteButton"
+            :showUpDownButton="showUpDownButton"
+            :isFirst="key === 0"
+            :isLast="key === model.length - 1"
+            @update:childData="(v) => (model as unknown as IDynamicFormObject)[key] = v"
+            @delete="handleRemove"
+            @down="handleDown"
+            @up="handleUp"
+          >
+            <template #child="values">
+              <slot name="child" v-bind="values" />
+            </template>
+            <template #itemButton="values">
+              <slot name="itemButton" v-bind="values" />
+            </template>
+          </FormArrayGroupItem>
+        </div>
+      </template>
+    </div>
+
+    <!--添加按钮-->
+    <slot name="addButton" :onClick="handleAdd">
+      <button v-if="showAddButton" class="add dynamic-form-base-control base-button" type="button" @click="handleAdd">
+        <IconAdd /> 
+        <span>添加</span>
+      </button>
+    </slot>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ArrayUtils } from "@imengyu/imengyu-utils";
+import FormArrayGroupItem from "./FormArrayGroupItem.vue";
+import IconAdd from "../Images/IconAdd.vue";
+import type { IDynamicFormItem, IDynamicFormObject } from "../DynamicForm";
+
+export interface FormArrayGroupProps {
+  model: unknown[];
+  item: IDynamicFormItem;
+  parent?: IDynamicFormItem;
+  name: string;
+  isObject?: boolean;
+  direction?: 'vertical' | 'horizontal',
+  showAddButton?: boolean;
+  showDeleteButton?: boolean;
+  showUpDownButton?: boolean;
+  deleteCallback?: (arr: unknown[], data: unknown) => void;
+  addCallback?: (arr: unknown[]) => unknown;
+}
+
+const props = withDefaults(defineProps<FormArrayGroupProps>(), {
+  isObject: true,
+  showAddButton: true,
+  showDeleteButton: true,
+  showUpDownButton: true,
+  direction: 'vertical',
+})
+
+function handleAdd() {
+  if (typeof props.addCallback === "function") {
+    const arr = props.model;
+    const ret = props.addCallback(props.model);
+    if (typeof ret !== "undefined")
+      arr.push(ret);
+  }
+}
+function handleRemove(data: unknown) {
+  const arr = props.model;
+  if (typeof props.deleteCallback === "function")
+    props.deleteCallback(arr, data);
+  else {
+    const index = arr.indexOf(data);
+    if (index >= 0)
+      arr.splice(index, 1);
+  }
+}
+function handleUp(data: unknown) {
+  const arr = props.model;
+  const index = arr.indexOf(data);
+  if (index > 0)
+    ArrayUtils.upData(arr, index);
+}
+function handleDown(data: unknown) {
+  const arr = props.model as unknown[];
+  const index = arr.indexOf(data);
+  if (index < arr.length - 1)
+    ArrayUtils.downData(arr, index);
+}
+</script>
+
+<style lang="scss">
+.dynamic-form-array-group {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: flex-start;
+
+  > .list {
+
+    &.vertical {
+      display: flex;
+      flex-direction: column;
+      justify-content: flex-start;
+      align-items: flex-start;
+        
+      .item-container {
+        flex: 1;
+      }
+    }
+    &.horizontal {
+      display: flex;
+      flex-direction: row;
+      flex-wrap: wrap;
+      justify-content: flex-start;
+      align-items: flex-start;
+    }
+  }
+
+  .item-container {
+    display: flex;
+    flex-direction: row;
+    background-color: var(--dynamic-form-background-color);
+    padding: 10px;
+    padding-bottom: 0;
+    border-radius: var(--dynamic-form-border-radius);
+    margin-bottom: 10px;
+  }
+
+  .form-container {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+  }
+
+  .nav-button-conntainer {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+
+    .base-button {
+      padding: 0;
+      width: 24px;
+      height: 24px;
+      border-radius: 50%;
+
+      svg {
+        width: 16px;
+        height: 16px;
+      }
+    }
+  }
+
+  .base-button {
+    cursor: pointer;
+    min-width: 20px !important;
+
+    &.margin {
+      margin-left: 6px;
+    }
+
+    &.add {
+      --dynamic-form-button-shadow-color: var(--dynamic-form-shadow-primary-color);
+      
+      color: var(--dynamic-form-text-light-color);
+      background-color: var(--dynamic-form-primary-color);
+
+      &:hover {
+        background-color: var(--dynamic-form-primary-color2);
+      }
+    }
+
+    &.delete {
+      --dynamic-form-button-shadow-color: var(--dynamic-form-shadow-error-color);
+
+      color: var(--dynamic-form-text-light-color);
+      background-color: var(--dynamic-form-error-color);
+
+      &:hover {
+        background-color: var(--dynamic-form-error-color2);
+      }
+    }
+  }
+}
+</style>

+ 102 - 0
src/components/dynamic/group/FormArrayGroupItem.vue

@@ -0,0 +1,102 @@
+<template>
+  <div v-if="isObject" class="form-container">
+    <!--循环子条目-->
+    <slot 
+      name="child" 
+      v-for="(child, k) in item.children"
+      :key="k"
+      :item="child"
+      :kname="name+keyName+'.'+child.name"
+      :model="(childData as IDynamicFormObject)[child.name]"
+      :onUpdateValue="(v: any) => (childData as IDynamicFormObject)[child.name] = v"
+      :isFirst="k === 0"
+      :isLast="k === (item.children?.length || 0) - 1"
+    />
+  </div>
+  <div v-else-if="item.children" class="form-container">
+    <!--循环子条目-->
+    <slot 
+      name="child"
+      :item="item.children?.[0]"
+      :pitem="parent"
+      :kname="name+keyName"
+      :model="childData"
+      :onUpdateValue="(v: any) => $emit('update:childData', v)"
+      :isFirst="isFirst"
+      :isLast="isLast"
+    />
+  </div>
+
+  <slot name="itemButton" 
+    :onDeleteClick="() => $emit('delete', childData)"
+    :onUpClick="() => $emit('up', childData)"
+    :onDownClick="() => $emit('down', childData)"
+  >
+    <div class="nav-button-conntainer">
+      <button v-if="showUpDownButton" title="上移" class="dynamic-form-base-control base-button margin" type="button" @click="$emit('up', childData)">
+        <IconUp />
+      </button>
+      <button v-if="showUpDownButton" title="下移" class="dynamic-form-base-control base-button margin" type="button" @click="$emit('down', childData)">
+        <IconDown />
+      </button>
+      <button v-if="showDeleteButton" title="删除" class="dynamic-form-base-control base-button delete margin" type="button" @click="$emit('delete', childData)">
+        <IconDelete />
+      </button>
+    </div>
+  </slot>
+</template>
+
+<script lang="ts" setup>
+import type { PropType } from "vue";
+import type { IDynamicFormItem, IDynamicFormObject } from "../DynamicForm";
+import IconUp from "../Images/IconUp.vue";
+import IconDown from "../Images/IconDown.vue";
+import IconDelete from "../Images/IconDelete.vue";
+
+defineEmits([ 'up', 'down', 'delete', 'update:childData' ]);
+defineProps({
+  childData: {
+    required: true,
+  },
+  isObject: {
+    type: Boolean,
+    default: true,
+  },
+  name: {
+    type: String,
+    required: true,
+  },
+  keyName: {
+    type: [String, Number],
+    required: true,
+  },
+  item: {
+    type: Object as PropType<IDynamicFormItem>,
+    required: true,
+  },
+  parent: {
+    type: Object as PropType<IDynamicFormItem>,
+    default: null
+  },
+  showAddButton: {
+    type: Boolean,
+    default: true,
+  },
+  showDeleteButton: {
+    type: Boolean,
+    default: true,
+  },
+  showUpDownButton: {
+    type: Boolean,
+    default: true,
+  },
+  isFirst: {
+    type: Boolean,
+    default: false,
+  },
+  isLast: {
+    type: Boolean,
+    default: false,
+  },
+});
+</script>

+ 31 - 0
src/components/dynamic/group/FormGroup.ts

@@ -0,0 +1,31 @@
+import type { HTMLAttributes } from "vue";
+
+export interface FormGroupProps extends HTMLAttributes {
+  /**
+   * 标题
+   */
+  title: string
+  /**
+   * 列元素之间的间距(单位为 px)
+   */
+  gutter: number,
+  /**
+   * flex 布局下的水平排列方式:
+   */
+  justify: 'start'|'end'|'center'|'space-around'|'space-between';
+  /**
+   * 是否为朴素样式
+   * @default false
+   */
+  plain: boolean,
+  /**
+   * 是否默认折叠
+   * @default false
+   */
+  collapsed: boolean,
+  /**
+   * 是否可折叠
+   * @default false
+   */
+  collapsible: boolean,
+}

+ 115 - 0
src/components/dynamic/group/FormGroup.vue

@@ -0,0 +1,115 @@
+<template>
+  <div :class="[
+    'dynamic-form-group', 
+    {
+      'collapsed': collapsed,
+      'collapsible': collapsible,
+      'plain': plain,
+    }
+  ]">
+    <h5 v-if="title" @click="collapsible ? collapsed = !collapsed : null">
+      <span>{{ title }}</span>
+      <IconDown v-if="collapsible" class="collapsible-icon" />
+    </h5>
+    <Row v-if="!collapsed" :justify="(justify as any)" :gutter="gutter">
+      <slot />
+    </Row>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import Row from "../DynamicFormBasicControls/Layout/Row.vue";
+import IconDown from "../Images/IconDown.vue";
+
+const props = defineProps({
+  /**
+   * 标题
+   */
+  title: {
+    type: String,
+    default: "",
+  },
+  /**
+   * 栅格间隔 px
+   */
+  gutter: {
+    type: Number,
+    default: null,
+  },
+  /**
+   * flex 布局下的水平排列方式:start end center space-around space-between
+   */
+  justify: {
+    type: String,
+    default: "start",
+  },
+  /**
+   * 是否可折叠
+   */
+  collapsible: {
+    type: Boolean,
+    default: false,
+  },
+  /**
+   * 是否为朴素样式
+   */
+  plain: {
+    type: Boolean,
+    default: false,
+  },
+  /**
+   * 是否默认折叠
+   */
+  collapsed: {
+    type: Boolean,
+    default: false,
+  },
+});
+
+const collapsed = ref(props.collapsed);
+
+
+</script>
+
+<style lang="scss">
+.dynamic-form-group {
+  padding: 10px;
+  background-color: var(--dynamic-form-background-color);
+  border-radius: var(--dynamic-form-border-radius);
+
+  &.collapsed {
+    padding: 0;
+
+    .collapsible-icon {
+      transform: rotate(0deg);
+    }
+  }
+  &.collapsible {
+    h5 {
+      cursor: pointer;
+    }
+  }
+
+  .collapsible-icon {
+    transform: rotate(180deg);
+    transition: transform 0.3s ease-in-out;
+    width: 16px;
+    height: 16px;
+  }
+
+  h5 {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    color: var(--dynamic-form-text-color);
+    margin: 0;
+    margin-bottom: 12px;
+  }
+
+  &.plain {
+    padding: 0;
+    background-color: transparent;
+  }
+}
+</style>

+ 1 - 174
src/components/dynamic/index.ts

@@ -1,174 +1 @@
-import type { RuleItem } from "async-validator";
-import type { FormInstance } from "../form/Form.vue";
-import type { FieldProps } from "../form/Field.vue";
-import type { Ref } from "vue";
-
-export interface FormDefine {
-  /**
-   * Todo: page
-   */
-  type?: 'flat'|'page'|'group',
-  props?: any;
-  /**
-   * 表单属性嵌套类型
-   * * flat: 扁平属性,直接在表单对象上定义属性
-   * * nest: 嵌套属性,属性值为对象,对象下有子属性
-   * * array: 数组属性,属性值为数组,数组下有子属性
-   */
-  propNestType?: 'flat'|'nest'|'array',
-  /**
-   * 子条目定义
-   */
-  items: FormDefineItem[];
-}
-
-
-/**
- * 表单动态属性定义
- */
-export declare type IFormItemCallback<T> = {
-  /**
-   * 预留,暂未使用
-   */
-  type?: string;
-  /**
-   * @param model 当前表单条目的值
-   * @param rawModel 整个 form 的值 (最常用,当两个关联组件距离较远时,可以从顶层的 rawModel 里获取)
-   * @param parentModel 父表单元素的值 (上一级的值,只在列表场景的使用,例如列表某个元素的父级就是整个 item)
-   * @param item 当前表单条目信息
-   */
-  callback: (model: any, rawModel: any, parentModel: any, params: { 
-    formGlobalParams: any, 
-    item: FormDefineItem
-  }) => T;
-};
-export type IFormItemCallbackAdditionalProps<T> = { [P in keyof T]?: T[P]|IFormItemCallback<T[P]> }
-
-/**
- * 表单动态组件属性定义
- */
-export interface FormItemComponentAdditionalDefine {
-  /**
-   * 组件名称
-   */
-  name: string,
-  /**
-   * 是否需要显示右侧箭头
-   */
-  needArrow?: boolean,
-  /**
-   * 传递给表单条目的参数
-   */
-  itemProps?: FieldProps,
-  /**
-   * 传递给组件的参数
-   */
-  props?: Record<string, unknown>|unknown,
-}
-
-export interface FormDefineItem {
-  /**
-   * 表单项显示标签
-   */
-  label?: string|IFormItemCallback<string>;
-  /**
-   * 属性名称
-   */
-  name: string;
-  fullName?: string;
-  /**
-   * 表单项组件类型
-   */
-  type?: string;
-  /**
-   * 传递给条目组件的参数。(允许动态回调)
-   */
-  params?: Record<string, unknown|IFormItemCallback<unknown>>|unknown;
-  /**
-   * 传递给FormItem组件的参数
-   */
-  itemParams?: any;
-  /**
-   * 默认值,用于默认数据生成
-   */
-  defaultValue?: any;
-  /**
-   * 当前条目的校验规则
-   */
-  rules?: RuleItem[],
-  /**
-   * 子条目,在对象中为对象子属性,在数组中为数组条目(单条目按单项控制,多条目按对象看待控制)
-   */
-  children?: FormDefine,
-
-  //todo:联动
-
-  /**
-   * 是否显示。当为undefined时,默认显示。
-   */
-  show?: boolean|IFormItemCallback<boolean>|undefined,
-
-  /**
-   * 当前条目组件加载时发生事件
-   * @param topModel 顶层数据对象
-   * @param ref 组件实例
-   * @returns 
-   */
-  onMounted?: (topModel: any, ref: any) => void;
-  /**
-   * 当前条目组件卸载时发生事件
-   * @param topModel 顶层数据对象
-   * @param ref 组件实例
-   * @returns 
-   */
-  onBeforeUnMount?: (topModel: any, ref: any) => void;
-  /**
-   * 当前条目数据更改时发生事件
-   * @param oldValue 旧值
-   * @param newValue 新值
-   * @param topModel 顶层数据对象
-   * @param ref 组件实例
-   * @returns 
-   */
-  onChange?: (oldValue: any, newValue: any, topModel: any, ref: any) => void;
-}
-export interface FormExport {
-  /**
-   * 初始化表单数据对象
-   */
-  initFormData(data: () => any): void;
-  /**
-   * 获取表单数据模型
-   * @returns 表单数据模型
-   */
-  getFormModel(): Ref<Record<string, any>>;
-  /**
-   * 获取表单实例
-   * @returns 表单实例
-   */
-  getFormRef(): FormInstance;
-  /**
-   * 获取表单条目的实例
-   * @param path 表单条目的路径
-   * @returns 表单条目的实例
-   */
-  getFormItemRef(path: string): any;
-  /**
-   * 获取表单数据
-   * @returns 表单数据
-   */
-  getFormData(): Record<string, any>;
-  /**
-   * 加载表单数据
-   * @param value 表单数据
-   */
-  loadFormData(value?: Record<string, any>): void;
-  /**
-   * 提交表单
-   */
-  submitForm<T = Record<string, any>>(): Promise<T|null>;
-  /**
-   * 重置整个表单数据
-   */
-  resetForm(): void;
-}
+export * from './DynamicForm'

+ 65 - 0
src/components/dynamic/nest/DynamicFormCheckEmpty.vue

@@ -0,0 +1,65 @@
+
+<template>
+  <Alert v-if="!suppressEmptyError && (model === undefined || model === null)"
+    type="warning"
+    :message="`DynamicForm: Input field ${name} is undefined or null`"
+    :description="`At form ${formName}: ${name}`"
+  />
+  <Alert v-else-if="(checkType === 'array' && !Array.isArray(modelWithDefault))"
+    type="error"
+    :message="`DynamicForm: Input field ${name} (${modelWithDefault}) is not array`"
+    :description="`At form ${formName}: ${name}`"
+  />
+  <Alert v-else-if="(checkType && checkType !== 'array' && typeof modelWithDefault !== checkType)"
+    type="error"
+    :message="`DynamicForm: Input field ${name} (${modelWithDefault}) is not a ${checkType}`"
+    :description="`At form ${formName}: ${name}`"
+  />
+  <Alert v-else-if="(checkCustomType && !checkCustomType(modelWithDefault))"
+    type="error"
+    :message="`DynamicForm: Input field ${name} (${modelWithDefault}) is not a valid type, type is ${typeof modelWithDefault}`"
+    :description="`At form ${formName}: ${name}`"
+  />
+  <slot v-else />
+</template>
+
+<script lang="ts" setup>
+import { inject, type PropType } from 'vue';
+import type { IDynamicFormObject } from '..';
+import Alert from '@/components/feedback/Alert.vue';
+
+const formName = inject('formName', '')
+
+/**
+ * 动态表单组件。
+ */
+defineProps({
+  model: {
+    type: Object as PropType<IDynamicFormObject>,
+    default: null
+  },
+  modelWithDefault: {
+    type: Object as PropType<IDynamicFormObject>,
+    default: null
+  },
+  checkType: {
+    type: String as PropType<'object' | 'string' | 'number' | 'array' | ''>,
+    default: ''
+  },
+  checkCustomType: {
+    type: Function as PropType<(value: any) => boolean>,
+  },
+  name: {
+    type: String,
+    default: ''
+  },
+  /**
+   * 是否屏蔽空错误。默认否
+   * @default false
+   */
+  suppressEmptyError: {
+    type: Boolean,
+    default: false
+  },
+});
+</script>

+ 425 - 0
src/components/dynamic/nest/DynamicFormItemContainer.vue

@@ -0,0 +1,425 @@
+
+<template>
+  <Col 
+    v-if="isShow"
+    :class="[
+      'dynamic-form-item-wrapper',
+      finalOptions?.nestObjectMargin !== false && item.nestObjectMargin !== false ? 'nest-with-margin' : '',
+    ]"
+    :data-dynamic-form-item="item.name"
+    :data-dynamic-form-item-type="item.type"
+    v-bind="{
+      ...colProps,
+      ...(item.colProps as {})
+    }"
+  >
+    <!--对象组-->
+    <DynamicFormCheckEmpty 
+      v-if="item.type === 'object'" 
+      :model="model"
+      :modelWithDefault="finalModel"
+      :suppressEmptyError="editmode || finalOptions?.suppressEmptyError || item.suppressEmptyError"
+      :name="name"
+      checkType="object"
+    >
+      <!--标题-->
+      <DynamicFormItemNormal 
+        v-if="item.label"
+        :item="item"
+        :parent="parent"
+        :name="''"
+        :rawModel="rawModel"
+        :model="null"
+        :noLabel="true"
+        :disabled="disabled || evaluateCallback(item.disabled)"
+      >
+        <template #insertion>
+          <span v-if="item.label" class="dynamic-form-object-title">{{ evaluateCallback(item.label) }}</span>
+        </template>
+      </DynamicFormItemNormal>
+      <DynamicFormItemEditorContainerEmptyNote v-if="showContainerEmptyNote" />
+      <!--循环子条目-->
+      <DynamicFormItemContainer 
+        v-for="(child, k) in item.children"
+        :key="k"
+        :item="child"
+        :name="name+'.'+child.name"
+        :rawModel="rawModel"
+        :model="(finalModel as IDynamicFormObject)[child.name]"
+        :parent="item"
+        :parentModel="finalModel"
+        :parentName="name"
+        :isFirst="k === 0"
+        :isLast="k === (item.children?.length || 0) - 1"
+        @update:model="(v: unknown) => (model as IDynamicFormObject)[child.name] = v"
+        :disabled="disabled || evaluateCallback(item.disabled)"
+      />
+    </DynamicFormCheckEmpty>
+    <!--对象组-->
+    <DynamicFormCheckEmpty 
+      v-else-if="item.type === 'object-group'" 
+      :model="model"
+      :modelWithDefault="finalModel"
+      :suppressEmptyError="editmode || finalOptions?.suppressEmptyError || item.suppressEmptyError"
+      :name="name"
+      checkType="object"
+    >
+      <FormGroup :title="evaluateCallback(item.label)" v-bind="(item.additionalProps as object)">
+        <DynamicFormItemEditorContainerEmptyNote v-if="showContainerEmptyNote" />
+        <Row v-bind="item.rowProps">
+          <!--循环子条目-->
+          <DynamicFormItemContainer 
+            v-for="(child, k) in item.children" 
+            :key="k"
+            :item="child"
+            :colProps="{ ...item.childrenColProps, ...child.colProps }"
+            :name="name+'.'+child.name"
+            :rawModel="rawModel"
+            :model="((finalModel as IDynamicFormObject)[child.name] as IDynamicFormObject)"
+            :parent="item"
+            :parentModel="finalModel"
+            :parentName="name"
+            :isFirst="k === 0"
+            :isLast="k === (item.children?.length || 0) - 1"
+            @update:model="(v: unknown) => (model as IDynamicFormObject)[child.name] = v"
+            :disabled="disabled || evaluateCallback(item.disabled)"
+          />
+        </Row>
+      </FormGroup>
+    </DynamicFormCheckEmpty>
+    <!--扁平组-->
+    <FormGroup v-else-if="item.type === 'flat-group'" :title="evaluateCallback(item.label)" v-bind="(item.additionalProps as object)">
+      <DynamicFormItemEditorContainerEmptyNote v-if="showContainerEmptyNote" />
+      <Row v-bind="item.rowProps">
+        <!--循环子条目-->
+        <DynamicFormItemContainer 
+          v-for="(child, k) in item.children" 
+          :colProps="{ ...item.childrenColProps, ...child.colProps }"
+          :key="k"
+          :item="child"
+          :name="parentName ? `${parentName}.${child.name}` : child.name"
+          :rawModel="rawModel"
+          :model="((parentModel as IDynamicFormObject)[child.name])"
+          :parent="item"
+          :parentModel="parentModel"
+          :parentName="parentName"
+          :isFirst="k === 0"
+          :isLast="k === (item.children?.length || 0) - 1"
+          @update:model="(v: unknown) => (parentModel as IDynamicFormObject)[child.name] = v"
+          :disabled="disabled || evaluateCallback(item.disabled)"
+        >
+          <template #formCeil="values">
+            <slot name="formCeil" v-bind="(values as FormCeilProps)" />
+          </template>
+        </DynamicFormItemContainer>
+      </Row>
+    </FormGroup>
+    <!--扁平普通-->
+    <DynamicFormItemNormal v-else-if="item.type === 'flat-simple'" 
+      :item="item"
+      :parent="parent"
+      :name="name"
+      :disabled="disabled || evaluateCallback(item.disabled)"
+      :model="(finalModel as IDynamicFormObject)"
+      :rawModel="rawModel"
+      :parentModel="parentModel"
+      :parentName="parentName"
+    >
+      <template #insertion>
+        <DynamicFormItemEditorContainerEmptyNote v-if="showContainerEmptyNote" />
+        <Row v-bind="item.rowProps">
+          <!--循环子条目-->
+          <DynamicFormItemContainer 
+            v-for="(child, k) in item.children" 
+            :key="k"
+            :item="child"
+            :colProps="{ ...item.childrenColProps, ...child.colProps }"
+            :name="parentName ? `${parentName}.${child.name}` : child.name"
+            :rawModel="rawModel"
+            :model="((parentModel as IDynamicFormObject)[child.name])"
+            :parent="item"
+            :parentModel="parentModel"
+            :parentName="parentName"
+            :isFirst="k === 0"
+            :isLast="k === (item.children?.length || 0) - 1"
+            @update:model="(v: unknown) => (parentModel as IDynamicFormObject)[child.name] = v"
+            :disabled="disabled || evaluateCallback(item.disabled)"
+          >
+            <template #formCeil="values">
+              <slot name="formCeil" v-bind="(values as FormCeilProps)" />
+            </template>
+          </DynamicFormItemContainer>
+        </Row>
+      </template>
+    </DynamicFormItemNormal>
+    <!--数组变量组-->
+    <DynamicFormCheckEmpty 
+      v-else-if="item.type === 'array-single'"
+      :model="model"
+      :modelWithDefault="finalModel"
+      :suppressEmptyError="editmode || finalOptions?.suppressEmptyError || item.suppressEmptyError"
+      :name="name"
+      checkType="array"
+    >
+      <DynamicFormItemNormal 
+        :item="item"
+        :name="name"
+        :disabled="disabled || evaluateCallback(item.disabled)"
+        :model="(finalModel as IDynamicFormObject)"
+        :rawModel="rawModel"
+        :parentModel="parentModel"
+        :parent="parent"
+        :parentName="name"
+      >
+        <template #insertion>
+          <DynamicFormItemEditorContainerEmptyNote v-if="showContainerEmptyNote" />
+          <FormArrayGroup
+            :model="(finalModel as unknown as unknown[])"
+            :item="item"
+            :parent="parent"
+            :name="name"
+            :rawModel="rawModel"
+            :isObject="false"
+            :addCallback="item.newChildrenObject"
+            :deleteCallback="item.deleteChildrenCallback"
+            v-bind="(item.additionalProps as IDynamicFormObject)"
+          >
+            <template #addButton="props">
+              <slot name="arrayButtonAdd" v-bind="props" />
+            </template>
+            <template #itemButton="props">
+              <slot name="arrayButtons" v-bind="props" />
+            </template>
+            <template #child="{ item, pitem, kname, model: child, onUpdateValue, isFirst, isLast }">
+              <DynamicFormItemContainer
+                :item="item"
+                :name="kname"
+                :rawModel="rawModel"
+                :model="child"
+                :parent="pitem"
+                :parentModel="model"
+                :parentName="name"
+                :disabled="disabled || evaluateCallback(item.disabled)"
+                :isFirst="isFirst"
+                :isLast="isLast"
+                @update:model="(v: unknown) => onUpdateValue(v)"
+              />
+            </template>
+          </FormArrayGroup>
+        </template>
+      </DynamicFormItemNormal>
+    </DynamicFormCheckEmpty>
+    <!--数组对象组-->
+    <DynamicFormCheckEmpty 
+      v-else-if="item.type === 'array-object'"
+      :model="model"
+      :modelWithDefault="finalModel"
+      :suppressEmptyError="editmode || finalOptions?.suppressEmptyError || item.suppressEmptyError"
+      :name="name"
+      checkType="array"
+    >
+      <DynamicFormItemNormal  
+        :item="item"
+        :parent="parent"
+        :name="name"
+        :disabled="disabled || evaluateCallback(item.disabled)"
+        :model="finalModel"
+        :rawModel="rawModel"
+        :parentModel="parentModel"
+        :parentName="parentName"
+        :isFirst="isFirst"
+        :isLast="isLast"
+      >
+        <template #insertion>
+          <DynamicFormItemEditorContainerEmptyNote v-if="showContainerEmptyNote" />
+          <FormArrayGroup
+            :model="(finalModel as unknown as unknown[])"
+            :item="item"
+            :parent="parent"
+            :name="name"
+            :rawModel="rawModel"
+            :isObject="true"
+            :addCallback="item.newChildrenObject"
+            :deleteCallback="item.deleteChildrenCallback"
+            v-bind="(item.additionalProps as IDynamicFormObject)"
+          >
+            <template #addButton="props">
+              <slot name="arrayButtonAdd" v-bind="props" />
+            </template>
+            <template #itemButton="props">
+              <slot name="arrayButtons" v-bind="props" />
+            </template>
+            <template #child="{ item, pitem, kname, model: child, onUpdateValue, isFirst, isLast }">
+              <DynamicFormItemContainer
+                :item="item"
+                :name="kname"
+                :rawModel="rawModel"
+                :model="child"
+                :parent="pitem"
+                :parentModel="model"
+                :parentName="name"
+                :isFirst="isFirst"
+                :isLast="isLast"
+                :disabled="disabled || evaluateCallback(item.disabled)"
+                @update:model="(v: unknown) => onUpdateValue(v)"
+              />
+            </template>
+          </FormArrayGroup>
+        </template>
+      </DynamicFormItemNormal>
+    </DynamicFormCheckEmpty>
+    <!--正常条目-->
+    <DynamicFormItemNormal
+      v-else
+      :item="item"
+      :parent="parent"
+      :name="name"
+      :disabled="disabled || evaluateCallback(item.disabled)"
+      :rawModel="rawModel"
+      :parentModel="parentModel"
+      :model="finalModel"
+      :isFirst="isFirst"
+      :isLast="isLast"
+      @update:model="(v: unknown) => $emit('update:model', v)"
+    >
+      <template #formCeil="values">
+        <slot name="formCeil" v-bind="(values as unknown as FormCeilProps)" />
+      </template>
+    </DynamicFormItemNormal>
+  </Col>
+</template>
+
+<script lang="ts" setup>
+import { inject, type PropType, type Ref, toRefs, computed, provide } from 'vue';
+import type { Rules } from 'async-validator';
+import DynamicFormItemNormal, { type FormCeilProps } from '../DynamicFormControl.vue';
+import FormGroup from '../group/FormGroup.vue';
+import FormArrayGroup from '../group/FormArrayGroup.vue';;
+import Col, { type ColProps } from '@/components/layout/grid/Col.vue';
+import Row from '@/components/layout/grid/Row.vue';
+import DynamicFormCheckEmpty from './DynamicFormCheckEmpty.vue';
+import type { IDynamicFormItem, IDynamicFormItemCallback, IDynamicFormObject, IDynamicFormOptions, IDynamicFormRef, IEvaluateCallback } from '..';
+
+/**
+ * 动态表单条目包装组件,处理基础类型分支、数据传入、回调处理、事件传递。
+ */
+
+const props = defineProps({
+  item: {
+    type: Object as PropType<IDynamicFormItem>,
+    required: true,
+  },
+  name: {
+    type: String,
+    required: true,
+  },
+  disabled: {
+    type: Boolean,
+    default: false,
+  },
+  model: {
+    type: null
+  },
+  parent: {
+    type: Object as PropType<IDynamicFormItem>,
+    default: null,
+  },
+  parentModel: {
+    type: null
+  },
+  parentName: {
+    type: String,
+    default: null,
+  },
+  rawModel: {
+    type: Object as PropType<IDynamicFormObject>,
+    required: true,
+  },
+  colProps: {
+    type: Object as PropType<ColProps>,
+    default: null,
+  },
+  isFirst: {
+    type: Boolean,
+    default: false,
+  },
+  isLast: {
+    type: Boolean,
+    default: false,
+  },
+});
+
+const containerTypes = ['object', 'object-group', 'array-single','group-array','flat-simple','flat-group'];
+const isContainer = computed(() => props.item.type && containerTypes.includes(props.item.type));
+const showContainerEmptyNote = computed(() => isContainer.value && (!props.item.children || props.item.children.length === 0));
+
+defineEmits([	'update:model' ]);
+defineSlots<{
+  formCeil(props: FormCeilProps): any,
+  arrayButtonAdd(props: {
+    onClick: () => void;
+  }): any,
+  arrayButtons(props: {
+    onDeleteClick: () => void;
+    onUpClick: () => void;
+    onDownClick: () => void;
+  }): any,
+}>()
+
+const propsP = toRefs(props);
+const finalOptions = inject<Ref<IDynamicFormOptions>>('finalOptions'); 
+const globalParams = inject<Ref<IDynamicFormObject>>('globalParams');
+const formRef = inject<IDynamicFormRef>('formRef');
+const editmode = inject('editmode', false);
+const formName = inject('formName', '')
+
+//判断是否显示当前条目
+const isShow = computed(() => {
+  if (props.item.show !== undefined) {
+    const show = evaluateCallback(props.item.show);
+    if (show === false)
+      return false;
+  }
+  return true;
+});
+
+//处理默认值
+const finalModel = computed(() => {
+  const val = props.model
+  if (val !== undefined && val !== null)
+    return props.model;
+  if (props.item.defaultValue) {
+    if (typeof props.item.defaultValue === 'function')
+      return props.item.defaultValue();
+    return props.item.defaultValue;
+  }
+  return null;
+});
+
+//处理回调函数
+function evaluateCallback<T>(val: T|IDynamicFormItemCallback<T>) {
+  if (typeof val === 'object' && typeof (val as IDynamicFormItemCallback<T>).callback === 'function')
+    return (val as IDynamicFormItemCallback<T>).callback(
+      finalModel.value, 
+      propsP.rawModel.value,
+      propsP.parentModel?.value,
+      {
+        item: propsP.item.value,
+        form: formRef!,
+        formGlobalParams: globalParams?.value || {},
+        formRules: (finalOptions?.value.formRules ?? {}) as Record<string, Rules>,
+        isFirst: propsP.isFirst.value,
+        isLast: propsP.isLast.value,
+      }
+    );
+  return val as T;
+}
+
+provide<IEvaluateCallback>('evaluateCallback', evaluateCallback);
+</script>
+
+<style lang="scss">
+.dynamic-form-item-wrapper {
+  position: relative;
+}
+</style>

+ 70 - 0
src/components/dynamic/nest/DynamicFormRoot.vue

@@ -0,0 +1,70 @@
+<!-- eslint-disable vue/no-mutating-props -->
+<template>
+  <!--空显示-->
+  <slot name="empty" v-if="options.formItems?.length == 0 || !model">
+    <div v-if="options.emptyText" class="dynamic-form-item-empty">{{ options.emptyText }}</div>
+  </slot>
+  <Alert
+    v-else-if="(typeof model !== 'object' && !options.suppressRootError)"
+    type="warning"
+    message="DynamicForm: model is not a object!"
+    :description="`At form ${name || 'unnamed'} Root`"
+  />
+  <template v-else>
+    <!--表单条目渲染核心-->
+    <DynamicFormItemContainer 
+      v-for="(item, index) in options.formItems"
+      :key="index"
+      :item="item"
+      :name="item.name"
+      :rawModel="finalModel"
+      :model="finalModel[item.name]"
+      :parentModel="finalModel"
+      :isFirst="index === 0"
+      :isLast="index === options.formItems.length - 1"
+      @update:model="(v: unknown) => finalModel[item.name] = v"
+      :disabled="options.disabled"
+    >
+      <template #arrayButtonAdd="props">
+        <slot name="formArrayButtonAdd" v-bind="props" />
+      </template>
+      <template #arrayButtons="props">
+        <slot name="formArrayButtons" v-bind="props" />
+      </template>
+      <template #formCeil="values">
+        <slot name="formCeil" v-bind="values" />
+      </template>
+    </DynamicFormItemContainer>
+    <slot name="endButton" />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, type PropType } from 'vue';
+import DynamicFormItemContainer from './DynamicFormItemContainer.vue';
+import type { IDynamicFormObject, IDynamicFormOptions } from '..';
+import Alert from '@/components/feedback/Alert.vue';
+
+/**
+ * 动态表单组件。
+ */
+const props = defineProps({
+  model: {
+    type: Object as PropType<IDynamicFormObject>,
+    default: null
+  },
+  options: {
+    type: Object as PropType<IDynamicFormOptions>,
+    default: null
+  },
+  name: {
+    type: String,
+    default: ''
+  }
+});
+const finalModel = computed(() => {
+  if (typeof props.model !== 'object')
+    return {};
+  return props.model;
+});
+</script>

+ 5 - 5
src/pages/dig/admin/volunteer.vue

@@ -37,7 +37,7 @@ import Height from '@/components/layout/space/Height.vue';
 import VillageApi, { VolunteerInfo } from '@/api/inhert/VillageApi';
 import CommonContent from '@/api/CommonContent';
 import type { RuleItem } from 'async-validator';
-import type { FormDefine, FormExport, IFormItemCallbackAdditionalProps } from '@/components/dynamic';
+import type { IDynamicFormOptions, IDynamicFormRef, IDynamicFormItemCallbackAdditionalProps } from '@/components/dynamic';
 import type { UploaderFieldProps } from '@/components/form/UploaderField.vue';
 import type { FieldProps } from '@/components/form/Field.vue';
 import type { PickerIdFieldProps } from '@/components/dynamic/wrappers/PickerIdField';
@@ -46,8 +46,8 @@ import type { CheckBoxListProps } from '@/components/dynamic/wrappers/CheckBoxLi
 
 const loading = ref(false);
 
-const formRef = ref<FormExport>();
-const formDefine : FormDefine = {
+const formRef = ref<IDynamicFormRef>();
+const formDefine : IDynamicFormOptions = {
   items: [
     { 
       label: '用户名', name: 'username', type: 'text',
@@ -101,7 +101,7 @@ const formDefine : FormDefine = {
         placeholder: '请选择区域',
         disabled: { callback: () => !isNew.value },
         loadData: async () => (await CommonContent.getCategoryList(1)).map(p => ({ text: p.title, value: p.id, raw: p }))
-      } as IFormItemCallbackAdditionalProps<PickerIdFieldProps>,
+      } as IDynamicFormItemCallbackAdditionalProps<PickerIdFieldProps>,
       rules: [{ required: true, message: '请选择区域' }],
     },
     { 
@@ -110,7 +110,7 @@ const formDefine : FormDefine = {
         placeholder: '请选择所属村庄',
         disabled: { callback: () => !isNew.value },
         loadData: async () => (await VillageApi.getClaimedVallageList()).map(p => ({ text: p.title, value: p.id, raw: p })),
-      } as IFormItemCallbackAdditionalProps<PickerIdFieldProps>,
+      } as IDynamicFormItemCallbackAdditionalProps<PickerIdFieldProps>,
       rules: [{ required: true, message: '请选择所属村庄' }],
     },
     {

+ 3 - 3
src/pages/dig/forms/common.vue

@@ -34,7 +34,7 @@ import LoadingPage from '@/components/display/loading/LoadingPage.vue';
 import Button from '@/components/basic/Button.vue';
 import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
-import type { FormDefine, FormExport } from '@/components/dynamic';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
 import Height from '@/components/layout/space/Height.vue';
 import { backAndCallOnPageBack } from '@/components/utils/PageAction';
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
@@ -42,8 +42,8 @@ import XBarSpace from '@/components/layout/space/XBarSpace.vue';
 const loading = ref(false);
 const subTitle = ref('');
 
-const formRef = ref<FormExport>();
-const formDefine = ref<FormDefine>();
+const formRef = ref<IDynamicFormRef>();
+const formDefine = ref<IDynamicFormOptions>();
 
 async function submit() {
   if (!formRef.value)

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 254 - 255
src/pages/dig/forms/forms.ts