feat: 临时提交
parent
34b3a73f30
commit
a2c263cef7
|
|
@ -1,80 +1,90 @@
|
||||||
import { FormItem as BaseFormItem, FieldRule, FormItemInstance, SelectOptionData } from "@arco-design/web-vue";
|
import { FormItem as BaseFormItem, FormItemInstance, SelectOptionData } from "@arco-design/web-vue";
|
||||||
import { NodeType, NodeUnion, nodeMap } from "./form-node";
|
import { NodeUnion, nodeMap } from "./form-node";
|
||||||
import { RuleMap } from "./form-rules";
|
import { FieldObjectRule, FieldRuleMap, Rule } from "./form-rules";
|
||||||
|
import { PropType } from "vue";
|
||||||
export type FieldStringRule = keyof typeof RuleMap;
|
import { strOrFnRender } from "./util";
|
||||||
export type FieldObjectRule = FieldRule & {
|
|
||||||
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
|
||||||
};
|
|
||||||
export type FieldRuleType = FieldStringRule | FieldObjectRule;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单项
|
* 表单项
|
||||||
*/
|
*/
|
||||||
export const FormItem = (props: any, { emit }: any) => {
|
export const FormItem = defineComponent({
|
||||||
const { item } = props;
|
name: "AppnifyFormItem",
|
||||||
const args = {
|
props: {
|
||||||
...props,
|
/**
|
||||||
field: item.field,
|
* 表单项
|
||||||
};
|
*/
|
||||||
|
item: {
|
||||||
|
type: Object as PropType<IFormItem>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 表单项数组
|
||||||
|
*/
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<IFormItem[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 表单数据
|
||||||
|
*/
|
||||||
|
model: {
|
||||||
|
type: Object as PropType<Recordable>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
/**
|
||||||
|
* 校验规则
|
||||||
|
*/
|
||||||
|
const rules = computed(() => props.item.rules?.filter((i) => !i.disable?.(props)));
|
||||||
|
|
||||||
const rules = computed(() => {
|
/**
|
||||||
const result = [];
|
* 是否禁用
|
||||||
if (item.required) {
|
*/
|
||||||
result.push(RuleMap.required);
|
const disabled = computed(() => Boolean(props.item.disable?.(props)));
|
||||||
|
|
||||||
|
if (props.item.visible && !props.item.visible(props as any)) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
item.rules?.forEach((rule: any) => {
|
|
||||||
if (typeof rule === "string") {
|
|
||||||
result.push(RuleMap[rule as FieldStringRule]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!rule.disable) {
|
|
||||||
result.push(rule);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!rule.disable({ model: props.model, item, items: props.items })) {
|
|
||||||
result.push(rule);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
const disabled = computed(() => {
|
/**
|
||||||
if (item.disable === undefined) {
|
* 渲染函数
|
||||||
return false;
|
*/
|
||||||
}
|
const render = () => {
|
||||||
if (typeof item.disable === "function") {
|
const Item = props.item.component as any;
|
||||||
return item.disable(args);
|
if (props.item.type === "custom") {
|
||||||
}
|
return <Item {...props.item.nodeProps} items={props.items} model={props.model} item={props.item} />;
|
||||||
return item.disable;
|
}
|
||||||
});
|
return <Item {...props.item.nodeProps} v-model={props.model[props.item.field]} />;
|
||||||
|
};
|
||||||
|
|
||||||
if (item.visible && !item.visible(args)) {
|
/**
|
||||||
return null;
|
* 标签渲染
|
||||||
}
|
*/
|
||||||
|
const label = strOrFnRender(props.item.label, props);
|
||||||
|
|
||||||
return (
|
/**
|
||||||
<BaseFormItem {...item.itemProps} rules={rules.value} disabled={disabled.value} field={item.field}>
|
* 帮助信息渲染函数
|
||||||
{{
|
*/
|
||||||
default: () => {
|
const help = strOrFnRender(props.item.help, props);
|
||||||
if (item.component) {
|
|
||||||
return <item.component {...item.nodeProps} model={props.model} item={props.item} />;
|
/**
|
||||||
}
|
* 额外信息渲染函数
|
||||||
const comp = nodeMap[item.type as NodeType]?.component;
|
*/
|
||||||
if (!comp) {
|
const extra = strOrFnRender(props.item.extra, props);
|
||||||
return null;
|
|
||||||
}
|
return () => (
|
||||||
if (item.type === "submit") {
|
<BaseFormItem {...props.item.itemProps} rules={rules.value} disabled={disabled.value} field={props.item.field}>
|
||||||
return <comp loading={props.loading} onSubmit={() => emit("submit")} onCancel={emit("cancel")} />;
|
{{ default: render, label, help, extra }}
|
||||||
}
|
</BaseFormItem>
|
||||||
return <comp v-model={props.model[item.field]} {...item.nodeProps} />;
|
);
|
||||||
},
|
},
|
||||||
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 FormItemFnArg<T = IFormItem> = {
|
||||||
}}
|
item: T;
|
||||||
</BaseFormItem>
|
items: T[];
|
||||||
);
|
model: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormItemBase = {
|
type FormItemBase = {
|
||||||
|
|
@ -105,7 +115,7 @@ type FormItemBase = {
|
||||||
* 标签名
|
* 标签名
|
||||||
* @description 同FormItem组件的label属性
|
* @description 同FormItem组件的label属性
|
||||||
*/
|
*/
|
||||||
label?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
label?: string | ((args: FormItemFnArg) => any);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 传递给`FormItem`组件的参数
|
* 传递给`FormItem`组件的参数
|
||||||
|
|
@ -136,45 +146,45 @@ type FormItemBase = {
|
||||||
*```
|
*```
|
||||||
* @see https://arco.design/vue/component/form#FieldRule
|
* @see https://arco.design/vue/component/form#FieldRule
|
||||||
*/
|
*/
|
||||||
rules?: FieldRuleType[];
|
rules?: FieldObjectRule<IFormItem>[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否可见
|
* 是否可见
|
||||||
* @description 动态控制表单项是否可见
|
* @description 动态控制表单项是否可见
|
||||||
*/
|
*/
|
||||||
visible?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
visible?: (arg: FormItemFnArg) => boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否禁用
|
* 是否禁用
|
||||||
* @description 动态控制表单项是否禁用
|
* @description 动态控制表单项是否禁用
|
||||||
*/
|
*/
|
||||||
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
disable?: (arg: FormItemFnArg) => boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选项,数组或者函数
|
* 选项,数组或者函数
|
||||||
* @description 用于下拉框、单选框、多选框等组件, 支持动态加载
|
* @description 用于下拉框、单选框、多选框等组件, 支持动态加载
|
||||||
*/
|
*/
|
||||||
options?: SelectOptionData[] | ((arg: { item: IFormItem; model: Record<string, any> }) => Promise<any>);
|
options?: SelectOptionData[] | ((arg: FormItemFnArg) => Promise<any>);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单项内容的渲染函数
|
* 表单项内容的渲染函数
|
||||||
* @description 用于自定义表单项内容
|
* @description 用于自定义表单项内容
|
||||||
*/
|
*/
|
||||||
component?: (args: { item: IFormItem; model: Record<string, any>; field: string }) => any;
|
component?: (args: FormItemFnArg) => any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 帮助提示
|
* 帮助提示
|
||||||
* @description 同FormItem组件的help插槽
|
* @description 同FormItem组件的help插槽
|
||||||
* @see https://arco.design/vue/component/form#form-item%20Slots
|
* @see https://arco.design/vue/component/form#form-item%20Slots
|
||||||
*/
|
*/
|
||||||
help?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
help?: string | ((args: FormItemFnArg) => any);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 额外内容
|
* 额外内容
|
||||||
* @description 同FormItem组件的extra插槽
|
* @description 同FormItem组件的extra插槽
|
||||||
* @see https://arco.design/vue/component/form#form-item%20Slots
|
* @see https://arco.design/vue/component/form#form-item%20Slots
|
||||||
*/
|
*/
|
||||||
extra?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
extra?: string | ((args: FormItemFnArg) => any);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IFormItem = FormItemBase & NodeUnion;
|
export type IFormItem = FormItemBase & NodeUnion;
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ export type NodeUnion = {
|
||||||
/**
|
/**
|
||||||
* 输入框类型,默认为`input`
|
* 输入框类型,默认为`input`
|
||||||
*/
|
*/
|
||||||
type: key;
|
type?: key;
|
||||||
/**
|
/**
|
||||||
* 传递给`type`属性对应组件的参数
|
* 传递给`type`属性对应组件的参数
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { FieldRule } from "@arco-design/web-vue";
|
import { FieldRule } from "@arco-design/web-vue";
|
||||||
|
import { isString } from "lodash-es";
|
||||||
|
|
||||||
const defineRuleMap = <T extends Record<string, FieldRule>>(ruleMap: T) => ruleMap;
|
/**
|
||||||
|
* 内置规则
|
||||||
export const RuleMap = defineRuleMap({
|
*/
|
||||||
|
export const FieldRuleMap = defineRuleMap({
|
||||||
required: {
|
required: {
|
||||||
required: true,
|
required: true,
|
||||||
message: "该项不能为空",
|
message: "该项不能为空",
|
||||||
|
|
@ -44,3 +46,47 @@ export const RuleMap = defineRuleMap({
|
||||||
message: "至少包含大写字母、小写字母、数字和特殊字符",
|
message: "至少包含大写字母、小写字母、数字和特殊字符",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符串形式(枚举)
|
||||||
|
*/
|
||||||
|
export type FieldStringRule = keyof typeof FieldRuleMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象形式
|
||||||
|
*/
|
||||||
|
export type FieldObjectRule<T> = FieldRule & {
|
||||||
|
disable?: (arg: { item: T; model: Record<string, any> }) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整类型
|
||||||
|
*/
|
||||||
|
export type Rule<T> = FieldStringRule | FieldObjectRule<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 助手函数(获得TS提示)
|
||||||
|
*/
|
||||||
|
function defineRuleMap<T extends Record<string, FieldRule>>(ruleMap: T) {
|
||||||
|
return ruleMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表单规则
|
||||||
|
* @param item 表单项
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const useFieldRules = <T extends { required?: boolean; rules?: Rule<any>[] }>(item: T) => {
|
||||||
|
const rules: FieldObjectRule<T>[] = [];
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Form as BaseForm, FormInstance as BaseFormInstance, Message } from "@arco-design/web-vue";
|
import { Form as BaseForm, FormInstance as BaseFormInstance, Message } from "@arco-design/web-vue";
|
||||||
import { assign, cloneDeep, defaultsDeep } from "lodash-es";
|
import { assign, cloneDeep, defaultsDeep, merge } from "lodash-es";
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { FormItem, IFormItem } from "./form-item";
|
import { FormItem, IFormItem } from "./form-item";
|
||||||
import { NodeType, nodeMap } from "./form-node";
|
import { nodeMap } from "./form-node";
|
||||||
import { config } from "./form-config";
|
import { config } from "./form-config";
|
||||||
|
|
||||||
type SubmitFn = (arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>;
|
type SubmitFn = (arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>;
|
||||||
|
|
@ -11,13 +11,13 @@ type SubmitFn = (arg: { model: Record<string, any>; items: IFormItem[] }) => Pro
|
||||||
* 表单组件
|
* 表单组件
|
||||||
*/
|
*/
|
||||||
export const Form = defineComponent({
|
export const Form = defineComponent({
|
||||||
name: "Form",
|
name: "AppnifyForm",
|
||||||
props: {
|
props: {
|
||||||
/**
|
/**
|
||||||
* 表单数据
|
* 表单数据
|
||||||
*/
|
*/
|
||||||
model: {
|
model: {
|
||||||
type: Object as PropType<Record<any, any>>,
|
type: Object as PropType<Recordable>,
|
||||||
default: () => reactive({}),
|
default: () => reactive({}),
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,11 +45,11 @@ export const Form = defineComponent({
|
||||||
const formRef = ref<InstanceType<typeof BaseForm>>();
|
const formRef = ref<InstanceType<typeof BaseForm>>();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
props.items.forEach((item: any) => {
|
for (const item of props.items) {
|
||||||
const node = nodeMap[item.type as NodeType];
|
const node = nodeMap[item.type] as any;
|
||||||
defaultsDeep(item, { nodeProps: node?.nodeProps ?? {} });
|
defaultsDeep(item, { nodeProps: node?.nodeProps ?? {} });
|
||||||
(node as any)?.init?.({ item, model: props.model });
|
(node as any)?.init?.({ item, model: props.model });
|
||||||
});
|
}
|
||||||
|
|
||||||
const getItem = (field: string) => {
|
const getItem = (field: string) => {
|
||||||
return props.items.find((item) => item.field === field);
|
return props.items.find((item) => item.field === field);
|
||||||
|
|
@ -64,7 +64,7 @@ export const Form = defineComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetModel = () => {
|
const resetModel = () => {
|
||||||
assign(props.model, model);
|
assign(props.model, merge({}, model));
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
|
|
@ -84,7 +84,7 @@ export const Form = defineComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
const injected = {
|
||||||
formRef,
|
formRef,
|
||||||
loading,
|
loading,
|
||||||
getItem,
|
getItem,
|
||||||
|
|
@ -93,6 +93,10 @@ export const Form = defineComponent({
|
||||||
setModel,
|
setModel,
|
||||||
getModel,
|
getModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
provide("form1", injected);
|
||||||
|
|
||||||
|
return injected;
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
(this.items as any).instance = this;
|
(this.items as any).instance = this;
|
||||||
|
|
@ -104,9 +108,9 @@ export const Form = defineComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseForm ref="formRef" layout="vertical" model={this.model} {...this.$attrs} {...this.formProps}>
|
<BaseForm layout="vertical" {...this.$attrs} {...this.formProps} ref="formRef" model={this.model}>
|
||||||
{this.items.map((item) => (
|
{this.items.map((item) => (
|
||||||
<FormItem loading={this.loading} onSubmit={this.submitForm} item={item} {...props}></FormItem>
|
<FormItem item={item} {...props}></FormItem>
|
||||||
))}
|
))}
|
||||||
</BaseForm>
|
</BaseForm>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { FormInstance } from "@arco-design/web-vue";
|
||||||
|
import { FormItem, FormItemFnArg } from "./FormItem";
|
||||||
|
|
||||||
|
export type UseForm = {
|
||||||
|
/**
|
||||||
|
* 表单数据模型
|
||||||
|
*/
|
||||||
|
model?: Recordable;
|
||||||
|
/**
|
||||||
|
* 表单项数组
|
||||||
|
*/
|
||||||
|
items?: FormItem[];
|
||||||
|
/**
|
||||||
|
* 提交表单
|
||||||
|
*/
|
||||||
|
submit?: (arg: Omit<FormItemFnArg, "item">) => PromiseLike<any>;
|
||||||
|
/**
|
||||||
|
* 表单实例属性
|
||||||
|
*/
|
||||||
|
formProps?: Partial<FormInstance["$props"]>;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { FormItemInstance, SelectOptionData } from "@arco-design/web-vue";
|
||||||
|
import { Rule } from "../useRules";
|
||||||
|
import { NodeUnion } from "../../form-node";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数参数
|
||||||
|
*/
|
||||||
|
export type FormItemFnArg<T = FormItem> = {
|
||||||
|
item: T;
|
||||||
|
items: T[];
|
||||||
|
model: Recordable;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单项基础
|
||||||
|
*/
|
||||||
|
type BaseFormItem = {
|
||||||
|
/**
|
||||||
|
* 传递给`FormItem`组件的参数
|
||||||
|
* @description 部分属性会不可用,如field、label、required、rules、disabled等
|
||||||
|
*/
|
||||||
|
itemProps?: Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可见
|
||||||
|
* @description 动态控制表单项是否可见
|
||||||
|
*/
|
||||||
|
visible?: (arg: FormItemFnArg) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否禁用
|
||||||
|
* @description 动态控制表单项是否禁用
|
||||||
|
*/
|
||||||
|
disable?: (arg: FormItemFnArg) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项,数组或者函数
|
||||||
|
* @description 用于下拉框、单选框、多选框等组件, 支持动态加载
|
||||||
|
*/
|
||||||
|
options?: SelectOptionData[] | ((arg: FormItemFnArg) => PromiseLike<Recordable[]>);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单项插槽
|
||||||
|
*/
|
||||||
|
type BaseFormItemSlots = {
|
||||||
|
/**
|
||||||
|
* 渲染函数
|
||||||
|
* @description 用于自定义表单项内容
|
||||||
|
*/
|
||||||
|
render?: (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<FormItem>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单项数据
|
||||||
|
*/
|
||||||
|
type BaseFormItemModel = {
|
||||||
|
/**
|
||||||
|
* 字段名,特殊语法在提交时会自动转换。
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* '[v1,v2]' => { v1: 1, v2: 2 }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
field: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始值
|
||||||
|
* @description 若指定该参数,将覆盖model中的同名属性。
|
||||||
|
*/
|
||||||
|
initial?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单项
|
||||||
|
*/
|
||||||
|
export type FormItem = BaseFormItem & BaseFormItemModel & BaseFormItemRules & BaseFormItemSlots & NodeUnion;
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useModel } from "./useModel";
|
||||||
|
import { useItems } from "./useItems";
|
||||||
|
import { UseOptions } from "./interface";
|
||||||
|
import { UseForm } from "./types/Form";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建表单组件的参数
|
||||||
|
*/
|
||||||
|
export const useForm = <T extends UseForm>(options: T) => {
|
||||||
|
const initModel = options.model ?? {};
|
||||||
|
const { items, updateItemOptions } = useItems(options.items ?? [], initModel, Boolean(options.submit));
|
||||||
|
const { model, resetModel, setModel, getModel } = useModel(initModel);
|
||||||
|
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
items,
|
||||||
|
resetModel,
|
||||||
|
setModel,
|
||||||
|
getModel,
|
||||||
|
updateItemOptions,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { merge } from "lodash-es";
|
||||||
|
import { FormItem } from "./types/FormItem";
|
||||||
|
import { nodeMap } from "../form-node";
|
||||||
|
import { useRules } from "./useRules";
|
||||||
|
|
||||||
|
const ITEM: Partial<FormItem> = {
|
||||||
|
type: "input",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SUBMIT_ITEM: FormItem = {
|
||||||
|
field: "id",
|
||||||
|
type: "submit",
|
||||||
|
itemProps: {
|
||||||
|
hideLabel: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useItems(list: FormItem[], model: Recordable, submit: boolean) {
|
||||||
|
const items = ref<FormItem[]>([]);
|
||||||
|
let hasSubmit = false;
|
||||||
|
|
||||||
|
for (const item of list) {
|
||||||
|
let target: Recordable = merge({}, nodeMap[item.type ?? "input"]);
|
||||||
|
|
||||||
|
if (item.type === "submit") {
|
||||||
|
target = merge(item, SUBMIT_ITEM);
|
||||||
|
hasSubmit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
target = merge(item, item);
|
||||||
|
target.rules = useRules(item);
|
||||||
|
|
||||||
|
model[item.field] = model[item.field] ?? item.initial;
|
||||||
|
items.value.push(target as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submit && !hasSubmit) {
|
||||||
|
items.value.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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
|
|
||||||
|
function formatModel(model: Recordable) {
|
||||||
|
const data: Recordable = {};
|
||||||
|
for (const [key, val] of Object.entries(model)) {
|
||||||
|
// 数组类型
|
||||||
|
if (/^\[.+\]$/.test(key)) {
|
||||||
|
const subkeysStr = key.replaceAll(/\s/g, "").match(/^\[(.+)\]$/)?.[1];
|
||||||
|
if (!subkeysStr) {
|
||||||
|
data[key] = val;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const subkeys = subkeysStr.split(",");
|
||||||
|
subkeys.forEach((subkey, index) => {
|
||||||
|
if (/(.+)?:number$/.test(subkey)) {
|
||||||
|
subkey = subkey.replace(/:number$/, "");
|
||||||
|
data[subkey] = val?.[index] && Number(val[index]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/(.+)?:boolean$/.test(subkey)) {
|
||||||
|
subkey = subkey.replace(/:boolean$/, "");
|
||||||
|
data[subkey] = val?.[index] && Boolean(val[index]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data[subkey] = val?.[index];
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 默认类型
|
||||||
|
data[key] = val;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单数据管理
|
||||||
|
* @param initial 初始值
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useModel(initial: Recordable) {
|
||||||
|
const model = ref<Recordable>({});
|
||||||
|
|
||||||
|
const resetModel = () => {
|
||||||
|
model.value = cloneDeep(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,
|
||||||
|
resetModel,
|
||||||
|
setModel,
|
||||||
|
getModel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { FieldRule } from "@arco-design/web-vue";
|
||||||
|
import { isString } from "lodash-es";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置规则
|
||||||
|
*/
|
||||||
|
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 FieldObjectRule<T> = FieldRule & {
|
||||||
|
disable?: (arg: { item: T; model: Record<string, any> }) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整类型
|
||||||
|
*/
|
||||||
|
export type Rule<T> = FieldStringRule | FieldObjectRule<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 助手函数(获得TS提示)
|
||||||
|
*/
|
||||||
|
function defineRuleMap<T extends Record<string, FieldRule>>(ruleMap: T) {
|
||||||
|
return ruleMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表单规则
|
||||||
|
* @param item 表单项
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const useRules = <T extends { required?: boolean; rules?: Rule<any>[] }>(item: T) => {
|
||||||
|
const rules: FieldObjectRule<T>[] = [];
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { FormInstance } from "@arco-design/web-vue";
|
import { FormInstance } from "@arco-design/web-vue";
|
||||||
import { IFormItem } from "./form-item";
|
import { IFormItem } from "./form-item";
|
||||||
import { merge } from "lodash-es";
|
import { useModel } from "./hooks/useModel";
|
||||||
|
import { useItems } from "./hooks/useItems";
|
||||||
|
|
||||||
export type Options = {
|
export type Options = {
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,34 +27,16 @@ export type Options = {
|
||||||
* @see src/components/form/use-form.tsx
|
* @see src/components/form/use-form.tsx
|
||||||
*/
|
*/
|
||||||
export const useForm = (options: Options) => {
|
export const useForm = (options: Options) => {
|
||||||
const { model: _model = {} } = options;
|
const initModel = options.model ?? {};
|
||||||
const model: Record<string, any> = { id: undefined, ..._model };
|
const { items, updateItemOptions } = useItems(options.items, initModel, Boolean(options.submit));
|
||||||
const items: IFormItem[] = [];
|
const { model, resetModel, setModel, getModel } = useModel(initModel);
|
||||||
|
|
||||||
for (const item of options.items) {
|
return {
|
||||||
if (!item.nodeProps) {
|
model,
|
||||||
item.nodeProps = {} as any;
|
items,
|
||||||
}
|
resetModel,
|
||||||
model[item.field] = model[item.field] ?? item.initial;
|
setModel,
|
||||||
items.push(item);
|
getModel,
|
||||||
}
|
updateItemOptions,
|
||||||
|
};
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,3 +32,21 @@ export function setModel(model: any, data: Record<string, any>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符串或函数渲染
|
||||||
|
* @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;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
|
import {
|
||||||
import { merge } from "lodash-es";
|
TableColumnData as BaseColumn,
|
||||||
|
TableData as BaseData,
|
||||||
|
Table as BaseTable,
|
||||||
|
PaginationProps,
|
||||||
|
} from "@arco-design/web-vue";
|
||||||
|
import { cloneDeep, isBoolean, isObject, merge } from "lodash-es";
|
||||||
import { PropType, computed, defineComponent, reactive, ref } from "vue";
|
import { PropType, computed, defineComponent, reactive, ref } from "vue";
|
||||||
import AniEmpty from "../empty/AniEmpty.vue";
|
import AniEmpty from "../empty/AniEmpty.vue";
|
||||||
import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form";
|
import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form";
|
||||||
import { config } from "./table.config";
|
import { config } from "./table.config";
|
||||||
|
|
||||||
type DataFn = (search: Record<string, any>, paging: { page: number; size: number }) => Promise<any>;
|
type DataFn = (search: Record<string, any>, paging: { page: number; size: number }) => PromiseLike<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表格组件
|
* 表格组件
|
||||||
|
|
@ -32,8 +37,7 @@ export const Table = defineComponent({
|
||||||
* 分页参数配置
|
* 分页参数配置
|
||||||
*/
|
*/
|
||||||
pagination: {
|
pagination: {
|
||||||
type: Object as PropType<any>,
|
type: [Boolean, Object] as PropType<boolean | PaginationProps>,
|
||||||
default: () => reactive(config.pagination),
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 搜索表单配置
|
* 搜索表单配置
|
||||||
|
|
@ -73,6 +77,7 @@ export const Table = defineComponent({
|
||||||
const createRef = ref<FormModalInstance>();
|
const createRef = ref<FormModalInstance>();
|
||||||
const modifyRef = ref<FormModalInstance>();
|
const modifyRef = ref<FormModalInstance>();
|
||||||
const renderData = ref<BaseData[]>([]);
|
const renderData = ref<BaseData[]>([]);
|
||||||
|
const paging = ref<PaginationProps>(merge({}, isObject(props.pagination) ? props.pagination : config.pagination));
|
||||||
const inlined = computed(() => (props.search?.items?.length ?? 0) <= config.searchInlineCount);
|
const inlined = computed(() => (props.search?.items?.length ?? 0) <= config.searchInlineCount);
|
||||||
const reloadData = () => loadData({ current: 1, pageSize: 10 });
|
const reloadData = () => loadData({ current: 1, pageSize: 10 });
|
||||||
const openModifyModal = (data: any) => modifyRef.value?.open(data);
|
const openModifyModal = (data: any) => modifyRef.value?.open(data);
|
||||||
|
|
@ -81,9 +86,8 @@ export const Table = defineComponent({
|
||||||
* 加载数据
|
* 加载数据
|
||||||
* @param pagination 自定义分页
|
* @param pagination 自定义分页
|
||||||
*/
|
*/
|
||||||
const loadData = async (pagination: Partial<any> = {}) => {
|
const loadData = async (pagination: Partial<PaginationProps> = {}) => {
|
||||||
const merged = { ...props.pagination, ...pagination };
|
const { current: page = 1, pageSize: size = 10 } = { ...paging.value, ...pagination };
|
||||||
const paging = { page: merged.current, size: merged.pageSize };
|
|
||||||
const model = searchRef.value?.getModel() ?? {};
|
const model = searchRef.value?.getModel() ?? {};
|
||||||
|
|
||||||
// 本地加载
|
// 本地加载
|
||||||
|
|
@ -98,21 +102,21 @@ export const Table = defineComponent({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
renderData.value = data;
|
renderData.value = data;
|
||||||
props.pagination.total = renderData.value.length;
|
paging.value.total = renderData.value.length;
|
||||||
props.pagination.current = 1;
|
paging.value.current = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 远程加载
|
// 远程加载
|
||||||
if (typeof props.data === "function") {
|
if (typeof props.data === "function") {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const resData = await props.data(model, paging);
|
const resData = await props.data(model, { page, size });
|
||||||
const { data = [], total = 0 } = resData?.data || {};
|
const { data = [], total = 0 } = resData?.data || {};
|
||||||
renderData.value = data;
|
renderData.value = data;
|
||||||
props.pagination.total = total;
|
paging.value.total = total;
|
||||||
props.pagination.current = paging.page;
|
paging.value.current = page;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// todo
|
console.log(e);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -122,8 +126,8 @@ export const Table = defineComponent({
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (Array.isArray(props.data)) {
|
if (Array.isArray(props.data)) {
|
||||||
renderData.value = props.data;
|
renderData.value = props.data;
|
||||||
props.pagination.total = props.data.length;
|
paging.value.total = props.data.length;
|
||||||
props.pagination.current = 1;
|
paging.value.current = 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -143,6 +147,7 @@ export const Table = defineComponent({
|
||||||
createRef,
|
createRef,
|
||||||
modifyRef,
|
modifyRef,
|
||||||
renderData,
|
renderData,
|
||||||
|
paging,
|
||||||
loadData,
|
loadData,
|
||||||
reloadData,
|
reloadData,
|
||||||
openModifyModal,
|
openModifyModal,
|
||||||
|
|
@ -177,7 +182,10 @@ export const Table = defineComponent({
|
||||||
)}
|
)}
|
||||||
{this.$slots.action?.()}
|
{this.$slots.action?.()}
|
||||||
</div>
|
</div>
|
||||||
<div>{this.inlined && <Form ref="searchRef" {...this.search}></Form>}</div>
|
<div>
|
||||||
|
{this.inlined && <Form ref="searchRef" {...this.search}></Form>}
|
||||||
|
{this.$slots.tool?.(this.renderData)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BaseTable
|
<BaseTable
|
||||||
|
|
@ -187,7 +195,7 @@ export const Table = defineComponent({
|
||||||
{...this.$attrs}
|
{...this.$attrs}
|
||||||
{...this.tableProps}
|
{...this.tableProps}
|
||||||
loading={this.loading}
|
loading={this.loading}
|
||||||
pagination={this.pagination}
|
pagination={this.pagination && this.paging}
|
||||||
data={this.renderData}
|
data={this.renderData}
|
||||||
columns={this.columns}
|
columns={this.columns}
|
||||||
onPageChange={(current: number) => this.loadData({ current })}
|
onPageChange={(current: number) => this.loadData({ current })}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ declare module "*.vue" {
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'numeral' {
|
declare module "numeral" {
|
||||||
const numeral: any
|
const numeral: any;
|
||||||
export default numeral;
|
export default numeral;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Recordable = Record<string, any>;
|
||||||
Loading…
Reference in New Issue