feat: 临时提交
parent
34b3a73f30
commit
a2c263cef7
|
|
@ -1,80 +1,90 @@
|
|||
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<string, any> }) => boolean;
|
||||
};
|
||||
export type FieldRuleType = FieldStringRule | FieldObjectRule;
|
||||
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";
|
||||
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
export const FormItem = (props: any, { emit }: any) => {
|
||||
const { item } = props;
|
||||
const args = {
|
||||
...props,
|
||||
field: item.field,
|
||||
};
|
||||
export const FormItem = defineComponent({
|
||||
name: "AppnifyFormItem",
|
||||
props: {
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
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;
|
||||
}
|
||||
if (typeof item.disable === "function") {
|
||||
return item.disable(args);
|
||||
}
|
||||
return item.disable;
|
||||
});
|
||||
/**
|
||||
* 渲染函数
|
||||
*/
|
||||
const render = () => {
|
||||
const Item = props.item.component as any;
|
||||
if (props.item.type === "custom") {
|
||||
return <Item {...props.item.nodeProps} items={props.items} model={props.model} item={props.item} />;
|
||||
}
|
||||
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: () => {
|
||||
if (item.component) {
|
||||
return <item.component {...item.nodeProps} model={props.model} item={props.item} />;
|
||||
}
|
||||
const comp = nodeMap[item.type as NodeType]?.component;
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
if (item.type === "submit") {
|
||||
return <comp loading={props.loading} onSubmit={() => emit("submit")} onCancel={emit("cancel")} />;
|
||||
}
|
||||
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))),
|
||||
}}
|
||||
</BaseFormItem>
|
||||
);
|
||||
/**
|
||||
* 帮助信息渲染函数
|
||||
*/
|
||||
const help = strOrFnRender(props.item.help, props);
|
||||
|
||||
/**
|
||||
* 额外信息渲染函数
|
||||
*/
|
||||
const extra = strOrFnRender(props.item.extra, props);
|
||||
|
||||
return () => (
|
||||
<BaseFormItem {...props.item.itemProps} rules={rules.value} disabled={disabled.value} field={props.item.field}>
|
||||
{{ default: render, label, help, extra }}
|
||||
</BaseFormItem>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
type FormItemFnArg<T = IFormItem> = {
|
||||
item: T;
|
||||
items: T[];
|
||||
model: Record<string, any>;
|
||||
};
|
||||
|
||||
type FormItemBase = {
|
||||
|
|
@ -105,7 +115,7 @@ type FormItemBase = {
|
|||
* 标签名
|
||||
* @description 同FormItem组件的label属性
|
||||
*/
|
||||
label?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
||||
label?: string | ((args: FormItemFnArg) => any);
|
||||
|
||||
/**
|
||||
* 传递给`FormItem`组件的参数
|
||||
|
|
@ -136,45 +146,45 @@ type FormItemBase = {
|
|||
*```
|
||||
* @see https://arco.design/vue/component/form#FieldRule
|
||||
*/
|
||||
rules?: FieldRuleType[];
|
||||
rules?: FieldObjectRule<IFormItem>[];
|
||||
|
||||
/**
|
||||
* 是否可见
|
||||
* @description 动态控制表单项是否可见
|
||||
*/
|
||||
visible?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
||||
visible?: (arg: FormItemFnArg) => boolean;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
* @description 动态控制表单项是否禁用
|
||||
*/
|
||||
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
||||
disable?: (arg: FormItemFnArg) => boolean;
|
||||
|
||||
/**
|
||||
* 选项,数组或者函数
|
||||
* @description 用于下拉框、单选框、多选框等组件, 支持动态加载
|
||||
*/
|
||||
options?: SelectOptionData[] | ((arg: { item: IFormItem; model: Record<string, any> }) => Promise<any>);
|
||||
options?: SelectOptionData[] | ((arg: FormItemFnArg) => Promise<any>);
|
||||
|
||||
/**
|
||||
* 表单项内容的渲染函数
|
||||
* @description 用于自定义表单项内容
|
||||
*/
|
||||
component?: (args: { item: IFormItem; model: Record<string, any>; field: string }) => any;
|
||||
component?: (args: FormItemFnArg) => any;
|
||||
|
||||
/**
|
||||
* 帮助提示
|
||||
* @description 同FormItem组件的help插槽
|
||||
* @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插槽
|
||||
* @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;
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ export type NodeUnion = {
|
|||
/**
|
||||
* 输入框类型,默认为`input`
|
||||
*/
|
||||
type: key;
|
||||
type?: key;
|
||||
/**
|
||||
* 传递给`type`属性对应组件的参数
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
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: true,
|
||||
message: "该项不能为空",
|
||||
|
|
@ -44,3 +46,47 @@ export const RuleMap = defineRuleMap({
|
|||
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 { assign, cloneDeep, defaultsDeep } from "lodash-es";
|
||||
import { assign, cloneDeep, defaultsDeep, merge } from "lodash-es";
|
||||
import { PropType } from "vue";
|
||||
import { FormItem, IFormItem } from "./form-item";
|
||||
import { NodeType, nodeMap } from "./form-node";
|
||||
import { nodeMap } from "./form-node";
|
||||
import { config } from "./form-config";
|
||||
|
||||
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({
|
||||
name: "Form",
|
||||
name: "AppnifyForm",
|
||||
props: {
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
model: {
|
||||
type: Object as PropType<Record<any, any>>,
|
||||
type: Object as PropType<Recordable>,
|
||||
default: () => reactive({}),
|
||||
},
|
||||
/**
|
||||
|
|
@ -45,11 +45,11 @@ export const Form = defineComponent({
|
|||
const formRef = ref<InstanceType<typeof BaseForm>>();
|
||||
const loading = ref(false);
|
||||
|
||||
props.items.forEach((item: any) => {
|
||||
const node = nodeMap[item.type as NodeType];
|
||||
for (const item of props.items) {
|
||||
const node = nodeMap[item.type] as any;
|
||||
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, model);
|
||||
assign(props.model, merge({}, model));
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
|
|
@ -84,7 +84,7 @@ export const Form = defineComponent({
|
|||
}
|
||||
};
|
||||
|
||||
return {
|
||||
const injected = {
|
||||
formRef,
|
||||
loading,
|
||||
getItem,
|
||||
|
|
@ -93,6 +93,10 @@ export const Form = defineComponent({
|
|||
setModel,
|
||||
getModel,
|
||||
};
|
||||
|
||||
provide("form1", injected);
|
||||
|
||||
return injected;
|
||||
},
|
||||
render() {
|
||||
(this.items as any).instance = this;
|
||||
|
|
@ -104,9 +108,9 @@ export const Form = defineComponent({
|
|||
};
|
||||
|
||||
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) => (
|
||||
<FormItem loading={this.loading} onSubmit={this.submitForm} item={item} {...props}></FormItem>
|
||||
<FormItem item={item} {...props}></FormItem>
|
||||
))}
|
||||
</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 { IFormItem } from "./form-item";
|
||||
import { merge } from "lodash-es";
|
||||
import { useModel } from "./hooks/useModel";
|
||||
import { useItems } from "./hooks/useItems";
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
|
|
@ -26,34 +27,16 @@ export type Options = {
|
|||
* @see src/components/form/use-form.tsx
|
||||
*/
|
||||
export const useForm = (options: Options) => {
|
||||
const { model: _model = {} } = options;
|
||||
const model: Record<string, any> = { id: undefined, ..._model };
|
||||
const items: IFormItem[] = [];
|
||||
const initModel = options.model ?? {};
|
||||
const { items, updateItemOptions } = useItems(options.items, initModel, Boolean(options.submit));
|
||||
const { model, resetModel, setModel, getModel } = useModel(initModel);
|
||||
|
||||
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;
|
||||
return {
|
||||
model,
|
||||
items,
|
||||
resetModel,
|
||||
setModel,
|
||||
getModel,
|
||||
updateItemOptions,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 { merge } from "lodash-es";
|
||||
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 { 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<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: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => reactive(config.pagination),
|
||||
type: [Boolean, Object] as PropType<boolean | PaginationProps>,
|
||||
},
|
||||
/**
|
||||
* 搜索表单配置
|
||||
|
|
@ -73,6 +77,7 @@ export const Table = defineComponent({
|
|||
const createRef = ref<FormModalInstance>();
|
||||
const modifyRef = ref<FormModalInstance>();
|
||||
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 reloadData = () => loadData({ current: 1, pageSize: 10 });
|
||||
const openModifyModal = (data: any) => modifyRef.value?.open(data);
|
||||
|
|
@ -81,9 +86,8 @@ export const Table = defineComponent({
|
|||
* 加载数据
|
||||
* @param pagination 自定义分页
|
||||
*/
|
||||
const loadData = async (pagination: Partial<any> = {}) => {
|
||||
const merged = { ...props.pagination, ...pagination };
|
||||
const paging = { page: merged.current, size: merged.pageSize };
|
||||
const loadData = async (pagination: Partial<PaginationProps> = {}) => {
|
||||
const { current: page = 1, pageSize: size = 10 } = { ...paging.value, ...pagination };
|
||||
const model = searchRef.value?.getModel() ?? {};
|
||||
|
||||
// 本地加载
|
||||
|
|
@ -98,21 +102,21 @@ export const Table = defineComponent({
|
|||
});
|
||||
});
|
||||
renderData.value = data;
|
||||
props.pagination.total = renderData.value.length;
|
||||
props.pagination.current = 1;
|
||||
paging.value.total = renderData.value.length;
|
||||
paging.value.current = 1;
|
||||
}
|
||||
|
||||
// 远程加载
|
||||
if (typeof props.data === "function") {
|
||||
try {
|
||||
loading.value = true;
|
||||
const resData = await props.data(model, paging);
|
||||
const resData = await props.data(model, { page, size });
|
||||
const { data = [], total = 0 } = resData?.data || {};
|
||||
renderData.value = data;
|
||||
props.pagination.total = total;
|
||||
props.pagination.current = paging.page;
|
||||
paging.value.total = total;
|
||||
paging.value.current = page;
|
||||
} catch (e) {
|
||||
// todo
|
||||
console.log(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
|
@ -122,8 +126,8 @@ export const Table = defineComponent({
|
|||
watchEffect(() => {
|
||||
if (Array.isArray(props.data)) {
|
||||
renderData.value = props.data;
|
||||
props.pagination.total = props.data.length;
|
||||
props.pagination.current = 1;
|
||||
paging.value.total = props.data.length;
|
||||
paging.value.current = 1;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -143,6 +147,7 @@ export const Table = defineComponent({
|
|||
createRef,
|
||||
modifyRef,
|
||||
renderData,
|
||||
paging,
|
||||
loadData,
|
||||
reloadData,
|
||||
openModifyModal,
|
||||
|
|
@ -177,7 +182,10 @@ export const Table = defineComponent({
|
|||
)}
|
||||
{this.$slots.action?.()}
|
||||
</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>
|
||||
|
||||
<BaseTable
|
||||
|
|
@ -187,7 +195,7 @@ export const Table = defineComponent({
|
|||
{...this.$attrs}
|
||||
{...this.tableProps}
|
||||
loading={this.loading}
|
||||
pagination={this.pagination}
|
||||
pagination={this.pagination && this.paging}
|
||||
data={this.renderData}
|
||||
columns={this.columns}
|
||||
onPageChange={(current: number) => this.loadData({ current })}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ declare module "*.vue" {
|
|||
export default component;
|
||||
}
|
||||
|
||||
declare module 'numeral' {
|
||||
const numeral: any
|
||||
declare module "numeral" {
|
||||
const numeral: any;
|
||||
export default numeral;
|
||||
}
|
||||
|
||||
type Recordable = Record<string, any>;
|
||||
Loading…
Reference in New Issue