diff --git a/src/components/AnForm/components/Form.tsx b/src/components/AnForm/components/Form.tsx new file mode 100644 index 0000000..fd62ab8 --- /dev/null +++ b/src/components/AnForm/components/Form.tsx @@ -0,0 +1,77 @@ +import { Form, FormInstance } from "@arco-design/web-vue"; +import { PropType } from "vue"; +import { FormContextKey } from "../core/useFormContext"; +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 { useVModel } from "@vueuse/core"; + +/** + * 表单组件 + */ +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 IAnFormProps, + }, + }, + emits: ["update:model"], + setup(props, { slots, emit }) { + const model = useVModel(props, "model", emit); + const items = computed(() => props.items); + const submit = computed(() => props.submit); + const formRefes = useFormRef(); + const formModel = useFormModel(model, formRefes.clearValidate); + const formItems = useFormItems(items, model); + const formSubmit = useFormSubmit({ items, model, validate: formRefes.validate, submit }, formModel.getModel); + 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 IAnFormProps = PropType>; + +export type IAnForm = Pick; + +export type IAnFormSubmit = (model: Recordable, items: IAnFormItem) => any; diff --git a/src/components/AnForm/components/FormItem.tsx b/src/components/AnForm/components/FormItem.tsx new file mode 100644 index 0000000..2f686ac --- /dev/null +++ b/src/components/AnForm/components/FormItem.tsx @@ -0,0 +1,136 @@ +import { FormItem as BaseFormItem, FieldRule, FormItemInstance } from "@arco-design/web-vue"; +import { isFunction } from "lodash-es"; +import { PropType } from "vue"; +import { SetterItem, SetterType, setterMap } from "./FormSetter"; + +/** + * 表单项 + */ +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.model, props.item, props.items))); + const disabled = computed(() => Boolean(props.item.disable?.(props.model, props.item, props.items))); + const label = strOrFnRender(props.item.label, props); + const help = strOrFnRender(props.item.help, props); + const extra = strOrFnRender(props.item.extra, props); + + const render = () => { + let render = props.item.setter as any; + if (!render) { + return null; + } + if (typeof render === "string") { + render = setterMap[render as SetterType]?.render; + if (!render) { + return null; + } + return ; + } + if (isFunction(render)) { + return ; + } + }; + + return () => { + if (props.item.visible && !props.item.visible(props.model, props.item, props.items)) { + return null; + } + return ( + + {{ default: render, label, help, extra }} + + ); + }; + }, +}); + +export function strOrFnRender(fn: any, ...args: any[]) { + if (typeof fn === "string") { + return () => fn; + } + if (typeof fn === "function") { + return fn(...args); + } + return null; +} + +export type IAnFormItemBoolFn = (model: Recordable, item: IAnFormItem, items: IAnFormItem[]) => boolean; + +export type IAnFormItemElemFn = (model: Recordable, item: IAnFormItem, items: IAnFormItem[]) => any; + +export type IAnFormItemRule = FieldRule & { disable?: IAnFormItemBoolFn }; + +export type IAnFormItemBase = { + /** + * 字段名 + * @description 请保持唯一,支持特殊语法 + * @required + */ + field: string; + + /** + * 透传的表单项参数 + * @default null + */ + itemProps?: Partial>; + + /** + * 校验规则 + */ + rules?: IAnFormItemRule[]; + + /** + * 是否可见 + */ + visible?: IAnFormItemBoolFn; + + /** + * 是否禁用 + */ + disable?: IAnFormItemBoolFn; + + /** + * 标签名 + */ + label?: string | IAnFormItemElemFn; + + /** + * 帮助提示 + */ + help?: string | IAnFormItemElemFn; + + /** + * 额外内容 + */ + extra?: string | IAnFormItemElemFn; + + options?: any; + + init?: any; +}; + +export type IAnFormItem = IAnFormItemBase & SetterItem; diff --git a/src/components/AnForm/components/FormModal.tsx b/src/components/AnForm/components/FormModal.tsx new file mode 100644 index 0000000..d6c9594 --- /dev/null +++ b/src/components/AnForm/components/FormModal.tsx @@ -0,0 +1,141 @@ +import { Button, ButtonInstance, Form, FormInstance, Modal } from "@arco-design/web-vue"; +import { PropType } from "vue"; +import { FormContextKey } from "../core/useFormContext"; +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 { useVModel } from "@vueuse/core"; +import { AnForm, IAnFormProps, IAnFormSubmit } from "./Form"; + +/** + * 表单组件 + */ +export const AnFormModal = defineComponent({ + name: "AnFormModal", + props: { + /** + * 弹窗标题 + * @default '添加' + */ + title: { + type: [String, Function] as PropType, + default: "添加", + }, + /** + * 触发元素 + */ + trigger: { + type: [Boolean, Function, Object] as PropType, + default: true, + }, + /** + * 传递给Modal组件的props + */ + modalProps: { + type: Object as PropType, + }, + /** + * 表单数据 + */ + model: { + type: Object as PropType, + required: true, + }, + /** + * 表单项 + */ + items: { + type: Array as PropType, + default: () => [], + }, + /** + * 提交表单 + */ + submit: { + type: Function as PropType, + }, + /** + * 传给Form组件的参数 + */ + formProps: { + type: Object as IAnFormProps, + }, + }, + emits: ["update:model"], + setup(props, { slots, emit }) { + const visible = ref(false); + const formRef = ref | null>(null); + + const modalTitle = () => { + if (typeof props.title === "string") { + return props.title; + } + return ; + }; + + const modalTrigger = () => { + if (!props.trigger) { + return null; + } + if (typeof props.trigger === "boolean") { + return ; + } + if (typeof props.trigger === "object") { + return ( + + ); + } + return ; + }; + + return { + visible, + formRef, + modalTitle, + modalTrigger, + }; + }, + render() { + return ( + <> + + + {{ + title: this.modalTitle, + default: () => ( + this.$emit("update:model", v)} + items={this.items} + submit={this.submit} + formProps={this.formProps} + > + ), + ...this.$slots, + }} + + + ); + }, +}); + +type ModalProps = Omit["$props"], "visible" | "title" | "onBeforeOk">; + +type ModalType = string | ((model: Recordable, items: IAnFormItem[]) => any); + +type ModalTrigger = + | boolean + | ((model: Recordable, items: IAnFormItem[]) => any) + | { + text?: string; + buttonProps?: ButtonInstance["$props"]; + }; + +export type FormModalProps = Pick< + InstanceType["$props"], + "title" | "trigger" | "modalProps" | "model" | "items" | "submit" | "formProps" +>; diff --git a/src/components/AnForm/components/FormSetter.tsx b/src/components/AnForm/components/FormSetter.tsx new file mode 100644 index 0000000..0df10b8 --- /dev/null +++ b/src/components/AnForm/components/FormSetter.tsx @@ -0,0 +1,18 @@ +import setterMap from "../setters"; + +export type SetterMap = typeof setterMap; + +export type SetterType = keyof SetterMap; + +export type SetterItem = { + [key in SetterType]: Partial< + Omit & { + /** + * 组件类型 + */ + setter: key; + } + >; +}[SetterType]; + +export { setterMap }; \ No newline at end of file diff --git a/src/components/AnForm/core/useFormContext.ts b/src/components/AnForm/core/useFormContext.ts new file mode 100644 index 0000000..cbefe1a --- /dev/null +++ b/src/components/AnForm/core/useFormContext.ts @@ -0,0 +1,14 @@ +import { InjectionKey } from "vue"; +import { FormItems } from "./useFormItems"; +import { FormModel } from "./useFormModel"; +import { FormRef } from "./useFormRef"; +import { FormSubmit } from "./useFormSubmit"; + +export type FormContextInterface = FormModel & + FormItems & + FormRef & + FormSubmit & { + slots: Recordable; + }; + +export const FormContextKey = Symbol("FormContextKey") as InjectionKey; diff --git a/src/components/AnForm/core/useFormItems.ts b/src/components/AnForm/core/useFormItems.ts new file mode 100644 index 0000000..211d63d --- /dev/null +++ b/src/components/AnForm/core/useFormItems.ts @@ -0,0 +1,46 @@ +import { Ref } from "vue"; +import { IAnFormItem } from "../components/FormItem"; + +export function useFormItems(items: Ref, model: Ref) { + const getItem = (field: string) => { + return items.value.find((i) => i.field === field); + }; + + const getItemOptions = (field: string) => { + const item = getItem(field); + if (item) { + return (item.nodeProps as any)?.options; + } + }; + + const initItemOptions = (field: string) => { + const item = getItem(field); + item && item.init?.(); + }; + + const initItems = () => { + for (const item of items.value) { + item.init?.({ item, model: model.value }); + } + }; + + const initItem = (field: string) => { + const item = getItem(field); + item && item.init?.({ item, model: model.value }); + }; + + onMounted(() => { + initItems(); + }); + + return { + items, + getItem, + initItem, + initItems, + getItemOptions, + initItemOptions, + }; +} + +export type FormItems = ReturnType; diff --git a/src/components/AnForm/core/useFormModel.ts b/src/components/AnForm/core/useFormModel.ts new file mode 100644 index 0000000..290e5ba --- /dev/null +++ b/src/components/AnForm/core/useFormModel.ts @@ -0,0 +1,112 @@ +import { cloneDeep } from "lodash-es"; +import { Ref } from "vue"; + +/** + * 表单数据管理 + * @param initial 初始值 + * @returns + */ +export function useFormModel(model: Ref, clearValidate: any) { + const initial = cloneDeep(model.value); + + const resetModel = () => { + model.value = cloneDeep(initial); + clearValidate(); + }; + + const getInitialModel = () => { + return initial; + }; + + const setModel = (data: Recordable) => { + for (const key of Object.keys(model.value)) { + model.value[key] = data[key]; + } + }; + + const getModel = () => { + return formatModel(model.value); + }; + + return { + model, + getInitialModel, + resetModel, + setModel, + getModel, + }; +} + +export type FormModel = ReturnType; + +export function formatModel(model: Recordable) { + const data: Recordable = {}; + + for (const [key, value] of Object.entries(model)) { + if (/^\[.+\]$/.test(key)) { + formatModelArray(key, value, data); + continue; + } + if (/^\{.+\}$/.test(key)) { + formatModelObject(key, value, data); + continue; + } + data[key] = value; + } + + return data; +} + +function formatModelArray(key: string, value: any, data: Recordable) { + let field = key.replaceAll(/\s/g, ""); + field = field.match(/^\[(.+)\]$/)?.[1] ?? ""; + + if (!field) { + data[key] = value; + return; + } + + const keys = field.split(","); + keys.forEach((k, i) => { + if (/(.+)?:number$/.test(k)) { + k = k.replace(/:number$/, ""); + data[k] = value?.[i] && Number(value[i]); + return; + } + if (/(.+)?:boolean$/.test(k)) { + k = k.replace(/:boolean$/, ""); + data[k] = value?.[i] && Boolean(value[i]); + return; + } + data[k] = value?.[i]; + }); + + return data; +} + +function formatModelObject(key: string, value: any, data: Recordable) { + let field = key.replaceAll(/\s/g, ""); + field = field.match(/^\{(.+)\}$/)?.[1] ?? ""; + + if (!field) { + data[key] = value; + return; + } + + const keys = field.split(","); + keys.forEach((k, i) => { + if (/(.+)?:number$/.test(k)) { + k = k.replace(/:number$/, ""); + data[k] = value?.[i] && Number(value[i]); + return; + } + if (/(.+)?:boolean$/.test(k)) { + k = k.replace(/:boolean$/, ""); + data[k] = value?.[i] && Boolean(value[i]); + return; + } + data[k] = value?.[i]; + }); + + return data; +} diff --git a/src/components/AnForm/core/useFormRef.ts b/src/components/AnForm/core/useFormRef.ts new file mode 100644 index 0000000..3aadc6c --- /dev/null +++ b/src/components/AnForm/core/useFormRef.ts @@ -0,0 +1,34 @@ +import { FormInstance } from "@arco-design/web-vue"; + +export function useFormRef() { + /** + * 原始表单实例 + */ + const formRef = ref(null); + + type Validate = FormInstance["validate"]; + type ValidateField = FormInstance["validateField"]; + type ResetFields = FormInstance["resetFields"]; + type ClearValidate = FormInstance["clearValidate"]; + type SetFields = FormInstance["setFields"]; + type ScrollToField = FormInstance["scrollToField"]; + + const validate: Validate = async (...args) => formRef.value?.validate(...args); + const validateField: ValidateField = async (...args) => formRef.value?.validateField(...args); + const resetFields: ResetFields = (...args) => formRef.value?.resetFields(...args); + const clearValidate: ClearValidate = (...args) => formRef.value?.clearValidate(...args); + const setFields: SetFields = (...args) => formRef.value?.setFields(...args); + const scrollToField: ScrollToField = (...args) => formRef.value?.scrollToField(...args); + + return { + formRef, + validate, + validateField, + resetFields, + clearValidate, + setFields, + scrollToField, + }; +} + +export type FormRef = ReturnType; diff --git a/src/components/AnForm/core/useFormSubmit.ts b/src/components/AnForm/core/useFormSubmit.ts new file mode 100644 index 0000000..9add517 --- /dev/null +++ b/src/components/AnForm/core/useFormSubmit.ts @@ -0,0 +1,57 @@ +import { FormInstance, Message } from "@arco-design/web-vue"; +import { Ref } from "vue"; +import { IAnFormItem } from "../components/FormItem"; + +interface Options { + items: Ref; + model: Ref; + submit: Ref; + validate: FormInstance["validate"]; +} + +export function useFormSubmit(options: Options, getModel: any) { + const { items, submit, validate } = options; + const loading = ref(false); + + /** + * 设置loading + * @param value 值 + */ + const setLoading = (value: boolean) => { + loading.value = value; + }; + + /** + * 提交表单 + */ + const submitForm = async () => { + if (await validate()) { + return; + } + try { + loading.value = true; + const data = getModel(); + const res = await submit.value?.(data, items.value); + const msg = res?.data?.message; + msg && Message.success(`提示: ${msg}`); + } catch { + console.log(); + } finally { + loading.value = false; + } + }; + + /** + * 取消提交 + */ + const cancelForm = () => {}; + + return { + loading, + setLoading, + submitForm, + cancelForm, + }; +} + +export type FormSubmit = ReturnType; diff --git a/src/components/AnForm/hooks/useForm.tsx b/src/components/AnForm/hooks/useForm.tsx new file mode 100644 index 0000000..61138b9 --- /dev/null +++ b/src/components/AnForm/hooks/useForm.tsx @@ -0,0 +1,45 @@ +import { FormItem, useItems } from "./useItems"; +import { AnForm, IAnForm } from "../components/Form"; + +export type UseForm = Partial> & { + /** + * 表单项 + */ + items?: FormItem[]; +}; + +/** + * 构建表单组件的参数 + */ +export const useForm = (options: UseForm) => { + const { items: _items = [], model: _model = {}, submit, formProps: _props = {} } = options; + const items = useItems(_items, _model, Boolean(options.submit)); + const model = ref(_model); + const formProps = ref(_props); + const instance = ref | null>(null); + + const component = () => { + const onUpdateModel = (value: Recordable) => { + model.value = value; + }; + return ( + (instance.value = el)} + model={model.value} + onUpdate:model={onUpdateModel} + items={items.value} + submit={submit} + formProps={formProps.value} + > + ); + }; + + return { + component, + instance, + model, + items, + formProps, + submit, + }; +}; diff --git a/src/components/AnForm/hooks/useFormModal.tsx b/src/components/AnForm/hooks/useFormModal.tsx new file mode 100644 index 0000000..b269c45 --- /dev/null +++ b/src/components/AnForm/hooks/useFormModal.tsx @@ -0,0 +1,39 @@ +import { AnFormModal, FormModalProps } from "../components/FormModal"; +import { UseForm, useForm } from "./useForm"; +import { FormItem } from "./useItems"; + +type FormModalUseOptions = Partial> & { + items: FormItem[]; +}; + +export function useFormModal(options: FormModalUseOptions) { + const { model, items, formProps } = useForm({ ...options, submit: undefined }); + const trigger = ref(options.trigger); + const title = ref(options.title); + const modalProps = ref(options.modalProps); + const modalRef = ref | null>(null); + const formRef = computed(() => modalRef.value?.formRef); + + const component = () => { + return ( + (modalRef.value = el)} + title={title.value} + trigger={trigger.value} + modalProps={modalProps.value} + model={model.value} + items={items.value} + formProps={formProps.value} + > + ); + }; + + return { + model, + items, + formProps, + component, + modalRef, + formRef, + }; +} diff --git a/src/components/AnForm/hooks/useItems.ts b/src/components/AnForm/hooks/useItems.ts new file mode 100644 index 0000000..9ff04c9 --- /dev/null +++ b/src/components/AnForm/hooks/useItems.ts @@ -0,0 +1,77 @@ +import { defaultsDeep, merge, omit } from "lodash-es"; +import { Rule, useRules } from "./useRules"; +import { IAnFormItem } from "../components/FormItem"; +import { setterMap } from "../components/FormSetter"; + +/** + * 表单项数据 + */ +export type FormItem = Omit & { + /** + * 默认值 + * @default undefined + */ + value?: any; + + /** + * 是否必填 + * @default false + */ + required?: boolean; + + /** + * 校验规则 + * @default undefined + */ + rules?: Rule[]; +}; + +const ITEM: Partial = { + setter: "input", + itemProps: {}, +}; + +const SUBMIT_ITEM: FormItem = { + field: "id", + setter: "submit", + itemProps: { + hideLabel: true, + }, +}; + +export function useItems(list: FormItem[], model: Recordable, submit: boolean) { + const items = ref([]); + let hasSubmit = false; + + for (const item of list) { + let target: any = defaultsDeep({}, ITEM); + + if (!item.setter || typeof item.setter === "string") { + const defaults = setterMap[item.setter ?? "input"]; + if (defaults) { + defaultsDeep(target, defaults); + } + } + + if (item.setter === "submit") { + target = merge(target, SUBMIT_ITEM); + hasSubmit = true; + } + + target = merge(target, omit(item, ["required", "rules"])); + + const rules = useRules(item); + if (rules) { + target.rules = rules; + } + + model[item.field] = model[item.field] ?? item.value; + items.value.push(target); + } + + if (submit && !hasSubmit) { + items.value.push(defaultsDeep({}, SUBMIT_ITEM, setterMap.submit)); + } + + return items; +} diff --git a/src/components/AnForm/hooks/useRules.ts b/src/components/AnForm/hooks/useRules.ts new file mode 100644 index 0000000..56ab8ec --- /dev/null +++ b/src/components/AnForm/hooks/useRules.ts @@ -0,0 +1,96 @@ +import { FieldRule } from "@arco-design/web-vue"; +import { has, isString } from "lodash-es"; +import { IAnFormItemRule } from "../components/FormItem"; + +/** + * 内置规则 + */ +export const FieldRuleMap = defineRuleMap({ + required: { + required: true, + message: "该项不能为空", + }, + string: { + type: "string", + message: "请输入字符串", + }, + number: { + type: "number", + message: "请输入数字", + }, + email: { + type: "email", + message: "邮箱格式错误,示例: xx@abc.com", + }, + url: { + type: "url", + message: "URL格式错误, 示例: www.abc.com", + }, + ip: { + type: "ip", + message: "IP格式错误, 示例: 101.10.10.30", + }, + phone: { + match: /^(?:(?:\+|00)86)?1\d{10}$/, + message: "手机格式错误, 示例(11位): 15912345678", + }, + idcard: { + match: /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/, + message: "身份证格式错误, 长度为15或18位", + }, + alphabet: { + match: /^[a-zA-Z]\w{4,15}$/, + message: "请输入英文字母, 长度为4~15位", + }, + password: { + match: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/, + message: "至少包含大写字母、小写字母、数字和特殊字符", + }, +}); + +/** + * 字符串形式(枚举) + */ +export type FieldStringRule = keyof typeof FieldRuleMap; + +/** + * 完整类型 + */ +export type Rule = FieldStringRule | IAnFormItemRule; + +/** + * 助手函数(获得TS提示) + */ +function defineRuleMap>(ruleMap: T) { + return ruleMap; +} + +/** + * 获取表单规则 + * @param item 表单项 + * @returns + */ +export const useRules = (item: T) => { + const data: IAnFormItemRule[] = []; + const { required, rules } = item; + + if (!has(item, "required") && !has(item, "rules")) { + return null; + } + + if (required) { + data.push(FieldRuleMap.required); + } + + for (const rule of rules ?? []) { + if (isString(rule)) { + if (FieldRuleMap[rule]) { + data.push(FieldRuleMap[rule]); + } + } else { + data.push(rule); + } + } + + return data; +}; diff --git a/src/components/AnForm/index.ts b/src/components/AnForm/index.ts new file mode 100644 index 0000000..794980a --- /dev/null +++ b/src/components/AnForm/index.ts @@ -0,0 +1,3 @@ +export * from "./components/Form"; +export * from "./hooks/useForm"; +export * from "./core/useFormContext"; diff --git a/src/components/AnForm/setters/Cascader.tsx b/src/components/AnForm/setters/Cascader.tsx new file mode 100644 index 0000000..d2e1696 --- /dev/null +++ b/src/components/AnForm/setters/Cascader.tsx @@ -0,0 +1,14 @@ +import { Cascader, CascaderInstance } from "@arco-design/web-vue"; +import { initOptions } from "../utils/initOptions"; + +type Props = CascaderInstance["$props"]; + +export default { + render: Cascader, + init: initOptions, + nodeProps: { + placeholder: "请选择", + allowClear: true, + expandTrigger: "hover", + } as Props, +}; diff --git a/src/components/AnForm/setters/Custom.tsx b/src/components/AnForm/setters/Custom.tsx new file mode 100644 index 0000000..8db05e3 --- /dev/null +++ b/src/components/AnForm/setters/Custom.tsx @@ -0,0 +1,6 @@ +export default { + render: () => { + return "1"; + }, + nodeProps: {}, +}; diff --git a/src/components/AnForm/setters/Date.tsx b/src/components/AnForm/setters/Date.tsx new file mode 100644 index 0000000..6fbdf83 --- /dev/null +++ b/src/components/AnForm/setters/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/AnForm/setters/DateRange.tsx b/src/components/AnForm/setters/DateRange.tsx new file mode 100644 index 0000000..a2226e1 --- /dev/null +++ b/src/components/AnForm/setters/DateRange.tsx @@ -0,0 +1,10 @@ +import { RangePicker, RangePickerInstance } from "@arco-design/web-vue"; + +type Props = RangePickerInstance["$props"]; + +export default { + render: RangePicker, + nodeProps: { + allowClear: true, + } as Props, +}; diff --git a/src/components/AnForm/setters/Input.tsx b/src/components/AnForm/setters/Input.tsx new file mode 100644 index 0000000..7df4095 --- /dev/null +++ b/src/components/AnForm/setters/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/AnForm/setters/Number.tsx b/src/components/AnForm/setters/Number.tsx new file mode 100644 index 0000000..4659f5f --- /dev/null +++ b/src/components/AnForm/setters/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/AnForm/setters/Password.tsx b/src/components/AnForm/setters/Password.tsx new file mode 100644 index 0000000..301027d --- /dev/null +++ b/src/components/AnForm/setters/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/AnForm/setters/Search.tsx b/src/components/AnForm/setters/Search.tsx new file mode 100644 index 0000000..7cd7437 --- /dev/null +++ b/src/components/AnForm/setters/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/AnForm/setters/Select.tsx b/src/components/AnForm/setters/Select.tsx new file mode 100644 index 0000000..9248a15 --- /dev/null +++ b/src/components/AnForm/setters/Select.tsx @@ -0,0 +1,14 @@ +import { Select, SelectInstance, SelectOptionData } from "@arco-design/web-vue"; +import { initOptions } from "../utils/initOptions"; + +export default { + render: Select, + init: initOptions, + nodeProps: { + placeholder: "请选择", + allowClear: true, + allowSearch: true, + options: [], + } as SelectInstance["$props"], + options: [] as SelectOptionData[] | ((arg: any) => Recordable[] | Promise) | undefined, +}; diff --git a/src/components/AnForm/setters/Submit.tsx b/src/components/AnForm/setters/Submit.tsx new file mode 100644 index 0000000..d4510f2 --- /dev/null +++ b/src/components/AnForm/setters/Submit.tsx @@ -0,0 +1,19 @@ +import { Button } from "@arco-design/web-vue"; +import { FormContextKey } from "../core/useFormContext"; + +export default { + render() { + const { loading, submitForm, resetModel } = inject(FormContextKey)!; + return ( + <> + + + + ); + }, + nodeProps: {}, +}; diff --git a/src/components/AnForm/setters/Textarea.tsx b/src/components/AnForm/setters/Textarea.tsx new file mode 100644 index 0000000..0d29b38 --- /dev/null +++ b/src/components/AnForm/setters/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/AnForm/setters/Time.tsx b/src/components/AnForm/setters/Time.tsx new file mode 100644 index 0000000..7a9418f --- /dev/null +++ b/src/components/AnForm/setters/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/AnForm/setters/TreeSelect.tsx b/src/components/AnForm/setters/TreeSelect.tsx new file mode 100644 index 0000000..1774c69 --- /dev/null +++ b/src/components/AnForm/setters/TreeSelect.tsx @@ -0,0 +1,15 @@ +import { TreeSelect, TreeSelectInstance } from "@arco-design/web-vue"; +import { initOptions } from "../utils/initOptions"; + +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/AnForm/setters/index.ts b/src/components/AnForm/setters/index.ts new file mode 100644 index 0000000..c2aca90 --- /dev/null +++ b/src/components/AnForm/setters/index.ts @@ -0,0 +1,29 @@ +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"; +import dateRange from "./DateRange"; + +export default { + input, + number, + search, + textarea, + select, + treeSelect, + time, + password, + cascader, + date, + submit, + custom, + dateRange, +}; diff --git a/src/components/AnForm/utils/defineSetter.ts b/src/components/AnForm/utils/defineSetter.ts new file mode 100644 index 0000000..5212b62 --- /dev/null +++ b/src/components/AnForm/utils/defineSetter.ts @@ -0,0 +1,27 @@ +import { Component } from "vue"; +import { IAnFormItem, IAnFormItemBase } from "../components/FormItem"; + +interface Setter any ? InstanceType["$props"] : any> { + /** + * 输入组件 + */ + component: T; + + /** + * 输入组件参数 + */ + componentProps?: P; + + /** + * 初始化钩子 + * @param model 表单数据 + * @param item 表单项 + * @param items 表单项列表 + * @returns + */ + onSetup?: (model: Recordable, item: IAnFormItemBase, items: IAnFormItemBase[]) => void; +} + +export function defineSetter(options: Setter): Setter { + return options; +} diff --git a/src/components/AnForm/utils/initOptions.ts b/src/components/AnForm/utils/initOptions.ts new file mode 100644 index 0000000..39860c8 --- /dev/null +++ b/src/components/AnForm/utils/initOptions.ts @@ -0,0 +1,30 @@ +export function initOptions({ item, model }: any, key = "options") { + if (Array.isArray(item.options)) { + item.nodeProps[key] = item.options; + return; + } + if (item.options && typeof item.options === "object") { + const { value, source } = item.options; + item._updateOptions = async () => {}; + return; + } + if (typeof item.options === "function") { + const loadData = item.options; + item.nodeProps[key] = reactive([]); + item._updateOptions = async () => { + let data = await loadData({ item, model }); + if (Array.isArray(data?.data?.data)) { + data = data.data.data.map((i: any) => ({ + ...i, + label: i.name, + value: i.id, + })); + } + if (Array.isArray(data)) { + item.nodeProps[key].splice(0); + item.nodeProps[key].push(...data); + } + }; + item._updateOptions(); + } +} diff --git a/src/components/AnForm/utils/strOrFnRender.ts b/src/components/AnForm/utils/strOrFnRender.ts new file mode 100644 index 0000000..efb845c --- /dev/null +++ b/src/components/AnForm/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/form/components/Form.tsx b/src/components/form/components/Form.tsx index 9c9e23a..56b27b9 100644 --- a/src/components/form/components/Form.tsx +++ b/src/components/form/components/Form.tsx @@ -7,6 +7,7 @@ import { useFormRef } from "../core/useFormRef"; import { useFormSubmit } from "../core/useFormSubmit"; import { AnFormItem, IAnFormItem } from "./FormItem"; import { SubmitFn } from "./types/Form"; +import { useVModel } from "@vueuse/core"; /** * 表单组件 @@ -32,7 +33,7 @@ export const AnForm = defineComponent({ * 提交表单 */ submit: { - type: Function as PropType, + type: Function as PropType, }, /** * 传给Form组件的参数 @@ -41,22 +42,16 @@ export const AnForm = defineComponent({ type: Object as PropType>, }, }, - setup(props, { slots }) { - const model = computed(() => props.model); + emits: ["update:model"], + setup(props, { slots, emit }) { + const model = useVModel(props, "model", emit); const items = computed(() => props.items); const submit = computed(() => props.submit); const formRefes = useFormRef(); - const formModel = useFormModel(model); + const formModel = useFormModel(model, formRefes.clearValidate); const formItems = useFormItems(items, model); - const formSubmit = useFormSubmit({ items, model, validate: formRefes.validate, submit }); - - const context = { - slots, - ...formModel, - ...formItems, - ...formRefes, - ...formSubmit, - }; + const formSubmit = useFormSubmit({ items, model, validate: formRefes.validate, submit }, formModel.getModel); + const context = { slots, ...formModel, ...formItems, ...formRefes, ...formSubmit }; provide(FormContextKey, context); return context; @@ -65,7 +60,7 @@ export const AnForm = defineComponent({ return ( {this.items.map((item) => ( - + ))} ); @@ -77,3 +72,5 @@ export type AnFormInstance = InstanceType; export type AnFormProps = AnFormInstance["$props"]; export type IAnForm = Pick; + +export type IAnFormSubmit = (model: Recordable, items: IAnFormItem) => any; diff --git a/src/components/form/components/FormItem.tsx b/src/components/form/components/FormItem.tsx index 92fe79e..877c2d5 100644 --- a/src/components/form/components/FormItem.tsx +++ b/src/components/form/components/FormItem.tsx @@ -1,9 +1,7 @@ -import { FormItem as BaseFormItem, FormItemInstance } from "@arco-design/web-vue"; +import { FormItem as BaseFormItem, FieldRule, 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"; /** * 表单项 @@ -34,19 +32,12 @@ export const AnFormItem = defineComponent({ }, }, setup(props) { - /** - * 校验规则 - */ - const rules = computed(() => props.item.rules?.filter((i) => !i.disable?.(props))); + const rules = computed(() => props.item.rules?.filter((i) => !i.disable?.(props.model, props.item, props.items))); + const disabled = computed(() => Boolean(props.item.disable?.(props.model, props.item, props.items))); + const label = strOrFnRender(props.item.label, props); + const help = strOrFnRender(props.item.help, props); + const extra = strOrFnRender(props.item.extra, props); - /** - * 是否禁用 - */ - const disabled = computed(() => Boolean(props.item.disable?.(props))); - - /** - * 渲染函数 - */ const render = () => { let render = (props.item as any).render; if (!render) { @@ -64,23 +55,8 @@ export const AnFormItem = defineComponent({ } }; - /** - * 标签渲染 - */ - 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)) { + if (props.item.visible && !props.item.visible(props.model, props.item, props.items)) { return null; } return ( @@ -92,13 +68,23 @@ export const AnFormItem = defineComponent({ }, }); -type FormItemFnArg = { - item: T; - items: T[]; - model: Record; -}; +export function strOrFnRender(fn: any, ...args: any[]) { + if (typeof fn === "string") { + return () => fn; + } + if (typeof fn === "function") { + return fn(...args); + } + return null; +} -type FormItemBase = { +export type IAnFormItemBoolFn = (model: Recordable, item: IAnFormItem, items: IAnFormItem[]) => boolean; + +export type IAnFormItemElemFn = (model: Recordable, item: IAnFormItem, items: IAnFormItem[]) => any; + +export type IAnFormItemRule = FieldRule & { disable?: IAnFormItemBoolFn }; + +export type IAnFormItemBase = { /** * 字段名,用于表单、校验和输入框绑定,支持特殊语法。 */ @@ -112,32 +98,34 @@ type FormItemBase = { /** * 校验规则数组 */ - rules?: FieldObjectRule[]; + rules?: IAnFormItemRule[]; /** * 是否可见 */ - visible?: (arg: FormItemFnArg) => boolean; + visible?: IAnFormItemBoolFn; /** * 是否禁用 */ - disable?: (arg: FormItemFnArg) => boolean; + disable?: IAnFormItemBoolFn; /** * 标签名 */ - label?: string | ((args: FormItemFnArg) => any); + label?: string | IAnFormItemElemFn; /** * 帮助提示 */ - help?: string | ((args: FormItemFnArg) => any); + help?: string | IAnFormItemElemFn; /** * 额外内容 */ - extra?: string | ((args: FormItemFnArg) => any); + extra?: string | IAnFormItemElemFn; + + init?: any; }; -export type IAnFormItem = FormItemBase & NodeUnion; +export type IAnFormItem = IAnFormItemBase & NodeUnion; diff --git a/src/components/form/core/useFormContext.ts b/src/components/form/core/useFormContext.ts new file mode 100644 index 0000000..752e6f0 --- /dev/null +++ b/src/components/form/core/useFormContext.ts @@ -0,0 +1,22 @@ +import { InjectionKey } from "vue"; +import { FormItems } from "./useFormItems"; +import { FormModel } from "./useFormModel"; +import { FormRef } from "./useFormRef"; +import { FormSubmit } from "./useFormSubmit"; + +export type FormContextInterface = FormModel & + FormItems & + FormRef & + FormSubmit & { + slots: Recordable; + }; + +export const FormContextKey = Symbol("FormContextKey") as InjectionKey; + +export function useFormContext() { + const context = inject(FormContextKey); + if (!context) { + throw Error("useFormContext musb be used in AnForm children!"); + } + return context; +} diff --git a/src/components/form/core/useFormItems.ts b/src/components/form/core/useFormItems.ts index 51ecc5a..211d63d 100644 --- a/src/components/form/core/useFormItems.ts +++ b/src/components/form/core/useFormItems.ts @@ -15,18 +15,18 @@ export function useFormItems(items: Ref, model: Ref) const initItemOptions = (field: string) => { const item = getItem(field); - item && item.initial?.(); + item && item.init?.(); }; const initItems = () => { for (const item of items.value) { - item.initial?.(item, model); + item.init?.({ item, model: model.value }); } }; const initItem = (field: string) => { const item = getItem(field); - item && item.initial(item, model); + item && item.init?.({ item, model: model.value }); }; onMounted(() => { diff --git a/src/components/form/core/useFormModel.ts b/src/components/form/core/useFormModel.ts index 9e137f0..290e5ba 100644 --- a/src/components/form/core/useFormModel.ts +++ b/src/components/form/core/useFormModel.ts @@ -6,11 +6,12 @@ import { Ref } from "vue"; * @param initial 初始值 * @returns */ -export function useFormModel(model: Ref) { +export function useFormModel(model: Ref, clearValidate: any) { const initial = cloneDeep(model.value); const resetModel = () => { model.value = cloneDeep(initial); + clearValidate(); }; const getInitialModel = () => { @@ -38,7 +39,7 @@ export function useFormModel(model: Ref) { export type FormModel = ReturnType; -function formatModel(model: Recordable) { +export function formatModel(model: Recordable) { const data: Recordable = {}; for (const [key, value] of Object.entries(model)) { diff --git a/src/components/form/core/useFormSubmit.ts b/src/components/form/core/useFormSubmit.ts index bb57dc1..9add517 100644 --- a/src/components/form/core/useFormSubmit.ts +++ b/src/components/form/core/useFormSubmit.ts @@ -1,16 +1,16 @@ import { FormInstance, Message } from "@arco-design/web-vue"; import { Ref } from "vue"; -import { IFormItem } from "../components/FormItem"; +import { IAnFormItem } from "../components/FormItem"; interface Options { - items: Ref; + items: Ref; model: Ref; submit: Ref; validate: FormInstance["validate"]; } -export function useFormSubmit(options: Options) { - const { model, items, submit, validate } = options; +export function useFormSubmit(options: Options, getModel: any) { + const { items, submit, validate } = options; const loading = ref(false); /** @@ -30,7 +30,8 @@ export function useFormSubmit(options: Options) { } try { loading.value = true; - const res = await submit.value?.(model.value, items.value); + const data = getModel(); + const res = await submit.value?.(data, items.value); const msg = res?.data?.message; msg && Message.success(`提示: ${msg}`); } catch { diff --git a/src/components/form/form-config.ts b/src/components/form/form-config.ts index 171011b..8b486fc 100644 --- a/src/components/form/form-config.ts +++ b/src/components/form/form-config.ts @@ -1,4 +1,9 @@ export const config = { + item: { + defaults: { + type: "input", + }, + }, /** * 获取API错误信息 */ @@ -79,10 +84,12 @@ export const config = { export function initOptions({ item, model }: any, key = "options") { if (Array.isArray(item.options)) { item.nodeProps[key] = item.options; + return; } if (item.options && typeof item.options === "object") { const { value, source } = item.options; item._updateOptions = async () => {}; + return; } if (typeof item.options === "function") { const loadData = item.options; diff --git a/src/components/form/hooks/types/Form.ts b/src/components/form/hooks/types/Form.ts index 0844c44..00c56c6 100644 --- a/src/components/form/hooks/types/Form.ts +++ b/src/components/form/hooks/types/Form.ts @@ -1,21 +1,25 @@ import { FormInstance } from "@arco-design/web-vue"; import { FormItem, FormItemFnArg } from "./FormItem"; +type FormInstanceProps = Partial>; + export type UseForm = { /** - * 表单数据模型 + * 表单数据 */ model?: Recordable; /** - * 表单项数组 + * 表单项 */ items?: FormItem[]; /** * 提交表单 + * @description 支持请求地址和请求函数 */ - submit?: (arg: Omit) => PromiseLike; + submit?: string | ((arg: any) => PromiseLike); /** - * 表单实例属性 + * 实例属性 + * @description 透传给表单组件的参数 */ - formProps?: Partial; + formProps?: FormInstanceProps; }; diff --git a/src/components/form/hooks/types/FormItem.ts b/src/components/form/hooks/types/FormItem.ts index 85028ae..6712e6f 100644 --- a/src/components/form/hooks/types/FormItem.ts +++ b/src/components/form/hooks/types/FormItem.ts @@ -1,6 +1,7 @@ -import { FormItemInstance, SelectOptionData } from "@arco-design/web-vue"; +import { FormItemInstance } from "@arco-design/web-vue"; import { NodeType, NodeUnion } from "../../nodes"; import { Rule } from "../useRules"; +import { IAnFormItem, IAnFormItemBoolFn, IAnFormItemElemFn } from "../../components/FormItem"; /** * 函数参数 @@ -12,9 +13,60 @@ export type FormItemFnArg = { }; /** - * 表单项基础 + * 表单项数据 */ type BaseFormItem = { + /** + * 字段名,特殊语法在提交时会自动转换。 + * @example + * ```typescript + * '[v1, v2]' => { v1, v2 } + * ``` + */ + field: string; + + /** + * 初始值 + * @description 若指定该参数,将覆盖model中的同名属性。 + */ + value?: any; + + /** + * 渲染函数 + * @description 用于自定义表单项内容 + */ + render?: NodeType | IAnFormItemElemFn; + + /** + * 标签名 + * @description 同FormItem组件的label属性 + */ + label?: string | IAnFormItemElemFn; + + /** + * 帮助提示 + * @description 同FormItem组件的help插槽 + */ + help?: string | IAnFormItemElemFn; + + /** + * 额外内容 + * @description 同FormItem组件的extra插槽 + */ + extra?: string | IAnFormItemElemFn; + + /** + * 是否必填 + * @description 默认值为false + */ + required?: boolean; + + /** + * 校验规则 + * @description 支持字符串(内置)、对象形式 + */ + rules?: Rule[]; + /** * 传递给`FormItem`组件的参数 * @description 部分属性会不可用,如field、label、required、rules、disabled等 @@ -25,91 +77,16 @@ type BaseFormItem = { * 是否可见 * @description 动态控制表单项是否可见 */ - visible?: (arg: FormItemFnArg) => boolean; + visible?: IAnFormItemBoolFn; /** * 是否禁用 * @description 动态控制表单项是否禁用 */ - disable?: (arg: FormItemFnArg) => boolean; - - /** - * 选项,数组或者函数 - * @description 用于下拉框、单选框、多选框等组件, 支持动态加载 - */ - options?: SelectOptionData[] | ((arg: FormItemFnArg) => PromiseLike); -}; - -/** - * 表单项插槽 - */ -type BaseFormItemSlots = { - /** - * 渲染函数 - * @description 用于自定义表单项内容 - */ - render?: NodeType | ((args: FormItemFnArg) => any); - - /** - * 标签名 - * @description 同FormItem组件的label属性 - */ - label?: string | ((args: FormItemFnArg) => any); - - /** - * 帮助提示 - * @description 同FormItem组件的help插槽 - * @see https://arco.design/vue/component/form#form-item%20Slots - */ - help?: string | ((args: FormItemFnArg) => any); - - /** - * 额外内容 - * @description 同FormItem组件的extra插槽 - * @see https://arco.design/vue/component/form#form-item%20Slots - */ - extra?: string | ((args: FormItemFnArg) => any); -}; - -/** - * 表单项校验 - */ -type BaseFormItemRules = { - /** - * 是否必填 - * @description 默认值为false - */ - required?: boolean; - - /** - * 校验规则 - * @description 支持字符串(内置)、对象形式 - * @see https://arco.design/vue/component/form#FieldRule - */ - rules?: Rule[]; -}; - -/** - * 表单项数据 - */ -type BaseFormItemModel = { - /** - * 字段名,特殊语法在提交时会自动转换。 - * @example - * ```typescript - * '[v1,v2]' => { v1: 1, v2: 2 } - * ``` - */ - field: string; - - /** - * 初始值 - * @description 若指定该参数,将覆盖model中的同名属性。 - */ - initial?: any; + disable?: IAnFormItemBoolFn; }; /** * 表单项 */ -export type FormItem = BaseFormItem & BaseFormItemModel & BaseFormItemRules & BaseFormItemSlots & NodeUnion; +export type FormItem = BaseFormItem & NodeUnion; diff --git a/src/components/form/hooks/useForm.ts b/src/components/form/hooks/useForm.ts index e7bb3b6..1717f2f 100644 --- a/src/components/form/hooks/useForm.ts +++ b/src/components/form/hooks/useForm.ts @@ -1,19 +1,44 @@ -import { UseForm } from "./types/Form"; import { useItems } from "./useItems"; +import { FormInstance } from "@arco-design/web-vue"; +import { FormItem } from "./types/FormItem"; +import { IAnForm, IAnFormSubmit } from "../components/Form"; + +type FormInstanceProps = Partial>; + +export type UseForm = { + /** + * 表单数据 + */ + model?: Recordable; + /** + * 表单项 + */ + items?: FormItem[]; + /** + * 提交表单 + * @description 支持请求地址和请求函数 + */ + submit?: IAnFormSubmit; + /** + * 实例属性 + * @description 透传给表单组件的参数 + */ + formProps?: FormInstanceProps; +}; /** * 构建表单组件的参数 */ export const useForm = (options: UseForm) => { - const { model: _model = {}, items: _items = [], submit, formProps: _formProps } = options; - const items = ref(useItems(_items, _model, Boolean(options.submit))) + const { items: _items = [], model: _model = {}, submit, formProps: _props = {} } = options; + const items = useItems(_items, _model, Boolean(options.submit)); const model = ref(_model); - const formProps = ref(_formProps); + const formProps = ref(_props); return { model, items, - submit, formProps, + submit, }; }; diff --git a/src/components/form/hooks/useItems.ts b/src/components/form/hooks/useItems.ts index 0a7abd9..6642770 100644 --- a/src/components/form/hooks/useItems.ts +++ b/src/components/form/hooks/useItems.ts @@ -1,10 +1,12 @@ -import { merge } from "lodash-es"; -import { nodeMap } from "../nodes"; +import { defaultsDeep, merge, omit } from "lodash-es"; +import { NodeType, nodeMap } from "../nodes"; import { FormItem } from "./types/FormItem"; import { useRules } from "./useRules"; +import { IAnFormItem } from "../components/FormItem"; const ITEM: Partial = { render: "input", + itemProps: {}, }; const SUBMIT_ITEM: FormItem = { @@ -16,26 +18,37 @@ const SUBMIT_ITEM: FormItem = { }; export function useItems(list: FormItem[], model: Recordable, submit: boolean) { - const items = []; + const items = ref([]); let hasSubmit = false; for (const item of list) { - let target: Recordable = merge({}, nodeMap[typeof item.render === "string" ? item.render : "input"]); + let target: any = defaultsDeep({}, ITEM); + + if (!item.render || typeof item.render === "string") { + const defaults = nodeMap[item.render ?? "input"]; + if (defaults) { + defaultsDeep(target, defaults); + } + } if (item.render === "submit") { - target = merge(item, SUBMIT_ITEM); + target = merge(target, SUBMIT_ITEM); hasSubmit = true; } - target = merge(item, item); - target.rules = useRules(item); + target = merge(target, omit(item, ["required", "rules"])); - model[item.field] = model[item.field] ?? item.initial; - items.push(target as any); + const rules = useRules(item); + if (rules) { + target.rules = rules; + } + + model[item.field] = model[item.field] ?? item.value; + items.value.push(target); } if (submit && !hasSubmit) { - items.push(merge({}, SUBMIT_ITEM)); + items.value.push(defaultsDeep({}, SUBMIT_ITEM, nodeMap.submit)); } return items; diff --git a/src/components/form/hooks/useRules.ts b/src/components/form/hooks/useRules.ts index e4f2912..799308c 100644 --- a/src/components/form/hooks/useRules.ts +++ b/src/components/form/hooks/useRules.ts @@ -1,5 +1,6 @@ import { FieldRule } from "@arco-design/web-vue"; -import { isString } from "lodash-es"; +import { has, isString } from "lodash-es"; +import { IAnFormItemBoolFn, IAnFormItemRule } from "../components/FormItem"; /** * 内置规则 @@ -52,17 +53,10 @@ export const FieldRuleMap = defineRuleMap({ */ export type FieldStringRule = keyof typeof FieldRuleMap; -/** - * 对象形式 - */ -export type FieldObjectRule = FieldRule & { - disable?: (arg: { item: T; model: Record }) => boolean; -}; - /** * 完整类型 */ -export type Rule = FieldStringRule | FieldObjectRule; +export type Rule = FieldStringRule | IAnFormItemRule; /** * 助手函数(获得TS提示) @@ -76,19 +70,26 @@ function defineRuleMap>(ruleMap: T) { * @param item 表单项 * @returns */ -export const useRules = [] }>(item: T) => { - const rules: FieldObjectRule[] = []; - if (item.required) { - rules.push(FieldRuleMap.required); +export const useRules = (item: T) => { + const { required, rules } = item; + if (!has(item, "required") && !has(item, "rules")) { + return null; } - for (const rule of item.rules ?? []) { + + const data: IAnFormItemRule[] = []; + if (required) { + data.push(FieldRuleMap.required); + } + + for (const rule of rules ?? []) { if (isString(rule)) { if (FieldRuleMap[rule]) { - rules.push(FieldRuleMap[rule]); + data.push(FieldRuleMap[rule]); } } else { - rules.push(rule); + data.push(rule); } } - return rules; + + return data; }; diff --git a/src/components/form/nodes/Date.tsx b/src/components/form/nodes/Date.tsx index 5fbe565..6fbdf83 100644 --- a/src/components/form/nodes/Date.tsx +++ b/src/components/form/nodes/Date.tsx @@ -5,7 +5,7 @@ type Props = DatePickerInstance["$props"]; export default { render: DatePicker, nodeProps: { - placeholder: "请输入", + placeholder: "请选择", allowClear: true, } as Props, }; diff --git a/src/components/form/nodes/DateRange.tsx b/src/components/form/nodes/DateRange.tsx new file mode 100644 index 0000000..a2226e1 --- /dev/null +++ b/src/components/form/nodes/DateRange.tsx @@ -0,0 +1,10 @@ +import { RangePicker, RangePickerInstance } from "@arco-design/web-vue"; + +type Props = RangePickerInstance["$props"]; + +export default { + render: RangePicker, + nodeProps: { + allowClear: true, + } as Props, +}; diff --git a/src/components/form/nodes/Select.tsx b/src/components/form/nodes/Select.tsx index 097139b..89d5d23 100644 --- a/src/components/form/nodes/Select.tsx +++ b/src/components/form/nodes/Select.tsx @@ -1,9 +1,19 @@ -import { Select, SelectInstance } from "@arco-design/web-vue"; +import { Select, SelectInstance, SelectOptionData } from "@arco-design/web-vue"; import { initOptions } from "../form-config"; +import { Component } from "vue"; +import { defineSetter } from "../utils/defineSetter"; -type Props = SelectInstance["$props"]; +interface Interface { + init: any; + render: Component; + nodeProps: SelectInstance["$props"]; + /** + * 选项 + */ + options?: SelectOptionData[] | ((arg: any) => Recordable[] | Promise); +} -export default { +const select: Interface = { render: Select, init: initOptions, nodeProps: { @@ -11,5 +21,18 @@ export default { allowClear: true, allowSearch: true, options: [], - } as Props, + }, + options: [], }; + +export default select; + +// export default defineSetter({ +// setter: Select, +// setterProps: { +// placeholder: "请选择", +// allowClear: true, +// allowSearch: true, +// options: [], +// }, +// }); diff --git a/src/components/form/nodes/Submit.tsx b/src/components/form/nodes/Submit.tsx index ae48d3b..412402d 100644 --- a/src/components/form/nodes/Submit.tsx +++ b/src/components/form/nodes/Submit.tsx @@ -1,15 +1,17 @@ import { Button } from "@arco-design/web-vue"; +import { FormContextKey } from "../core/interface"; export default { - render: (props: any, { emit }: any) => { + render() { + const { loading, submitForm, resetModel } = inject(FormContextKey)!; return ( <> - + - {/* */} ); }, diff --git a/src/components/form/nodes/index.ts b/src/components/form/nodes/index.ts index 55003a7..e2afe67 100644 --- a/src/components/form/nodes/index.ts +++ b/src/components/form/nodes/index.ts @@ -10,6 +10,7 @@ import submit from "./Submit"; import textarea from "./Textarea"; import time from "./Time"; import treeSelect from "./TreeSelect"; +import dateRange from "./DateRange"; export const nodeMap = { input, @@ -24,6 +25,7 @@ export const nodeMap = { date, submit, custom, + dateRange, }; export type NodeMap = typeof nodeMap; diff --git a/src/components/form/utils/defineSetter.ts b/src/components/form/utils/defineSetter.ts new file mode 100644 index 0000000..97cd8b9 --- /dev/null +++ b/src/components/form/utils/defineSetter.ts @@ -0,0 +1,27 @@ +import { Component } from "vue"; +import { FormItem } from "../hooks/types/FormItem"; + +interface Setter any ? InstanceType["$props"] : any> { + /** + * 输入组件 + */ + setter: T; + + /** + * 输入组件参数 + */ + setterProps?: P; + + /** + * 初始化钩子 + * @param model 表单数据 + * @param item 表单项 + * @param items 表单项列表 + * @returns + */ + onSetup?: (model: Recordable, item: FormItem, items: FormItem[]) => void; +} + +export function defineSetter(options: Setter): Setter { + return options; +} diff --git a/src/pages/home/home.vue b/src/pages/home/home.vue index 9a1945d..53144b8 100644 --- a/src/pages/home/home.vue +++ b/src/pages/home/home.vue @@ -1,32 +1,94 @@ diff --git a/src/types/auto-component.d.ts b/src/types/auto-component.d.ts index 9e3165d..7172e04 100644 --- a/src/types/auto-component.d.ts +++ b/src/types/auto-component.d.ts @@ -7,58 +7,28 @@ export {} declare module '@vue/runtime-core' { export interface GlobalComponents { - AAlert: typeof import('@arco-design/web-vue')['Alert'] - AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete'] AAvatar: typeof import('@arco-design/web-vue')['Avatar'] ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb'] ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem'] AButton: typeof import('@arco-design/web-vue')['Button'] - ACard: typeof import('@arco-design/web-vue')['Card'] - ACheckbox: typeof import('@arco-design/web-vue')['Checkbox'] - ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup'] AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'] - ADatePicker: typeof import('@arco-design/web-vue')['DatePicker'] ADivider: typeof import('@arco-design/web-vue')['Divider'] ADoption: typeof import('@arco-design/web-vue')['Doption'] ADrawer: typeof import('@arco-design/web-vue')['Drawer'] ADropdown: typeof import('@arco-design/web-vue')['Dropdown'] - ADropdownButton: typeof import('@arco-design/web-vue')['DropdownButton'] AEmpty: typeof import('@arco-design/web-vue')['Empty'] - AForm: typeof import('@arco-design/web-vue')['Form'] - AFormItem: typeof import('@arco-design/web-vue')['FormItem'] - AImage: typeof import('@arco-design/web-vue')['Image'] - AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview'] - AInput: typeof import('@arco-design/web-vue')['Input'] - AInputNumber: typeof import('@arco-design/web-vue')['InputNumber'] AInputSearch: typeof import('@arco-design/web-vue')['InputSearch'] ALayout: typeof import('@arco-design/web-vue')['Layout'] ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent'] ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader'] ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider'] ALink: typeof import('@arco-design/web-vue')['Link'] - AList: typeof import('@arco-design/web-vue')['List'] - AListItem: typeof import('@arco-design/web-vue')['ListItem'] - AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta'] AMenu: typeof import('@arco-design/web-vue')['Menu'] AMenuItem: typeof import('@arco-design/web-vue')['MenuItem'] - AModal: typeof import('@arco-design/web-vue')['Modal'] AniEmpty: typeof import('./../components/empty/AniEmpty.vue')['default'] - APagination: typeof import('@arco-design/web-vue')['Pagination'] - APopover: typeof import('@arco-design/web-vue')['Popover'] - AProgress: typeof import('@arco-design/web-vue')['Progress'] - ARadio: typeof import('@arco-design/web-vue')['Radio'] - ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup'] AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar'] - ASelect: typeof import('@arco-design/web-vue')['Select'] ASpin: typeof import('@arco-design/web-vue')['Spin'] - ASwitch: typeof import('@arco-design/web-vue')['Switch'] - ATabPane: typeof import('@arco-design/web-vue')['TabPane'] - ATabs: typeof import('@arco-design/web-vue')['Tabs'] - ATag: typeof import('@arco-design/web-vue')['Tag'] - ATextarea: typeof import('@arco-design/web-vue')['Textarea'] ATooltip: typeof import('@arco-design/web-vue')['Tooltip'] - ATree: typeof import('@arco-design/web-vue')['Tree'] - AUpload: typeof import('@arco-design/web-vue')['Upload'] BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default'] BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default'] BreadPage: typeof import('./../components/breadcrumb/bread-page.vue')['default']