From 51e287c7474a8786b40a2df0dda667a1ce0681ba Mon Sep 17 00:00:00 2001 From: juetan Date: Mon, 13 Nov 2023 22:01:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/form/components/Form.tsx | 79 +++++++++ src/components/form/components/FormItem.tsx | 143 ++++++++++++++++ src/components/form/components/FormModal.tsx | 0 src/components/form/components/FormNode.tsx | 16 ++ src/components/form/components/types/Form.ts | 110 +++++++++++++ src/components/form/core/interface.ts | 13 +- src/components/form/core/useFormItems.ts | 4 +- src/components/form/core/useFormSubmit.ts | 6 +- src/components/form/form-item.tsx | 162 +++++++++---------- src/components/form/form-node.tsx | 4 +- src/components/form/form-rules.ts | 54 +------ src/components/form/form.tsx | 30 ++-- src/components/form/hooks/types/FormItem.ts | 4 +- src/components/form/hooks/useForm.ts | 19 +-- src/components/form/hooks/useItems.ts | 28 ++-- src/components/form/hooks/useModel.ts | 2 +- src/components/form/hooks/useRules.ts | 4 +- src/components/form/nodes/Cascader.tsx | 14 ++ src/components/form/nodes/Custom.tsx | 6 + src/components/form/nodes/Date.tsx | 11 ++ src/components/form/nodes/Input.tsx | 11 ++ src/components/form/nodes/Number.tsx | 12 ++ src/components/form/nodes/Password.tsx | 10 ++ src/components/form/nodes/Search.tsx | 11 ++ src/components/form/nodes/Select.tsx | 15 ++ src/components/form/nodes/Submit.tsx | 17 ++ src/components/form/nodes/Textarea.tsx | 11 ++ src/components/form/nodes/Time.tsx | 10 ++ src/components/form/nodes/TreeSelect.tsx | 15 ++ src/components/form/nodes/index.ts | 42 +++++ src/components/form/use-form.tsx | 45 ++++-- src/components/form/util.ts | 20 +-- src/components/form/utils/strOrFnRender.ts | 9 ++ src/components/table/table.tsx | 48 +++--- src/pages/_layout/index.vue | 4 +- src/pages/home/home.vue | 35 ++-- src/types/auto-component.d.ts | 3 - 37 files changed, 754 insertions(+), 273 deletions(-) create mode 100644 src/components/form/components/Form.tsx create mode 100644 src/components/form/components/FormItem.tsx create mode 100644 src/components/form/components/FormModal.tsx create mode 100644 src/components/form/components/FormNode.tsx create mode 100644 src/components/form/components/types/Form.ts create mode 100644 src/components/form/nodes/Cascader.tsx create mode 100644 src/components/form/nodes/Custom.tsx create mode 100644 src/components/form/nodes/Date.tsx create mode 100644 src/components/form/nodes/Input.tsx create mode 100644 src/components/form/nodes/Number.tsx create mode 100644 src/components/form/nodes/Password.tsx create mode 100644 src/components/form/nodes/Search.tsx create mode 100644 src/components/form/nodes/Select.tsx create mode 100644 src/components/form/nodes/Submit.tsx create mode 100644 src/components/form/nodes/Textarea.tsx create mode 100644 src/components/form/nodes/Time.tsx create mode 100644 src/components/form/nodes/TreeSelect.tsx create mode 100644 src/components/form/nodes/index.ts create mode 100644 src/components/form/utils/strOrFnRender.ts diff --git a/src/components/form/components/Form.tsx b/src/components/form/components/Form.tsx new file mode 100644 index 0000000..9c9e23a --- /dev/null +++ b/src/components/form/components/Form.tsx @@ -0,0 +1,79 @@ +import { Form as BaseForm, FormInstance as BaseFormInstance } from "@arco-design/web-vue"; +import { PropType } from "vue"; +import { FormContextKey } from "../core/interface"; +import { useFormItems } from "../core/useFormItems"; +import { useFormModel } from "../core/useFormModel"; +import { useFormRef } from "../core/useFormRef"; +import { useFormSubmit } from "../core/useFormSubmit"; +import { AnFormItem, IAnFormItem } from "./FormItem"; +import { SubmitFn } from "./types/Form"; + +/** + * 表单组件 + */ +export const AnForm = defineComponent({ + name: "AnForm", + props: { + /** + * 表单数据 + */ + model: { + type: Object as PropType, + required: true, + }, + /** + * 表单项 + */ + items: { + type: Array as PropType, + default: () => [], + }, + /** + * 提交表单 + */ + submit: { + type: Function as PropType, + }, + /** + * 传给Form组件的参数 + */ + formProps: { + type: Object as PropType>, + }, + }, + setup(props, { slots }) { + const model = computed(() => props.model); + const items = computed(() => props.items); + const submit = computed(() => props.submit); + const formRefes = useFormRef(); + const formModel = useFormModel(model); + const formItems = useFormItems(items, model); + const formSubmit = useFormSubmit({ items, model, validate: formRefes.validate, submit }); + + const context = { + slots, + ...formModel, + ...formItems, + ...formRefes, + ...formSubmit, + }; + + provide(FormContextKey, context); + return context; + }, + render() { + return ( + + {this.items.map((item) => ( + + ))} + + ); + }, +}); + +export type AnFormInstance = InstanceType; + +export type AnFormProps = AnFormInstance["$props"]; + +export type IAnForm = Pick; diff --git a/src/components/form/components/FormItem.tsx b/src/components/form/components/FormItem.tsx new file mode 100644 index 0000000..92fe79e --- /dev/null +++ b/src/components/form/components/FormItem.tsx @@ -0,0 +1,143 @@ +import { FormItem as BaseFormItem, FormItemInstance } from "@arco-design/web-vue"; +import { isFunction } from "lodash-es"; +import { PropType } from "vue"; +import { FieldObjectRule } from "../hooks/useRules"; +import { NodeType, NodeUnion, nodeMap } from "../nodes"; +import { strOrFnRender } from "../utils/strOrFnRender"; + +/** + * 表单项 + */ +export const AnFormItem = defineComponent({ + name: "AnFormItem", + props: { + /** + * 表单项 + */ + item: { + type: Object as PropType, + required: true, + }, + /** + * 表单项数组 + */ + items: { + type: Array as PropType, + required: true, + }, + /** + * 表单数据 + */ + model: { + type: Object as PropType, + required: true, + }, + }, + setup(props) { + /** + * 校验规则 + */ + const rules = computed(() => props.item.rules?.filter((i) => !i.disable?.(props))); + + /** + * 是否禁用 + */ + const disabled = computed(() => Boolean(props.item.disable?.(props))); + + /** + * 渲染函数 + */ + const render = () => { + let render = (props.item as any).render; + if (!render) { + return null; + } + if (typeof render === "string") { + render = nodeMap[render as NodeType]?.render; + if (!render) { + return null; + } + return ; + } + if (isFunction(render)) { + return ; + } + }; + + /** + * 标签渲染 + */ + const label = strOrFnRender(props.item.label, props); + + /** + * 帮助信息渲染函数 + */ + const help = strOrFnRender(props.item.help, props); + + /** + * 额外信息渲染函数 + */ + const extra = strOrFnRender(props.item.extra, props); + + return () => { + if (props.item.visible && !props.item.visible(props)) { + return null; + } + return ( + + {{ default: render, label, help, extra }} + + ); + }; + }, +}); + +type FormItemFnArg = { + item: T; + items: T[]; + model: Record; +}; + +type FormItemBase = { + /** + * 字段名,用于表单、校验和输入框绑定,支持特殊语法。 + */ + field: string; + + /** + * 传递给`FormItem`组件的参数 + */ + itemProps?: Partial>; + + /** + * 校验规则数组 + */ + rules?: FieldObjectRule[]; + + /** + * 是否可见 + */ + visible?: (arg: FormItemFnArg) => boolean; + + /** + * 是否禁用 + */ + disable?: (arg: FormItemFnArg) => boolean; + + /** + * 标签名 + */ + label?: string | ((args: FormItemFnArg) => any); + + /** + * 帮助提示 + */ + help?: string | ((args: FormItemFnArg) => any); + + /** + * 额外内容 + */ + extra?: string | ((args: FormItemFnArg) => any); +}; + +export type IAnFormItem = FormItemBase & NodeUnion; diff --git a/src/components/form/components/FormModal.tsx b/src/components/form/components/FormModal.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/form/components/FormNode.tsx b/src/components/form/components/FormNode.tsx new file mode 100644 index 0000000..edf2b03 --- /dev/null +++ b/src/components/form/components/FormNode.tsx @@ -0,0 +1,16 @@ +import { nodeMap, NodeMap, NodeType } from "../nodes"; + +type NodeUnion = { + [key in NodeType]: Partial< + NodeMap[key] & { + /** + * 组件类型 + */ + type: key; + } + >; +}[NodeType]; + +export { nodeMap }; +export type { NodeMap, NodeType, NodeUnion }; + diff --git a/src/components/form/components/types/Form.ts b/src/components/form/components/types/Form.ts new file mode 100644 index 0000000..7e32b1c --- /dev/null +++ b/src/components/form/components/types/Form.ts @@ -0,0 +1,110 @@ +import { FieldRule, FormInstance, FormItemInstance, SelectOptionData } from "@arco-design/web-vue"; +import { InjectionKey, Ref } from "vue"; +import { NodeUnion } from "../../nodes"; + +/** + * 函数参数 + */ +export type FormItemFnArg = { + item: T; + items: T[]; + model: Recordable; +}; + +/** + * 表单项基础 + */ +type BaseFormItem = { + /** + * 传递给`FormItem`组件的参数 + * @description 部分属性会不可用,如field、label、required、rules、disabled等 + */ + itemProps: Omit; + + /** + * 是否可见 + * @description 动态控制表单项是否可见 + */ + visible?: (arg: FormItemFnArg) => boolean; + + /** + * 是否禁用 + * @description 动态控制表单项是否禁用 + */ + disable?: (arg: FormItemFnArg) => boolean; + + /** + * 选项,数组或者函数 + * @description 用于下拉框、单选框、多选框等组件, 支持动态加载 + */ + options?: SelectOptionData[] | ((arg: FormItemFnArg) => PromiseLike); +}; + +/** + * 表单项插槽 + */ +type BaseFormItemSlots = { + /** + * 渲染函数 + * @description 用于自定义表单项内容 + */ + render: (args: FormItemFnArg) => any; + + /** + * 标签名 + * @description 同FormItem组件的label属性 + */ + label?: string | ((args: FormItemFnArg) => any); + + /** + * 帮助提示 + * @description 同FormItem组件的help插槽 + */ + help?: string | ((args: FormItemFnArg) => any); + + /** + * 额外内容 + * @description 同FormItem组件的extra插槽 + */ + extra?: string | ((args: FormItemFnArg) => any); +}; + +/** + * 表单项校验 + */ +type BaseFormItemRules = { + /** + * 校验规则 + * @description 支持字符串(内置)、对象形式 + */ + rules?: FieldRule[]; +}; + +/** + * 表单项数据 + */ +type BaseFormItemModel = { + /** + * 字段名,特殊语法在提交时会自动转换。 + * @example + * ```typescript + * '[v1,v2]' => { v1: 1, v2: 2 } + * ``` + */ + field: string; +}; + +/** + * 表单项 + */ +export type AppFormItem = BaseFormItem & BaseFormItemModel & BaseFormItemRules & BaseFormItemSlots & NodeUnion; + +export type SubmitFn = (arg: { model: Recordable; items: AppFormItem[] }) => PromiseLike; + +interface FormContext { + loading: Ref; + formRef: Ref; + submitForm: () => PromiseLike; +} + +export const FormKey = Symbol("AppnifyForm") as InjectionKey; diff --git a/src/components/form/core/interface.ts b/src/components/form/core/interface.ts index 3a949d6..16f9516 100644 --- a/src/components/form/core/interface.ts +++ b/src/components/form/core/interface.ts @@ -1,9 +1,14 @@ import { InjectionKey } from "vue"; -import { FormRef } from "./useFormRef"; -import { FormSubmit } from "./useFormSubmit"; import { FormItems } from "./useFormItems"; import { FormModel } from "./useFormModel"; +import { FormRef } from "./useFormRef"; +import { FormSubmit } from "./useFormSubmit"; -export type FormContextInterface = FormModel & FormItems & FormRef & FormSubmit; +export type FormContextInterface = FormModel & + FormItems & + FormRef & + FormSubmit & { + slots: Recordable; + }; -export const FormContext = Symbol("FormKey") as InjectionKey; +export const FormContextKey = Symbol("FormKey") as InjectionKey; diff --git a/src/components/form/core/useFormItems.ts b/src/components/form/core/useFormItems.ts index fa4da7d..51ecc5a 100644 --- a/src/components/form/core/useFormItems.ts +++ b/src/components/form/core/useFormItems.ts @@ -1,7 +1,7 @@ import { Ref } from "vue"; -import { FormItem } from "../hooks/types/FormItem"; +import { IAnFormItem } from "../components/FormItem"; -export function useFormItems(items: Ref, model: Ref) { +export function useFormItems(items: Ref, model: Ref) { const getItem = (field: string) => { return items.value.find((i) => i.field === field); }; diff --git a/src/components/form/core/useFormSubmit.ts b/src/components/form/core/useFormSubmit.ts index 5fc68b6..bb57dc1 100644 --- a/src/components/form/core/useFormSubmit.ts +++ b/src/components/form/core/useFormSubmit.ts @@ -1,9 +1,9 @@ -import { Ref } from "vue"; -import { FormItem } from "../hooks/types/FormItem"; import { FormInstance, Message } from "@arco-design/web-vue"; +import { Ref } from "vue"; +import { IFormItem } from "../components/FormItem"; interface Options { - items: Ref; + items: Ref; model: Ref; submit: Ref; validate: FormInstance["validate"]; diff --git a/src/components/form/form-item.tsx b/src/components/form/form-item.tsx index 49e1b09..3742c3a 100644 --- a/src/components/form/form-item.tsx +++ b/src/components/form/form-item.tsx @@ -1,90 +1,80 @@ -import { FormItem as BaseFormItem, FormItemInstance, SelectOptionData } from "@arco-design/web-vue"; -import { NodeUnion, nodeMap } from "./form-node"; -import { FieldObjectRule, FieldRuleMap, Rule } from "./form-rules"; -import { PropType } from "vue"; -import { strOrFnRender } from "./util"; +import { FormItem as BaseFormItem, FieldRule, FormItemInstance, SelectOptionData } from "@arco-design/web-vue"; +import { NodeType, NodeUnion, nodeMap } from "./form-node"; +import { RuleMap } from "./form-rules"; + +export type FieldStringRule = keyof typeof RuleMap; +export type FieldObjectRule = FieldRule & { + disable?: (arg: { item: IFormItem; model: Record }) => boolean; +}; +export type FieldRuleType = FieldStringRule | FieldObjectRule; /** * 表单项 */ -export const FormItem = defineComponent({ - name: "AppnifyFormItem", - props: { - /** - * 表单项 - */ - item: { - type: Object as PropType, - required: true, - }, - /** - * 表单项数组 - */ - items: { - type: Array as PropType, - required: true, - }, - /** - * 表单数据 - */ - model: { - type: Object as PropType, - required: true, - }, - }, - setup(props) { - /** - * 校验规则 - */ - const rules = computed(() => props.item.rules?.filter((i) => !i.disable?.(props))); +export const FormItem = (props: any, { emit }: any) => { + const { item } = props; + const args = { + ...props, + field: item.field, + }; - /** - * 是否禁用 - */ - const disabled = computed(() => Boolean(props.item.disable?.(props))); - - if (props.item.visible && !props.item.visible(props as any)) { - return null; + const rules = computed(() => { + const result = []; + if (item.required) { + result.push(RuleMap.required); } - - /** - * 渲染函数 - */ - const render = () => { - const Item = props.item.component as any; - if (props.item.type === "custom") { - return ; + item.rules?.forEach((rule: any) => { + if (typeof rule === "string") { + result.push(RuleMap[rule as FieldStringRule]); + return; } - return ; - }; + if (!rule.disable) { + result.push(rule); + return; + } + if (!rule.disable({ model: props.model, item, items: props.items })) { + result.push(rule); + } + }); + return result; + }); - /** - * 标签渲染 - */ - const label = strOrFnRender(props.item.label, props); + const disabled = computed(() => { + if (item.disable === undefined) { + return false; + } + if (typeof item.disable === "function") { + return item.disable(args); + } + return item.disable; + }); - /** - * 帮助信息渲染函数 - */ - const help = strOrFnRender(props.item.help, props); + if (item.visible && !item.visible(args)) { + return null; + } - /** - * 额外信息渲染函数 - */ - const extra = strOrFnRender(props.item.extra, props); - - return () => ( - - {{ default: render, label, help, extra }} - - ); - }, -}); - -type FormItemFnArg = { - item: T; - items: T[]; - model: Record; + return ( + + {{ + default: () => { + if (item.component) { + return ; + } + const comp = nodeMap[item.type as NodeType]?.component; + if (!comp) { + return null; + } + if (item.type === "submit") { + return emit("submit")} onCancel={emit("cancel")} />; + } + return ; + }, + label: item.label && (() => (typeof item.label === "string" ? item.label : item.label?.(args))), + help: item.help && (() => (typeof item.help === "string" ? item.help : item.help?.(args))), + extra: item.extra && (() => (typeof item.extra === "string" ? item.extra : item.extra?.(args))), + }} + + ); }; type FormItemBase = { @@ -115,7 +105,7 @@ type FormItemBase = { * 标签名 * @description 同FormItem组件的label属性 */ - label?: string | ((args: FormItemFnArg) => any); + label?: string | ((args: { item: IFormItem; model: Record }) => any); /** * 传递给`FormItem`组件的参数 @@ -146,45 +136,45 @@ type FormItemBase = { *``` * @see https://arco.design/vue/component/form#FieldRule */ - rules?: FieldObjectRule[]; + rules?: FieldRuleType[]; /** * 是否可见 * @description 动态控制表单项是否可见 */ - visible?: (arg: FormItemFnArg) => boolean; + visible?: (arg: { item: IFormItem; model: Record }) => boolean; /** * 是否禁用 * @description 动态控制表单项是否禁用 */ - disable?: (arg: FormItemFnArg) => boolean; + disable?: (arg: { item: IFormItem; model: Record }) => boolean; /** * 选项,数组或者函数 * @description 用于下拉框、单选框、多选框等组件, 支持动态加载 */ - options?: SelectOptionData[] | ((arg: FormItemFnArg) => Promise); + options?: SelectOptionData[] | ((arg: { item: IFormItem; model: Record }) => Promise); /** * 表单项内容的渲染函数 * @description 用于自定义表单项内容 */ - component?: (args: FormItemFnArg) => any; + component?: (args: { item: IFormItem; model: Record; field: string }) => any; /** * 帮助提示 * @description 同FormItem组件的help插槽 * @see https://arco.design/vue/component/form#form-item%20Slots */ - help?: string | ((args: FormItemFnArg) => any); + help?: string | ((args: { item: IFormItem; model: Record }) => any); /** * 额外内容 * @description 同FormItem组件的extra插槽 * @see https://arco.design/vue/component/form#form-item%20Slots */ - extra?: string | ((args: FormItemFnArg) => any); + extra?: string | ((args: { item: IFormItem; model: Record }) => any); }; -export type IFormItem = FormItemBase & NodeUnion; +export type IFormItem = FormItemBase & NodeUnion; \ No newline at end of file diff --git a/src/components/form/form-node.tsx b/src/components/form/form-node.tsx index 990d844..b4d39c4 100644 --- a/src/components/form/form-node.tsx +++ b/src/components/form/form-node.tsx @@ -224,10 +224,10 @@ export type NodeUnion = { /** * 输入框类型,默认为`input` */ - type?: key; + type: key; /** * 传递给`type`属性对应组件的参数 */ nodeProps?: NodeMap[key]["nodeProps"]; }; -}[NodeType]; +}[NodeType]; \ No newline at end of file diff --git a/src/components/form/form-rules.ts b/src/components/form/form-rules.ts index 4ef0a4f..4fec365 100644 --- a/src/components/form/form-rules.ts +++ b/src/components/form/form-rules.ts @@ -1,10 +1,8 @@ import { FieldRule } from "@arco-design/web-vue"; -import { isString } from "lodash-es"; -/** - * 内置规则 - */ -export const FieldRuleMap = defineRuleMap({ +const defineRuleMap = >(ruleMap: T) => ruleMap; + +export const RuleMap = defineRuleMap({ required: { required: true, message: "该项不能为空", @@ -45,48 +43,4 @@ export const FieldRuleMap = defineRuleMap({ match: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/, message: "至少包含大写字母、小写字母、数字和特殊字符", }, -}); - -/** - * 字符串形式(枚举) - */ -export type FieldStringRule = keyof typeof FieldRuleMap; - -/** - * 对象形式 - */ -export type FieldObjectRule = FieldRule & { - disable?: (arg: { item: T; model: Record }) => boolean; -}; - -/** - * 完整类型 - */ -export type Rule = FieldStringRule | FieldObjectRule; - -/** - * 助手函数(获得TS提示) - */ -function defineRuleMap>(ruleMap: T) { - return ruleMap; -} - -/** - * 获取表单规则 - * @param item 表单项 - * @returns - */ -export const useFieldRules = [] }>(item: T) => { - const rules: FieldObjectRule[] = []; - if (item.required) { - rules.push(FieldRuleMap.required); - } - for (const rule of item.rules ?? []) { - if (isString(rule)) { - rules.push(FieldRuleMap[rule]); - } else { - rules.push(rule); - } - } - return rules; -}; +}); \ No newline at end of file diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index e4aa3b9..49b8fac 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -1,9 +1,9 @@ import { Form as BaseForm, FormInstance as BaseFormInstance, Message } from "@arco-design/web-vue"; -import { assign, cloneDeep, defaultsDeep, merge } from "lodash-es"; +import { assign, cloneDeep, defaultsDeep } from "lodash-es"; import { PropType } from "vue"; -import { FormItem, IFormItem } from "./form-item"; -import { nodeMap } from "./form-node"; import { config } from "./form-config"; +import { FormItem, IFormItem } from "./form-item"; +import { NodeType, nodeMap } from "./form-node"; type SubmitFn = (arg: { model: Record; items: IFormItem[] }) => Promise; @@ -11,13 +11,13 @@ type SubmitFn = (arg: { model: Record; items: IFormItem[] }) => Pro * 表单组件 */ export const Form = defineComponent({ - name: "AppnifyForm", + name: "Form", props: { /** * 表单数据 */ model: { - type: Object as PropType, + type: Object as PropType>, default: () => reactive({}), }, /** @@ -45,11 +45,11 @@ export const Form = defineComponent({ const formRef = ref>(); const loading = ref(false); - for (const item of props.items) { - const node = nodeMap[item.type] as any; + props.items.forEach((item: any) => { + const node = nodeMap[item.type as NodeType]; defaultsDeep(item, { nodeProps: node?.nodeProps ?? {} }); (node as any)?.init?.({ item, model: props.model }); - } + }); const getItem = (field: string) => { return props.items.find((item) => item.field === field); @@ -64,7 +64,7 @@ export const Form = defineComponent({ }; const resetModel = () => { - assign(props.model, merge({}, model)); + assign(props.model, model); }; const submitForm = async () => { @@ -84,7 +84,7 @@ export const Form = defineComponent({ } }; - const injected = { + return { formRef, loading, getItem, @@ -93,10 +93,6 @@ export const Form = defineComponent({ setModel, getModel, }; - - provide("form1", injected); - - return injected; }, render() { (this.items as any).instance = this; @@ -108,9 +104,9 @@ export const Form = defineComponent({ }; return ( - + {this.items.map((item) => ( - + ))} ); @@ -121,4 +117,4 @@ export type FormInstance = InstanceType; export type FormProps = FormInstance["$props"]; -export type FormDefinedProps = Pick; +export type FormDefinedProps = Pick; \ No newline at end of file diff --git a/src/components/form/hooks/types/FormItem.ts b/src/components/form/hooks/types/FormItem.ts index 4ad4b6c..85028ae 100644 --- a/src/components/form/hooks/types/FormItem.ts +++ b/src/components/form/hooks/types/FormItem.ts @@ -1,6 +1,6 @@ import { FormItemInstance, SelectOptionData } from "@arco-design/web-vue"; +import { NodeType, NodeUnion } from "../../nodes"; import { Rule } from "../useRules"; -import { NodeUnion } from "../../form-node"; /** * 函数参数 @@ -48,7 +48,7 @@ type BaseFormItemSlots = { * 渲染函数 * @description 用于自定义表单项内容 */ - render?: (args: FormItemFnArg) => any; + render?: NodeType | ((args: FormItemFnArg) => any); /** * 标签名 diff --git a/src/components/form/hooks/useForm.ts b/src/components/form/hooks/useForm.ts index 176ab17..e7bb3b6 100644 --- a/src/components/form/hooks/useForm.ts +++ b/src/components/form/hooks/useForm.ts @@ -1,22 +1,19 @@ -import { useModel } from "./useModel"; -import { useItems } from "./useItems"; -import { UseOptions } from "./interface"; import { UseForm } from "./types/Form"; +import { useItems } from "./useItems"; /** * 构建表单组件的参数 */ -export const useForm = (options: T) => { - const initModel = options.model ?? {}; - const { items, updateItemOptions } = useItems(options.items ?? [], initModel, Boolean(options.submit)); - const { model, resetModel, setModel, getModel } = useModel(initModel); +export const useForm = (options: UseForm) => { + const { model: _model = {}, items: _items = [], submit, formProps: _formProps } = options; + const items = ref(useItems(_items, _model, Boolean(options.submit))) + const model = ref(_model); + const formProps = ref(_formProps); return { model, items, - resetModel, - setModel, - getModel, - updateItemOptions, + submit, + formProps, }; }; diff --git a/src/components/form/hooks/useItems.ts b/src/components/form/hooks/useItems.ts index e63015a..0a7abd9 100644 --- a/src/components/form/hooks/useItems.ts +++ b/src/components/form/hooks/useItems.ts @@ -1,28 +1,28 @@ import { merge } from "lodash-es"; +import { nodeMap } from "../nodes"; import { FormItem } from "./types/FormItem"; -import { nodeMap } from "../form-node"; import { useRules } from "./useRules"; const ITEM: Partial = { - type: "input", + render: "input", }; const SUBMIT_ITEM: FormItem = { field: "id", - type: "submit", + render: "submit", itemProps: { hideLabel: true, }, }; export function useItems(list: FormItem[], model: Recordable, submit: boolean) { - const items = ref([]); + const items = []; let hasSubmit = false; for (const item of list) { - let target: Recordable = merge({}, nodeMap[item.type ?? "input"]); + let target: Recordable = merge({}, nodeMap[typeof item.render === "string" ? item.render : "input"]); - if (item.type === "submit") { + if (item.render === "submit") { target = merge(item, SUBMIT_ITEM); hasSubmit = true; } @@ -31,22 +31,12 @@ export function useItems(list: FormItem[], model: Recordable, submit: boolean) { target.rules = useRules(item); model[item.field] = model[item.field] ?? item.initial; - items.value.push(target as any); + items.push(target as any); } if (submit && !hasSubmit) { - items.value.push(merge({}, SUBMIT_ITEM)); + items.push(merge({}, SUBMIT_ITEM)); } - const updateItemOptions = (field: string) => { - const item = items.value.find((i) => i.field === field); - if (item) { - (item as any)._updateOptions?.(); - } - }; - - return { - items, - updateItemOptions, - }; + return items; } diff --git a/src/components/form/hooks/useModel.ts b/src/components/form/hooks/useModel.ts index cbde7da..3e1c5f2 100644 --- a/src/components/form/hooks/useModel.ts +++ b/src/components/form/hooks/useModel.ts @@ -1,6 +1,6 @@ import { cloneDeep } from "lodash-es"; -function formatModel(model: Recordable) { +export function formatModel(model: Recordable) { const data: Recordable = {}; for (const [key, val] of Object.entries(model)) { // 数组类型 diff --git a/src/components/form/hooks/useRules.ts b/src/components/form/hooks/useRules.ts index 6182148..e4f2912 100644 --- a/src/components/form/hooks/useRules.ts +++ b/src/components/form/hooks/useRules.ts @@ -83,7 +83,9 @@ export const useRules = [] }>( } for (const rule of item.rules ?? []) { if (isString(rule)) { - rules.push(FieldRuleMap[rule]); + if (FieldRuleMap[rule]) { + rules.push(FieldRuleMap[rule]); + } } else { rules.push(rule); } diff --git a/src/components/form/nodes/Cascader.tsx b/src/components/form/nodes/Cascader.tsx new file mode 100644 index 0000000..ee6f8a9 --- /dev/null +++ b/src/components/form/nodes/Cascader.tsx @@ -0,0 +1,14 @@ +import { Cascader, CascaderInstance } from "@arco-design/web-vue"; +import { initOptions } from "../form-config"; + +type Props = CascaderInstance["$props"]; + +export default { + render: Cascader, + init: initOptions, + nodeProps: { + placeholder: "请选择", + allowClear: true, + expandTrigger: "hover", + } as Props, +}; diff --git a/src/components/form/nodes/Custom.tsx b/src/components/form/nodes/Custom.tsx new file mode 100644 index 0000000..8db05e3 --- /dev/null +++ b/src/components/form/nodes/Custom.tsx @@ -0,0 +1,6 @@ +export default { + render: () => { + return "1"; + }, + nodeProps: {}, +}; diff --git a/src/components/form/nodes/Date.tsx b/src/components/form/nodes/Date.tsx new file mode 100644 index 0000000..5fbe565 --- /dev/null +++ b/src/components/form/nodes/Date.tsx @@ -0,0 +1,11 @@ +import { DatePicker, DatePickerInstance } from "@arco-design/web-vue"; + +type Props = DatePickerInstance["$props"]; + +export default { + render: DatePicker, + nodeProps: { + placeholder: "请输入", + allowClear: true, + } as Props, +}; diff --git a/src/components/form/nodes/Input.tsx b/src/components/form/nodes/Input.tsx new file mode 100644 index 0000000..7df4095 --- /dev/null +++ b/src/components/form/nodes/Input.tsx @@ -0,0 +1,11 @@ +import { Input, InputInstance } from "@arco-design/web-vue"; + +type Props = InputInstance["$props"]; + +export default { + render: Input, + nodeProps: { + placeholder: "请输入", + allowClear: true, + } as Props, +}; diff --git a/src/components/form/nodes/Number.tsx b/src/components/form/nodes/Number.tsx new file mode 100644 index 0000000..4659f5f --- /dev/null +++ b/src/components/form/nodes/Number.tsx @@ -0,0 +1,12 @@ +import { InputInstance, InputNumber, InputNumberInstance } from "@arco-design/web-vue"; + +type Props = InputInstance["$props"] & InputNumberInstance["$props"]; + +export default { + render: InputNumber, + nodeProps: { + placeholder: "请输入", + defaultValue: 0, + allowClear: true, + } as Props, +}; diff --git a/src/components/form/nodes/Password.tsx b/src/components/form/nodes/Password.tsx new file mode 100644 index 0000000..301027d --- /dev/null +++ b/src/components/form/nodes/Password.tsx @@ -0,0 +1,10 @@ +import { InputInstance, InputPassword, InputPasswordInstance } from "@arco-design/web-vue"; + +type Props = InputInstance["$props"] & InputPasswordInstance["$props"]; + +export default { + render: InputPassword, + nodeProps: { + placeholder: "请输入", + } as Props, +}; diff --git a/src/components/form/nodes/Search.tsx b/src/components/form/nodes/Search.tsx new file mode 100644 index 0000000..7cd7437 --- /dev/null +++ b/src/components/form/nodes/Search.tsx @@ -0,0 +1,11 @@ +import { InputInstance, InputSearch, InputSearchInstance } from "@arco-design/web-vue"; + +type Props = InputInstance["$props"] & InputSearchInstance["$props"]; + +export default { + render: InputSearch, + nodeProps: { + placeholder: "请输入", + allowClear: true, + } as Props, +}; diff --git a/src/components/form/nodes/Select.tsx b/src/components/form/nodes/Select.tsx new file mode 100644 index 0000000..097139b --- /dev/null +++ b/src/components/form/nodes/Select.tsx @@ -0,0 +1,15 @@ +import { Select, SelectInstance } from "@arco-design/web-vue"; +import { initOptions } from "../form-config"; + +type Props = SelectInstance["$props"]; + +export default { + render: Select, + init: initOptions, + nodeProps: { + placeholder: "请选择", + allowClear: true, + allowSearch: true, + options: [], + } as Props, +}; diff --git a/src/components/form/nodes/Submit.tsx b/src/components/form/nodes/Submit.tsx new file mode 100644 index 0000000..ae48d3b --- /dev/null +++ b/src/components/form/nodes/Submit.tsx @@ -0,0 +1,17 @@ +import { Button } from "@arco-design/web-vue"; + +export default { + render: (props: any, { emit }: any) => { + return ( + <> + + {/* */} + + ); + }, + nodeProps: {}, +}; diff --git a/src/components/form/nodes/Textarea.tsx b/src/components/form/nodes/Textarea.tsx new file mode 100644 index 0000000..0d29b38 --- /dev/null +++ b/src/components/form/nodes/Textarea.tsx @@ -0,0 +1,11 @@ +import { InputInstance, Textarea, TextareaInstance } from "@arco-design/web-vue"; + +type Props = InputInstance["$props"] & TextareaInstance["$props"]; + +export default { + render: Textarea, + nodeProps: { + placeholder: "请输入", + allowClear: true, + } as Props, +}; diff --git a/src/components/form/nodes/Time.tsx b/src/components/form/nodes/Time.tsx new file mode 100644 index 0000000..7a9418f --- /dev/null +++ b/src/components/form/nodes/Time.tsx @@ -0,0 +1,10 @@ +import { TimePicker, TimePickerInstance } from "@arco-design/web-vue"; + +type Props = TimePickerInstance["$props"]; + +export default { + render: TimePicker, + nodeProps: { + allowClear: true, + } as Props, +}; diff --git a/src/components/form/nodes/TreeSelect.tsx b/src/components/form/nodes/TreeSelect.tsx new file mode 100644 index 0000000..c791576 --- /dev/null +++ b/src/components/form/nodes/TreeSelect.tsx @@ -0,0 +1,15 @@ +import { TreeSelect, TreeSelectInstance } from "@arco-design/web-vue"; +import { initOptions } from "../form-config"; + +type Props = TreeSelectInstance["$props"]; + +export default { + render: TreeSelect, + init: (arg: any) => initOptions(arg, "data"), + nodeProps: { + placeholder: "请选择", + allowClear: true, + allowSearch: true, + options: [], + } as Props, +}; diff --git a/src/components/form/nodes/index.ts b/src/components/form/nodes/index.ts new file mode 100644 index 0000000..55003a7 --- /dev/null +++ b/src/components/form/nodes/index.ts @@ -0,0 +1,42 @@ +import cascader from "./Cascader"; +import custom from "./Custom"; +import date from "./Date"; +import input from "./Input"; +import number from "./Number"; +import password from "./Password"; +import search from "./Search"; +import select from "./Select"; +import submit from "./Submit"; +import textarea from "./Textarea"; +import time from "./Time"; +import treeSelect from "./TreeSelect"; + +export const nodeMap = { + input, + number, + search, + textarea, + select, + treeSelect, + time, + password, + cascader, + date, + submit, + custom, +}; + +export type NodeMap = typeof nodeMap; + +export type NodeType = keyof NodeMap; + +export type NodeUnion = { + [key in NodeType]: Partial< + Omit & { + /** + * 组件类型 + */ + render: key | ((...args: any[]) => any); + } + >; +}[NodeType]; diff --git a/src/components/form/use-form.tsx b/src/components/form/use-form.tsx index 3b1f570..3a18c86 100644 --- a/src/components/form/use-form.tsx +++ b/src/components/form/use-form.tsx @@ -1,7 +1,6 @@ import { FormInstance } from "@arco-design/web-vue"; +import { merge } from "lodash-es"; import { IFormItem } from "./form-item"; -import { useModel } from "./hooks/useModel"; -import { useItems } from "./hooks/useItems"; export type Options = { /** @@ -27,16 +26,34 @@ export type Options = { * @see src/components/form/use-form.tsx */ export const useForm = (options: Options) => { - const initModel = options.model ?? {}; - const { items, updateItemOptions } = useItems(options.items, initModel, Boolean(options.submit)); - const { model, resetModel, setModel, getModel } = useModel(initModel); + const { model: _model = {} } = options; + const model: Record = { id: undefined, ..._model }; + const items: IFormItem[] = []; - return { - model, - items, - resetModel, - setModel, - getModel, - updateItemOptions, - }; -}; + for (const item of options.items) { + if (!item.nodeProps) { + item.nodeProps = {} as any; + } + model[item.field] = model[item.field] ?? item.initial; + items.push(item); + } + + if (options.submit) { + const submit = items.find((item) => item.type === "submit") || {}; + items.push( + merge( + {}, + { + field: "id", + type: "submit", + itemProps: { + hideLabel: true, + }, + }, + submit + ) as any + ); + } + + return reactive({ ...options, model, items }) as any; +}; \ No newline at end of file diff --git a/src/components/form/util.ts b/src/components/form/util.ts index 25bcf36..dd08554 100644 --- a/src/components/form/util.ts +++ b/src/components/form/util.ts @@ -31,22 +31,4 @@ export function setModel(model: any, data: Record) { model[key] = data[key]; } } -} - -/** - * 字符串或函数渲染 - * @param value 值 - * @param arg 参数 - * @returns - */ -export function strOrFnRender(value?: string | Function, arg?: any) { - if (typeof value === "string") { - return () => value; - } - if (typeof value === "function") { - return () => value(arg); - } - return null; -} - -export const falsy = () => false; +} \ No newline at end of file diff --git a/src/components/form/utils/strOrFnRender.ts b/src/components/form/utils/strOrFnRender.ts new file mode 100644 index 0000000..efb845c --- /dev/null +++ b/src/components/form/utils/strOrFnRender.ts @@ -0,0 +1,9 @@ +export function strOrFnRender(fn: any, options: any) { + if (typeof fn === "string") { + return () => fn; + } + if (typeof fn === "function") { + return fn(options); + } + return null; +} diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index 933f3c6..0bc1e25 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -1,16 +1,11 @@ -import { - TableColumnData as BaseColumn, - TableData as BaseData, - Table as BaseTable, - PaginationProps, -} from "@arco-design/web-vue"; -import { cloneDeep, isBoolean, isObject, merge } from "lodash-es"; +import AniEmpty from "@/components/empty/AniEmpty.vue"; +import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue"; +import { merge } from "lodash-es"; import { PropType, computed, defineComponent, reactive, ref } from "vue"; -import AniEmpty from "../empty/AniEmpty.vue"; import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form"; import { config } from "./table.config"; -type DataFn = (search: Record, paging: { page: number; size: number }) => PromiseLike; +type DataFn = (search: Record, paging: { page: number; size: number }) => Promise; /** * 表格组件 @@ -37,7 +32,8 @@ export const Table = defineComponent({ * 分页参数配置 */ pagination: { - type: [Boolean, Object] as PropType, + type: Object as PropType, + default: () => reactive(config.pagination), }, /** * 搜索表单配置 @@ -77,7 +73,6 @@ export const Table = defineComponent({ const createRef = ref(); const modifyRef = ref(); const renderData = ref([]); - const paging = ref(merge({}, isObject(props.pagination) ? props.pagination : config.pagination)); const inlined = computed(() => (props.search?.items?.length ?? 0) <= config.searchInlineCount); const reloadData = () => loadData({ current: 1, pageSize: 10 }); const openModifyModal = (data: any) => modifyRef.value?.open(data); @@ -86,8 +81,9 @@ export const Table = defineComponent({ * 加载数据 * @param pagination 自定义分页 */ - const loadData = async (pagination: Partial = {}) => { - const { current: page = 1, pageSize: size = 10 } = { ...paging.value, ...pagination }; + const loadData = async (pagination: Partial = {}) => { + const merged = { ...props.pagination, ...pagination }; + const paging = { page: merged.current, size: merged.pageSize }; const model = searchRef.value?.getModel() ?? {}; // 本地加载 @@ -102,21 +98,21 @@ export const Table = defineComponent({ }); }); renderData.value = data; - paging.value.total = renderData.value.length; - paging.value.current = 1; + props.pagination.total = renderData.value.length; + props.pagination.current = 1; } // 远程加载 if (typeof props.data === "function") { try { loading.value = true; - const resData = await props.data(model, { page, size }); + const resData = await props.data(model, paging); const { data = [], total = 0 } = resData?.data || {}; renderData.value = data; - paging.value.total = total; - paging.value.current = page; + props.pagination.total = total; + props.pagination.current = paging.page; } catch (e) { - console.log(e); + // todo } finally { loading.value = false; } @@ -126,8 +122,8 @@ export const Table = defineComponent({ watchEffect(() => { if (Array.isArray(props.data)) { renderData.value = props.data; - paging.value.total = props.data.length; - paging.value.current = 1; + props.pagination.total = props.data.length; + props.pagination.current = 1; } }); @@ -147,7 +143,6 @@ export const Table = defineComponent({ createRef, modifyRef, renderData, - paging, loadData, reloadData, openModifyModal, @@ -182,10 +177,7 @@ export const Table = defineComponent({ )} {this.$slots.action?.()} -
- {this.inlined &&
} - {this.$slots.tool?.(this.renderData)} -
+
{this.inlined &&
}
this.loadData({ current })} @@ -218,4 +210,4 @@ export type TableInstance = InstanceType; /** * 表格组件参数 */ -export type TableProps = TableInstance["$props"]; +export type TableProps = TableInstance["$props"]; \ No newline at end of file diff --git a/src/pages/_layout/index.vue b/src/pages/_layout/index.vue index 7754f31..16e1852 100644 --- a/src/pages/_layout/index.vue +++ b/src/pages/_layout/index.vue @@ -61,7 +61,7 @@ - + @@ -73,11 +73,11 @@ + diff --git a/src/types/auto-component.d.ts b/src/types/auto-component.d.ts index 6d653e7..9e3165d 100644 --- a/src/types/auto-component.d.ts +++ b/src/types/auto-component.d.ts @@ -30,7 +30,6 @@ declare module '@vue/runtime-core' { AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview'] AInput: typeof import('@arco-design/web-vue')['Input'] AInputNumber: typeof import('@arco-design/web-vue')['InputNumber'] - AInputPassword: typeof import('@arco-design/web-vue')['InputPassword'] AInputSearch: typeof import('@arco-design/web-vue')['InputSearch'] ALayout: typeof import('@arco-design/web-vue')['Layout'] ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent'] @@ -51,7 +50,6 @@ declare module '@vue/runtime-core' { ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup'] AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar'] ASelect: typeof import('@arco-design/web-vue')['Select'] - ASpace: typeof import('@arco-design/web-vue')['Space'] ASpin: typeof import('@arco-design/web-vue')['Spin'] ASwitch: typeof import('@arco-design/web-vue')['Switch'] ATabPane: typeof import('@arco-design/web-vue')['TabPane'] @@ -68,7 +66,6 @@ declare module '@vue/runtime-core' { DragResizer: typeof import('./../components/editor/components/DragResizer.vue')['default'] Editor: typeof import('./../components/editor/components/Editor.vue')['default'] EditorPreview: typeof import('./../components/editor/components/EditorPreview.vue')['default'] - Empty: typeof import('./../components/empty/index.vue')['default'] ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default'] InputColor: typeof import('./../components/editor/components/InputColor.vue')['default'] InputImage: typeof import('./../components/editor/components/InputImage.vue')['default']