feat: 重构表单
parent
72fd7eba25
commit
eeed362320
2
.env
2
.env
|
|
@ -2,7 +2,7 @@
|
||||||
# 应用配置
|
# 应用配置
|
||||||
# =====================================================================================
|
# =====================================================================================
|
||||||
# 网站标题
|
# 网站标题
|
||||||
VITE_TITLE = 绝弹项目管理
|
VITE_TITLE = Appnify
|
||||||
# 网站副标题
|
# 网站副标题
|
||||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||||
# 部署路径: 当为 ./ 时路由模式需为 hash
|
# 部署路径: 当为 ./ 时路由模式需为 hash
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"bracketSpacing": true
|
"bracketSpacing": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Form, FormInstance } from "@arco-design/web-vue";
|
import { Form, FormInstance, FormItem } from "@arco-design/web-vue";
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { FormContextKey } from "../core/useFormContext";
|
import { FormContextKey } from "../core/useFormContext";
|
||||||
import { useFormItems } from "../core/useFormItems";
|
import { useFormItems } from "../core/useFormItems";
|
||||||
|
|
@ -19,7 +19,7 @@ export const AnForm = defineComponent({
|
||||||
*/
|
*/
|
||||||
model: {
|
model: {
|
||||||
type: Object as PropType<Recordable>,
|
type: Object as PropType<Recordable>,
|
||||||
required: true,
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 表单项
|
* 表单项
|
||||||
|
|
@ -32,7 +32,7 @@ export const AnForm = defineComponent({
|
||||||
* 提交表单
|
* 提交表单
|
||||||
*/
|
*/
|
||||||
submit: {
|
submit: {
|
||||||
type: Function as PropType<IAnFormSubmit>,
|
type: [String, Function, Object] as PropType<IAnFormSubmit>,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 传给Form组件的参数
|
* 传给Form组件的参数
|
||||||
|
|
@ -45,11 +45,10 @@ export const AnForm = defineComponent({
|
||||||
setup(props, { slots, emit }) {
|
setup(props, { slots, emit }) {
|
||||||
const model = useVModel(props, "model", emit);
|
const model = useVModel(props, "model", emit);
|
||||||
const items = computed(() => props.items);
|
const items = computed(() => props.items);
|
||||||
const submit = computed(() => props.submit);
|
|
||||||
const formRefes = useFormRef();
|
const formRefes = useFormRef();
|
||||||
const formModel = useFormModel(model, formRefes.clearValidate);
|
const formModel = useFormModel(model, formRefes.clearValidate);
|
||||||
const formItems = useFormItems(items, model);
|
const formItems = useFormItems(items, model);
|
||||||
const formSubmit = useFormSubmit({ items, model, validate: formRefes.validate, submit }, formModel.getModel);
|
const formSubmit = useFormSubmit(props, formRefes.validate, formModel.getModel);
|
||||||
const context = { slots, ...formModel, ...formItems, ...formRefes, ...formSubmit };
|
const context = { slots, ...formModel, ...formItems, ...formRefes, ...formSubmit };
|
||||||
|
|
||||||
provide(FormContextKey, context);
|
provide(FormContextKey, context);
|
||||||
|
|
@ -74,4 +73,6 @@ export type IAnFormProps = PropType<Omit<FormInstance["$props"], "model">>;
|
||||||
|
|
||||||
export type IAnForm = Pick<AnFormProps, "model" | "items" | "submit" | "formProps">;
|
export type IAnForm = Pick<AnFormProps, "model" | "items" | "submit" | "formProps">;
|
||||||
|
|
||||||
export type IAnFormSubmit = (model: Recordable, items: IAnFormItem) => any;
|
export type IAnFormSubmitFn = (model: Recordable, items: IAnFormItem[]) => any;
|
||||||
|
|
||||||
|
export type IAnFormSubmit = string | IAnFormSubmitFn;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
import { FormItem as BaseFormItem, FieldRule, FormItemInstance } from "@arco-design/web-vue";
|
import {
|
||||||
import { isFunction } from "lodash-es";
|
FormItem as BaseFormItem,
|
||||||
import { PropType } from "vue";
|
FieldRule,
|
||||||
import { SetterItem, SetterType, setterMap } from "./FormSetter";
|
FormItemInstance,
|
||||||
|
SelectOptionData,
|
||||||
|
SelectOptionGroup,
|
||||||
|
} from '@arco-design/web-vue';
|
||||||
|
import { InjectionKey, PropType, provide } from 'vue';
|
||||||
|
import { SetterItem, SetterType, setterMap } from './FormSetter';
|
||||||
|
|
||||||
|
export const FormItemContextKey = Symbol('FormItemContextKey') as InjectionKey<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单项
|
* 表单项
|
||||||
*/
|
*/
|
||||||
export const AnFormItem = defineComponent({
|
export const AnFormItem = defineComponent({
|
||||||
name: "AnFormItem",
|
name: 'AnFormItem',
|
||||||
props: {
|
props: {
|
||||||
/**
|
/**
|
||||||
* 表单项
|
* 表单项
|
||||||
|
|
@ -32,105 +39,164 @@ export const AnFormItem = defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const rules = computed(() => props.item.rules?.filter((i) => !i.disable?.(props.model, props.item, props.items)));
|
const rules = computed(() => props.item.rules?.filter(i => !i.disable?.(props)));
|
||||||
const disabled = computed(() => Boolean(props.item.disable?.(props.model, props.item, props.items)));
|
const disabled = computed(() => Boolean(props.item.disable?.(props)));
|
||||||
const label = strOrFnRender(props.item.label, props);
|
|
||||||
const help = strOrFnRender(props.item.help, props);
|
|
||||||
const extra = strOrFnRender(props.item.extra, props);
|
|
||||||
|
|
||||||
const render = () => {
|
const setterSlots = computed(() => {
|
||||||
let render = props.item.setter as any;
|
const slots = props.item.setterSlots;
|
||||||
if (!render) {
|
if (!slots) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (typeof render === "string") {
|
const items: Recordable = {};
|
||||||
render = setterMap[render as SetterType]?.render;
|
for (const [name, Slot] of Object.entries(slots)) {
|
||||||
if (!render) {
|
items[name] = () => <Slot {...props} />;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <render {...props.item.nodeProps} v-model={props.model[props.item.field]} />;
|
|
||||||
}
|
}
|
||||||
if (isFunction(render)) {
|
return items;
|
||||||
return <render {...props.item.nodeProps} items={props.items} model={props.model} item={props.item} />;
|
});
|
||||||
|
|
||||||
|
const contentRender = () => {
|
||||||
|
const Slot = props.item.itemSlots?.default;
|
||||||
|
if (Slot) {
|
||||||
|
return <Slot {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Setter = setterMap[props.item.setter as SetterType]?.render as any;
|
||||||
|
if (!Setter) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Setter {...props.item.nodeProps} v-model={props.model[props.item.field]}>
|
||||||
|
{setterSlots.value}
|
||||||
|
</Setter>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const makeSlot = (name: 'help' | 'extra' | 'label' | 'default') => {
|
||||||
|
return () => {
|
||||||
|
const Slot = props.item.itemSlots?.[name];
|
||||||
|
return Slot ? () => <Slot {...props} /> : null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const help = computed(makeSlot('help'));
|
||||||
|
const extra = computed(makeSlot('extra'));
|
||||||
|
const label = computed(makeSlot('label'));
|
||||||
|
|
||||||
|
provide(FormItemContextKey, props);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (props.item.visible && !props.item.visible(props.model, props.item, props.items)) {
|
if (props.item.visible && !props.item.visible(props)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BaseFormItem {...props.item.itemProps} rules={rules.value} disabled={disabled.value} field={props.item.field}>
|
<BaseFormItem
|
||||||
{{ default: render, label, help, extra }}
|
{...props.item.itemProps}
|
||||||
|
label={props.item.label as string}
|
||||||
|
rules={rules.value}
|
||||||
|
disabled={disabled.value}
|
||||||
|
field={props.item.field}
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
default: contentRender,
|
||||||
|
help: help.value,
|
||||||
|
extra: extra.value,
|
||||||
|
label: label.value,
|
||||||
|
}}
|
||||||
</BaseFormItem>
|
</BaseFormItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function strOrFnRender(fn: any, ...args: any[]) {
|
export type IAnFormItemBoolFn = (args: IAnFormItemFnProps) => boolean;
|
||||||
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 = (args: IAnFormItemFnProps) => any;
|
||||||
|
|
||||||
export type IAnFormItemElemFn = (model: Recordable, item: IAnFormItem, items: IAnFormItem[]) => any;
|
export type IAnFormItemFnProps = { model: Recordable; item: IAnFormItem; items: IAnFormItem[] };
|
||||||
|
|
||||||
export type IAnFormItemRule = FieldRule & { disable?: IAnFormItemBoolFn };
|
export type IAnFormItemRule = FieldRule & { disable?: IAnFormItemBoolFn };
|
||||||
|
|
||||||
|
export type IAnFormItemOption = string | number | boolean | SelectOptionData | SelectOptionGroup;
|
||||||
|
|
||||||
|
export type IAnFormItemSlot = (props: IAnFormItemFnProps) => any;
|
||||||
|
|
||||||
|
export type IAnFormItemSlots = {
|
||||||
|
/**
|
||||||
|
* 默认插槽
|
||||||
|
* @param props 参数
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
default?: IAnFormItemSlot;
|
||||||
|
/**
|
||||||
|
* 帮助插槽
|
||||||
|
* @param props 参数
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
help?: IAnFormItemSlot;
|
||||||
|
/**
|
||||||
|
* 额外插槽
|
||||||
|
* @param props 参数
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
extra?: IAnFormItemSlot;
|
||||||
|
/**
|
||||||
|
* 标签插槽
|
||||||
|
* @param props 参数
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
label?: IAnFormItemSlot;
|
||||||
|
};
|
||||||
|
|
||||||
export type IAnFormItemBase = {
|
export type IAnFormItemBase = {
|
||||||
/**
|
/**
|
||||||
* 字段名
|
* 字段名
|
||||||
* @description 请保持唯一,支持特殊语法
|
* @description 字段名唯一,支持特殊语法
|
||||||
* @required
|
* @required
|
||||||
*/
|
*/
|
||||||
field: string;
|
field: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 透传的表单项参数
|
* 标签
|
||||||
* @default null
|
* @example '昵称'
|
||||||
*/
|
*/
|
||||||
itemProps?: Partial<Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">>;
|
label?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验规则
|
* 校验规则
|
||||||
|
* @example ['email']
|
||||||
*/
|
*/
|
||||||
rules?: IAnFormItemRule[];
|
rules?: IAnFormItemRule[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否可见
|
* 是否可见
|
||||||
|
* @example (model) => Boolean(model.id)
|
||||||
*/
|
*/
|
||||||
visible?: IAnFormItemBoolFn;
|
visible?: IAnFormItemBoolFn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否禁用
|
* 是否禁用
|
||||||
|
* @example (model) => Boolean(model.id)
|
||||||
*/
|
*/
|
||||||
disable?: IAnFormItemBoolFn;
|
disable?: IAnFormItemBoolFn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标签名
|
* 选项
|
||||||
|
* @description 适用于下拉框等组件
|
||||||
*/
|
*/
|
||||||
label?: string | IAnFormItemElemFn;
|
options?: IAnFormItemOption[] | ((args: IAnFormItemFnProps) => IAnFormItemOption[] | Promise<IAnFormItemOption[]>);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 帮助提示
|
* 表单项参数
|
||||||
|
* @default null
|
||||||
*/
|
*/
|
||||||
help?: string | IAnFormItemElemFn;
|
itemProps?: Partial<Omit<FormItemInstance['$props'], 'field' | 'label' | 'required' | 'rules' | 'disabled'>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 额外内容
|
* 表单项插槽
|
||||||
|
* @see 1
|
||||||
*/
|
*/
|
||||||
extra?: string | IAnFormItemElemFn;
|
itemSlots?: IAnFormItemSlots;
|
||||||
|
|
||||||
options?: any;
|
|
||||||
|
|
||||||
init?: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IAnFormItem = IAnFormItemBase & SetterItem;
|
export type IAnFormItem = IAnFormItemBase & SetterItem;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { Button, ButtonInstance, Form, FormInstance, Modal } from "@arco-design/web-vue";
|
import { Button, ButtonInstance, Modal } from "@arco-design/web-vue";
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { FormContextKey } from "../core/useFormContext";
|
import { IAnFormItem } from "./FormItem";
|
||||||
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";
|
import { AnForm, IAnFormProps, IAnFormSubmit } from "./Form";
|
||||||
|
import { useModalTrigger } from "../core/useModalTrigger";
|
||||||
|
import { useModalSubmit } from "../core/useModalSubmit";
|
||||||
|
import { useVisible } from "@/hooks/useVisible";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单组件
|
* 表单组件
|
||||||
|
|
@ -25,13 +22,14 @@ export const AnFormModal = defineComponent({
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 触发元素
|
* 触发元素
|
||||||
|
* @default '新增'
|
||||||
*/
|
*/
|
||||||
trigger: {
|
trigger: {
|
||||||
type: [Boolean, Function, Object] as PropType<ModalTrigger>,
|
type: [Boolean, String, Function, Object] as PropType<ModalTrigger>,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 传递给Modal组件的props
|
* 传递给Modal的props
|
||||||
*/
|
*/
|
||||||
modalProps: {
|
modalProps: {
|
||||||
type: Object as PropType<ModalProps>,
|
type: Object as PropType<ModalProps>,
|
||||||
|
|
@ -54,7 +52,7 @@ export const AnFormModal = defineComponent({
|
||||||
* 提交表单
|
* 提交表单
|
||||||
*/
|
*/
|
||||||
submit: {
|
submit: {
|
||||||
type: Function as PropType<IAnFormSubmit>,
|
type: [String, Function] as PropType<IAnFormSubmit>,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 传给Form组件的参数
|
* 传给Form组件的参数
|
||||||
|
|
@ -65,8 +63,22 @@ export const AnFormModal = defineComponent({
|
||||||
},
|
},
|
||||||
emits: ["update:model"],
|
emits: ["update:model"],
|
||||||
setup(props, { slots, emit }) {
|
setup(props, { slots, emit }) {
|
||||||
const visible = ref(false);
|
|
||||||
const formRef = ref<InstanceType<typeof AnForm> | null>(null);
|
const formRef = ref<InstanceType<typeof AnForm> | null>(null);
|
||||||
|
const { visible, show, hide } = useVisible();
|
||||||
|
const { modalTrigger } = useModalTrigger(props, show);
|
||||||
|
const { loading, setLoading, submitForm } = useModalSubmit(props, formRef, visible);
|
||||||
|
|
||||||
|
const open = (data: Recordable = {}) => {
|
||||||
|
formRef.value?.setModel(data);
|
||||||
|
visible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
setLoading(false);
|
||||||
|
hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {};
|
||||||
|
|
||||||
const modalTitle = () => {
|
const modalTitle = () => {
|
||||||
if (typeof props.title === "string") {
|
if (typeof props.title === "string") {
|
||||||
|
|
@ -75,47 +87,53 @@ export const AnFormModal = defineComponent({
|
||||||
return <props.title model={props.model} items={props.items}></props.title>;
|
return <props.title model={props.model} items={props.items}></props.title>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalTrigger = () => {
|
|
||||||
if (!props.trigger) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeof props.trigger === "boolean") {
|
|
||||||
return <Button onClick={() => (visible.value = true)}>打开</Button>;
|
|
||||||
}
|
|
||||||
if (typeof props.trigger === "object") {
|
|
||||||
return (
|
|
||||||
<Button {...props.trigger} onClick={() => (visible.value = true)}>
|
|
||||||
打开
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <props.trigger model={props.model} items={props.items}></props.trigger>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visible,
|
visible,
|
||||||
|
loading,
|
||||||
formRef,
|
formRef,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
submitForm,
|
||||||
modalTitle,
|
modalTitle,
|
||||||
modalTrigger,
|
modalTrigger,
|
||||||
|
onClose,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<this.modalTrigger></this.modalTrigger>
|
<this.modalTrigger></this.modalTrigger>
|
||||||
<Modal titleAlign="start" closable={false} {...this.$attrs} {...this.modalProps} v-model:visible={this.visible}>
|
<Modal
|
||||||
|
titleAlign="start"
|
||||||
|
closable={false}
|
||||||
|
{...this.$attrs}
|
||||||
|
{...this.modalProps}
|
||||||
|
maskClosable={false}
|
||||||
|
onClose={this.onClose}
|
||||||
|
v-model:visible={this.visible}
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
title: this.modalTitle,
|
title: this.modalTitle,
|
||||||
default: () => (
|
default: () => (
|
||||||
<AnForm
|
<AnForm
|
||||||
|
ref="formRef"
|
||||||
model={this.model}
|
model={this.model}
|
||||||
onUpdate:model={(v) => this.$emit("update:model", v)}
|
onUpdate:model={(v) => this.$emit("update:model", v)}
|
||||||
items={this.items}
|
items={this.items}
|
||||||
submit={this.submit}
|
|
||||||
formProps={this.formProps}
|
formProps={this.formProps}
|
||||||
></AnForm>
|
></AnForm>
|
||||||
),
|
),
|
||||||
...this.$slots,
|
footer: () => (
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div></div>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<Button onClick={() => (this.visible = false)}>取消</Button>
|
||||||
|
<Button type="primary" loading={this.loading} onClick={this.submitForm}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
@ -123,16 +141,18 @@ export const AnFormModal = defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type ModalProps = Omit<InstanceType<typeof Modal>["$props"], "visible" | "title" | "onBeforeOk">;
|
type ModalProps = Partial<Omit<InstanceType<typeof Modal>["$props"], "visible" | "title" | "onBeforeOk">>;
|
||||||
|
|
||||||
type ModalType = string | ((model: Recordable, items: IAnFormItem[]) => any);
|
type ModalType = string | ((model: Recordable, items: IAnFormItem[]) => any);
|
||||||
|
|
||||||
type ModalTrigger =
|
type ModalTrigger =
|
||||||
| boolean
|
| boolean
|
||||||
|
| string
|
||||||
| ((model: Recordable, items: IAnFormItem[]) => any)
|
| ((model: Recordable, items: IAnFormItem[]) => any)
|
||||||
| {
|
| {
|
||||||
text?: string;
|
text?: string;
|
||||||
buttonProps?: ButtonInstance["$props"];
|
buttonProps?: ButtonInstance["$props"];
|
||||||
|
buttonSlots?: Recordable;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormModalProps = Pick<
|
export type FormModalProps = Pick<
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import setterMap from "../setters";
|
import setterMap from '../setters';
|
||||||
|
|
||||||
export type SetterMap = typeof setterMap;
|
export type SetterMap = typeof setterMap;
|
||||||
|
|
||||||
|
|
@ -6,11 +6,20 @@ export type SetterType = keyof SetterMap;
|
||||||
|
|
||||||
export type SetterItem = {
|
export type SetterItem = {
|
||||||
[key in SetterType]: Partial<
|
[key in SetterType]: Partial<
|
||||||
Omit<SetterMap[key], "setter"> & {
|
Omit<SetterMap[key], 'setter'> & {
|
||||||
/**
|
/**
|
||||||
* 组件类型
|
* 控件类型
|
||||||
|
* @example 'input'
|
||||||
*/
|
*/
|
||||||
setter: key;
|
setter: key;
|
||||||
|
/**
|
||||||
|
* 控件插槽
|
||||||
|
*/
|
||||||
|
setterSlots: Recordable;
|
||||||
|
/**
|
||||||
|
* 控件参数
|
||||||
|
*/
|
||||||
|
setterProps: Recordable;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
}[SetterType];
|
}[SetterType];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from 'lodash-es';
|
||||||
import { Ref } from "vue";
|
import { Ref } from 'vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单数据管理
|
* 表单数据管理
|
||||||
|
|
@ -58,55 +58,33 @@ export function formatModel(model: Recordable) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatModelArray(key: string, value: any, data: Recordable) {
|
function formatModelArray(key: string, value: any, data: Recordable) {
|
||||||
let field = key.replaceAll(/\s/g, "");
|
let field = key.replaceAll(/\s/g, '');
|
||||||
field = field.match(/^\[(.+)\]$/)?.[1] ?? "";
|
field = field.match(/^\[(.+)\]$/)?.[1] ?? '';
|
||||||
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
data[key] = value;
|
data[key] = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = field.split(",");
|
field.split(',').forEach((key, index) => {
|
||||||
keys.forEach((k, i) => {
|
data[key] = value?.[index];
|
||||||
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatModelObject(key: string, value: any, data: Recordable) {
|
function formatModelObject(key: string, value: any, data: Recordable) {
|
||||||
let field = key.replaceAll(/\s/g, "");
|
let field = key.replaceAll(/\s/g, '');
|
||||||
field = field.match(/^\{(.+)\}$/)?.[1] ?? "";
|
field = field.match(/^\{(.+)\}$/)?.[1] ?? '';
|
||||||
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
data[key] = value;
|
data[key] = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = field.split(",");
|
for (const key of field.split(',')) {
|
||||||
keys.forEach((k, i) => {
|
data[key] = value?.[key];
|
||||||
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,7 @@
|
||||||
import { FormInstance, Message } from "@arco-design/web-vue";
|
import { Message } from "@arco-design/web-vue";
|
||||||
import { Ref } from "vue";
|
import { IAnForm } from "../components/Form";
|
||||||
import { IAnFormItem } from "../components/FormItem";
|
|
||||||
|
|
||||||
interface Options {
|
export function useFormSubmit(props: IAnForm, validate: any, getModel: any) {
|
||||||
items: Ref<IAnFormItem[]>;
|
|
||||||
model: Ref<Recordable>;
|
|
||||||
submit: Ref<Function | undefined>;
|
|
||||||
validate: FormInstance["validate"];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFormSubmit(options: Options, getModel: any) {
|
|
||||||
const { items, submit, validate } = options;
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,10 +19,11 @@ export function useFormSubmit(options: Options, getModel: any) {
|
||||||
if (await validate()) {
|
if (await validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const submit = typeof props.submit === "string" ? () => null : props.submit;
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const data = getModel();
|
const data = getModel();
|
||||||
const res = await submit.value?.(data, items.value);
|
const res = await submit?.(data, props.items ?? []);
|
||||||
const msg = res?.data?.message;
|
const msg = res?.data?.message;
|
||||||
msg && Message.success(`提示: ${msg}`);
|
msg && Message.success(`提示: ${msg}`);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { sleep } from "@/utils";
|
||||||
|
import { Message } from "@arco-design/web-vue";
|
||||||
|
import { Ref } from "vue";
|
||||||
|
|
||||||
|
export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>) {
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (await formRef.value?.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const data = formRef.value?.getModel() ?? {};
|
||||||
|
await sleep(5000);
|
||||||
|
const res = await props.submit?.(data, props.items);
|
||||||
|
const msg = res?.data?.message;
|
||||||
|
msg && Message.success(msg);
|
||||||
|
visible.value = false;
|
||||||
|
} catch {
|
||||||
|
// todo
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLoading = (value: boolean) => {
|
||||||
|
loading.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
setLoading,
|
||||||
|
submitForm,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Button } from "@arco-design/web-vue";
|
||||||
|
|
||||||
|
export function useModalTrigger(props: any, open: () => void) {
|
||||||
|
const modalTrigger = () => {
|
||||||
|
if (!props.trigger) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof props.trigger === "function") {
|
||||||
|
return <props.trigger model={props.model} items={props.items} open={open}></props.trigger>;
|
||||||
|
}
|
||||||
|
const internal = {
|
||||||
|
text: "新增",
|
||||||
|
buttonProps: {},
|
||||||
|
buttonSlots: {},
|
||||||
|
};
|
||||||
|
if (typeof props.trigger === "string") {
|
||||||
|
internal.text = props.trigger;
|
||||||
|
}
|
||||||
|
if (typeof props.trigger === "object") {
|
||||||
|
Object.assign(internal, props.trigger);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button type="primary" {...internal.buttonProps} onClick={open}>
|
||||||
|
{{
|
||||||
|
...internal.buttonSlots,
|
||||||
|
default: () => internal.text,
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return { modalTrigger };
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { FormItem, useItems } from "./useItems";
|
import { FormItem, useItems } from "./useItems";
|
||||||
import { AnForm, IAnForm } from "../components/Form";
|
import { AnForm, IAnForm } from "../components/Form";
|
||||||
|
|
||||||
export type UseForm = Partial<Omit<IAnForm, "items">> & {
|
export type FormUseOptions = Partial<Omit<IAnForm, "items">> & {
|
||||||
/**
|
/**
|
||||||
* 表单项
|
* 表单项
|
||||||
*/
|
*/
|
||||||
|
|
@ -11,7 +11,7 @@ export type UseForm = Partial<Omit<IAnForm, "items">> & {
|
||||||
/**
|
/**
|
||||||
* 构建表单组件的参数
|
* 构建表单组件的参数
|
||||||
*/
|
*/
|
||||||
export const useForm = (options: UseForm) => {
|
export const useForm = (options: FormUseOptions) => {
|
||||||
const { items: _items = [], model: _model = {}, submit, formProps: _props = {} } = options;
|
const { items: _items = [], model: _model = {}, submit, formProps: _props = {} } = options;
|
||||||
const items = useItems(_items, _model, Boolean(options.submit));
|
const items = useItems(_items, _model, Boolean(options.submit));
|
||||||
const model = ref(_model);
|
const model = ref(_model);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { AnFormModal, FormModalProps } from "../components/FormModal";
|
import { AnFormModal, FormModalProps } from "../components/FormModal";
|
||||||
import { UseForm, useForm } from "./useForm";
|
import { useForm } from "./useForm";
|
||||||
import { FormItem } from "./useItems";
|
import { FormItem } from "./useItems";
|
||||||
|
|
||||||
type FormModalUseOptions = Partial<Omit<FormModalProps, "items">> & {
|
export type FormModalUseOptions = Partial<Omit<FormModalProps, "items">> & {
|
||||||
items: FormItem[];
|
items: FormItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -12,7 +12,9 @@ export function useFormModal(options: FormModalUseOptions) {
|
||||||
const title = ref(options.title);
|
const title = ref(options.title);
|
||||||
const modalProps = ref(options.modalProps);
|
const modalProps = ref(options.modalProps);
|
||||||
const modalRef = ref<InstanceType<typeof AnFormModal> | null>(null);
|
const modalRef = ref<InstanceType<typeof AnFormModal> | null>(null);
|
||||||
|
const submit = ref(options.submit);
|
||||||
const formRef = computed(() => modalRef.value?.formRef);
|
const formRef = computed(() => modalRef.value?.formRef);
|
||||||
|
const open = (data: Recordable = {}) => modalRef.value?.open(data);
|
||||||
|
|
||||||
const component = () => {
|
const component = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -20,10 +22,11 @@ export function useFormModal(options: FormModalUseOptions) {
|
||||||
ref={(el: any) => (modalRef.value = el)}
|
ref={(el: any) => (modalRef.value = el)}
|
||||||
title={title.value}
|
title={title.value}
|
||||||
trigger={trigger.value}
|
trigger={trigger.value}
|
||||||
modalProps={modalProps.value}
|
modalProps={modalProps.value as any}
|
||||||
model={model.value}
|
model={model.value}
|
||||||
items={items.value}
|
items={items.value}
|
||||||
formProps={formProps.value}
|
formProps={formProps.value}
|
||||||
|
submit={submit.value}
|
||||||
></AnFormModal>
|
></AnFormModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -35,5 +38,6 @@ export function useFormModal(options: FormModalUseOptions) {
|
||||||
component,
|
component,
|
||||||
modalRef,
|
modalRef,
|
||||||
formRef,
|
formRef,
|
||||||
|
open,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { setterMap } from "../components/FormSetter";
|
||||||
export type FormItem = Omit<IAnFormItem, "rules"> & {
|
export type FormItem = Omit<IAnFormItem, "rules"> & {
|
||||||
/**
|
/**
|
||||||
* 默认值
|
* 默认值
|
||||||
* @default undefined
|
* @example 1
|
||||||
*/
|
*/
|
||||||
value?: any;
|
value?: any;
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ export type FormItem = Omit<IAnFormItem, "rules"> & {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验规则
|
* 校验规则
|
||||||
* @default undefined
|
* @example ['email']
|
||||||
*/
|
*/
|
||||||
rules?: Rule[];
|
rules?: Rule[];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./components/Form";
|
export * from "./components/Form";
|
||||||
export * from "./hooks/useForm";
|
export * from "./hooks/useForm";
|
||||||
export * from "./core/useFormContext";
|
export * from "./core/useFormContext";
|
||||||
|
export * from "./components/FormItem";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
import { AnForm, IAnForm, IAnFormProps } from "@/components/AnForm";
|
||||||
|
import { AnFormModal } from "@/components/AnForm/components/FormModal";
|
||||||
|
import AniEmpty from "@/components/empty/AniEmpty.vue";
|
||||||
|
import { FormModalProps } from "@/components/form";
|
||||||
|
import {
|
||||||
|
TableColumnData as BaseColumn,
|
||||||
|
TableData as BaseData,
|
||||||
|
Table as BaseTable,
|
||||||
|
PaginationProps,
|
||||||
|
} from "@arco-design/web-vue";
|
||||||
|
import { isObject, merge } from "lodash-es";
|
||||||
|
import { PropType, computed, defineComponent, ref } from "vue";
|
||||||
|
|
||||||
|
type DataFn = (search: Record<string, any>, paging: { page: number; size: number }) => Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格组件
|
||||||
|
* @see src/components/table/table.tsx
|
||||||
|
*/
|
||||||
|
export const AnTable = defineComponent({
|
||||||
|
name: "AnTable",
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* 表格数据
|
||||||
|
* @description 可以是数组或者函数
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
|
type: [Array, Function] as PropType<BaseData[] | DataFn>,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 表格列设置
|
||||||
|
*/
|
||||||
|
columns: {
|
||||||
|
type: Array as PropType<BaseColumn[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 分页参数配置
|
||||||
|
*/
|
||||||
|
pagination: {
|
||||||
|
type: Object as PropType<boolean | PaginationProps>,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 搜索表单配置
|
||||||
|
*/
|
||||||
|
search: {
|
||||||
|
type: Object as PropType<IAnForm>,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 新建弹窗配置
|
||||||
|
*/
|
||||||
|
create: {
|
||||||
|
type: Object as PropType<FormModalProps>,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 修改弹窗配置
|
||||||
|
*/
|
||||||
|
modify: {
|
||||||
|
type: Object as PropType<FormModalProps>,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 传递给 Table 组件的属性
|
||||||
|
*/
|
||||||
|
tableProps: {
|
||||||
|
type: Object as PropType<InstanceType<typeof BaseTable>["$props"]>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const loading = ref(false);
|
||||||
|
const tableRef = ref<InstanceType<typeof BaseTable>>();
|
||||||
|
const searchRef = ref<FormInstance>();
|
||||||
|
const createRef = ref<FormModalInstance>();
|
||||||
|
const modifyRef = ref<FormModalInstance>();
|
||||||
|
const renderData = ref<BaseData[]>([]);
|
||||||
|
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);
|
||||||
|
|
||||||
|
const useTablePaging = () => {
|
||||||
|
const getPaging = () => {
|
||||||
|
if (isObject(props.pagination)) {
|
||||||
|
return {
|
||||||
|
page: props.pagination.current,
|
||||||
|
size: props.pagination.pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPaging = (paging: PaginationProps) => {
|
||||||
|
if (isObject(props.pagination)) {
|
||||||
|
merge(props.pagination, paging);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPaging = () => {
|
||||||
|
setPaging({ current: 1, pageSize: 10 });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getPaging,
|
||||||
|
setPaging,
|
||||||
|
resetPaging,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getPaging, setPaging, resetPaging } = useTablePaging();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载数据
|
||||||
|
* @param pagination 自定义分页
|
||||||
|
*/
|
||||||
|
const loadData = async () => {
|
||||||
|
const paging = getPaging();
|
||||||
|
const model = searchRef.value?.getModel() ?? {};
|
||||||
|
|
||||||
|
// 本地加载
|
||||||
|
if (Array.isArray(props.data)) {
|
||||||
|
const filters = Object.entries(model);
|
||||||
|
const data = props.data.filter((item) => {
|
||||||
|
return filters.every(([key, value]) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return item[key].includes(value);
|
||||||
|
}
|
||||||
|
return item[key] === value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
renderData.value = data;
|
||||||
|
setPaging({ total: renderData.value.length, current: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 远程加载
|
||||||
|
if (typeof props.data === "function") {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const resData = await props.data(model, paging);
|
||||||
|
const { data = [], total = 0 } = resData?.data || {};
|
||||||
|
renderData.value = data;
|
||||||
|
setPaging({ total });
|
||||||
|
} catch (e) {
|
||||||
|
// todo
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (Array.isArray(props.data)) {
|
||||||
|
renderData.value = props.data;
|
||||||
|
resetPaging();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.search) {
|
||||||
|
merge(props.search, { formProps: { layout: "inline" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
loading,
|
||||||
|
inlined,
|
||||||
|
tableRef,
|
||||||
|
searchRef,
|
||||||
|
createRef,
|
||||||
|
modifyRef,
|
||||||
|
renderData,
|
||||||
|
loadData,
|
||||||
|
reloadData,
|
||||||
|
openModifyModal,
|
||||||
|
};
|
||||||
|
|
||||||
|
provide("ref:table", { ...state, ...props });
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
(this.columns as any).instance = this;
|
||||||
|
return (
|
||||||
|
<div class="table w-full">
|
||||||
|
{!this.inlined && this.search && (
|
||||||
|
<div class="border-b pb-0 border-slate-200 mb-3">
|
||||||
|
<AnForm
|
||||||
|
ref="searchRef"
|
||||||
|
class="!grid grid-cols-4 gap-x-6"
|
||||||
|
v-model:model={this.search.model}
|
||||||
|
items={this.search.items}
|
||||||
|
submit={this.search.submit}
|
||||||
|
formProps={this.search.formProps}
|
||||||
|
></AnForm>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class={`mb-3 flex toolbar justify-between ${!this.inlined && "mt-2"}`}>
|
||||||
|
<div class={`${this.create || this.$slots.action ? null : "!hidden"} flex-1 flex gap-2 `}>
|
||||||
|
{this.create && (
|
||||||
|
<AnFormModal
|
||||||
|
ref="createRef"
|
||||||
|
{...this.create}
|
||||||
|
v-model:model={this.create.model}
|
||||||
|
onSubmited={this.reloadData}
|
||||||
|
></AnFormModal>
|
||||||
|
)}
|
||||||
|
{this.modify && (
|
||||||
|
<FormModal
|
||||||
|
{...(this.modify as any)}
|
||||||
|
ref="modifyRef"
|
||||||
|
onSubmited={this.reloadData}
|
||||||
|
trigger={false}
|
||||||
|
></FormModal>
|
||||||
|
)}
|
||||||
|
{this.$slots.action?.()}
|
||||||
|
</div>
|
||||||
|
<div>{this.inlined && <Form ref="searchRef" {...this.search}></Form>}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
bordered={false}
|
||||||
|
{...this.$attrs}
|
||||||
|
{...this.tableProps}
|
||||||
|
loading={this.loading}
|
||||||
|
pagination={this.pagination}
|
||||||
|
data={this.renderData}
|
||||||
|
columns={this.columns}
|
||||||
|
onPageChange={(current: number) => this.loadData({ current })}
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
empty: () => <AniEmpty />,
|
||||||
|
...this.$slots,
|
||||||
|
}}
|
||||||
|
</BaseTable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格组件实例
|
||||||
|
*/
|
||||||
|
export type TableInstance = InstanceType<typeof Table>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格组件参数
|
||||||
|
*/
|
||||||
|
export type TableProps = TableInstance["$props"];
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { cloneDeep, merge } from "lodash-es";
|
||||||
|
import { IAnFormItem } from "../../AnForm/components/FormItem";
|
||||||
|
import { FormModalProps } from "../../AnForm/components/FormModal";
|
||||||
|
import { FormModalUseOptions } from "../../AnForm/hooks/useFormModal";
|
||||||
|
import { ExtendFormItem } from "./useSearchForm";
|
||||||
|
|
||||||
|
export type ModifyForm = Omit<FormModalUseOptions, "items"> & {
|
||||||
|
/**
|
||||||
|
* 是否继承新建弹窗
|
||||||
|
*/
|
||||||
|
extend?: boolean;
|
||||||
|
/**
|
||||||
|
* 表单项
|
||||||
|
*/
|
||||||
|
items?: ExtendFormItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useModifyForm(form: ModifyForm, create: FormModalProps) {
|
||||||
|
const { extend, items, ...rest } = form;
|
||||||
|
let result = {};
|
||||||
|
if (extend) {
|
||||||
|
cloneDeep(create ?? {});
|
||||||
|
const createItems = create.items;
|
||||||
|
const modifyItems = form.items;
|
||||||
|
if (modifyItems && createItems) {
|
||||||
|
for (let i = 0; i < modifyItems.length; i++) {
|
||||||
|
if (modifyItems[i].extend) {
|
||||||
|
modifyItems[i] = merge({}, createItems[i], modifyItems[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { merge } from "lodash-es";
|
||||||
|
import { FormUseOptions } from "../../AnForm";
|
||||||
|
import { IAnFormItem } from "../../AnForm/components/FormItem";
|
||||||
|
import { FormItem } from "../../AnForm/hooks/useItems";
|
||||||
|
|
||||||
|
export type ExtendFormItem = Partial<
|
||||||
|
FormItem & {
|
||||||
|
/**
|
||||||
|
* 从新建弹窗继承表单项
|
||||||
|
* @example 'name'
|
||||||
|
*/
|
||||||
|
extend: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
type SearchFormItem = ExtendFormItem & {
|
||||||
|
/**
|
||||||
|
* 是否点击图标后进行搜索
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
searchable?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否回车后进行查询
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
enterable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SearchForm = Omit<FormUseOptions, "items"> & {
|
||||||
|
/**
|
||||||
|
* 搜索表单项
|
||||||
|
*/
|
||||||
|
items?: SearchFormItem[];
|
||||||
|
/**
|
||||||
|
* 是否隐藏查询按钮
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideSearch?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useSearchForm(search: SearchForm, extendItems: IAnFormItem[] = []) {
|
||||||
|
const data: any[] = [];
|
||||||
|
const { items = [], hideSearch, ...rest } = search;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const { searchable, enterable, ...itemRest } = item;
|
||||||
|
let _item;
|
||||||
|
if (item.extend) {
|
||||||
|
const extend = extendItems.find((i) => i.field === item.extend);
|
||||||
|
if (extend) {
|
||||||
|
_item = merge({}, extend, itemRest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (searchable) {
|
||||||
|
(item as any).nodeProps.onSearch = () => null;
|
||||||
|
}
|
||||||
|
if (enterable) {
|
||||||
|
(item as any).nodeProps.onPressEnter = () => null;
|
||||||
|
}
|
||||||
|
data.push(_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hideSearch) {
|
||||||
|
const index = data.findIndex((i) => i.type === "submit");
|
||||||
|
if (index > -1) {
|
||||||
|
data.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { FormModalUseOptions } from "../../AnForm/hooks/useFormModal";
|
||||||
|
import { ModifyForm } from "./useModiyForm";
|
||||||
|
import { SearchForm } from "./useSearchForm";
|
||||||
|
import { TableColumn } from "./useTableColumn";
|
||||||
|
|
||||||
|
export interface TableUseOptions {
|
||||||
|
/**
|
||||||
|
* 表格列
|
||||||
|
*/
|
||||||
|
columns?: TableColumn[];
|
||||||
|
/**
|
||||||
|
* 搜索表单
|
||||||
|
*/
|
||||||
|
search?: SearchForm;
|
||||||
|
/**
|
||||||
|
* 新建弹窗
|
||||||
|
*/
|
||||||
|
create?: FormModalUseOptions;
|
||||||
|
/**
|
||||||
|
* 新建弹窗
|
||||||
|
*/
|
||||||
|
modify?: ModifyForm;
|
||||||
|
/**
|
||||||
|
* 详情弹窗
|
||||||
|
*/
|
||||||
|
detail?: any;
|
||||||
|
/**
|
||||||
|
* 批量删除
|
||||||
|
*/
|
||||||
|
delete?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTable(options: TableUseOptions) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
useTable({
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
title: '测试',
|
||||||
|
type: 'index'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { TableColumnData } from "@arco-design/web-vue";
|
||||||
|
|
||||||
|
interface TableBaseColumn {
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
type?: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableIndexColumn {
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
type: "index";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableButtonColumn {
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
type: "button";
|
||||||
|
/**
|
||||||
|
* 按钮列表
|
||||||
|
*/
|
||||||
|
buttons: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableDropdownColumn {
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
type: "dropdown";
|
||||||
|
/**
|
||||||
|
* 下拉列表
|
||||||
|
*/
|
||||||
|
dropdowns: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TableColumn = TableColumnData &
|
||||||
|
(TableIndexColumn | TableBaseColumn | TableButtonColumn | TableDropdownColumn);
|
||||||
|
|
||||||
|
export function useTableColumns(data: TableColumn[]) {
|
||||||
|
const columns = ref<any>([]);
|
||||||
|
|
||||||
|
// for (let column of data) {
|
||||||
|
// if (column.type === "index") {
|
||||||
|
// column = useTableIndexColumn(column);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (column.type === "button") {
|
||||||
|
// column = useTableButtonColumn(column);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (column.type === "dropdown") {
|
||||||
|
// column = useTableDropdownColumn(column);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// columns.push({ ...config.columnBase, ...column });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
useTableColumns([
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
buttons: [{}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "11",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function useTableIndexColumn() {}
|
||||||
|
|
||||||
|
function useTableButtonColumn() {}
|
||||||
|
|
||||||
|
function useTableDropdownColumn() {}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function useVisible(initial = false) {
|
||||||
|
const visible = ref(initial);
|
||||||
|
const show = () => (visible.value = true);
|
||||||
|
const hide = () => (visible.value = false);
|
||||||
|
const toggle = () => (visible.value = !visible.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible,
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
toggle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,93 +1,105 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="m-4 bg-white p-4">
|
<div class="m-4 bg-white p-4">
|
||||||
<div class="border-2 border-green-500 px-2 w-40 text-3xl text-green-500 mb-4">AR K056</div>
|
<div class="border-2 border-green-500 px-2 w-40 text-3xl text-green-500 mb-4">AR K056</div>
|
||||||
<TestForm></TestForm>
|
<div>{{ formatModel(emodel) }}</div>
|
||||||
<div>{{ formatModel(model) }}</div>
|
<UpForm />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { useFormModal } from "@/components/AnForm/hooks/useFormModal";
|
import { useForm } from '@/components/AnForm';
|
||||||
import { formatModel } from "@/components/form/core/useFormModel";
|
import { formatModel } from '@/components/AnForm/core/useFormModel';
|
||||||
import { sleep } from "@/utils";
|
|
||||||
|
|
||||||
const { component: TestForm, model } = useFormModal({
|
const { component: UpForm, model: emodel } = useForm({
|
||||||
trigger: true,
|
|
||||||
modalProps: {
|
|
||||||
width: 880
|
|
||||||
},
|
|
||||||
formProps: {
|
formProps: {
|
||||||
class: 'grid! grid-cols-2 gap-x-4'
|
class: 'grid! grid-cols-2 gap-x-8',
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "id",
|
field: 'id',
|
||||||
label: "输入组件",
|
label: '输入组件',
|
||||||
setter: "input",
|
setter: 'input',
|
||||||
|
itemSlots: {
|
||||||
|
help(props) {
|
||||||
|
return props.item.label;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "xsa",
|
field: 'xsa',
|
||||||
label: "动态渲染",
|
label: '动态渲染',
|
||||||
setter: "input",
|
setter: 'input',
|
||||||
visible: (model) => model.id,
|
visible: props => props.model.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "fsa",
|
field: 'fsa',
|
||||||
label: "动态禁止",
|
label: '动态禁止',
|
||||||
setter: "input",
|
setter: 'input',
|
||||||
disable: (model) => model.id,
|
disable: props => props.model.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "sgs",
|
field: 'sgs',
|
||||||
label: "校验规则",
|
label: '校验规则',
|
||||||
setter: "input",
|
setter: 'input',
|
||||||
required: true,
|
// required: true,
|
||||||
rules: ["email"],
|
// rules: ["email"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "sgss",
|
field: 'sgss',
|
||||||
label: "动态规则",
|
label: '动态规则',
|
||||||
setter: "input",
|
setter: 'input',
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: "必须项",
|
message: '必须项',
|
||||||
disable: (model) => !model.id,
|
disable: props => !props.model.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "num",
|
field: 'num',
|
||||||
value: 20,
|
value: 20,
|
||||||
label: "数字组件",
|
label: '数字组件',
|
||||||
setter: "number",
|
setter: 'number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "date",
|
field: 'date',
|
||||||
label: "日期组件",
|
label: '日期组件',
|
||||||
setter: "date",
|
setter: 'date',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "[startDate, endDate]",
|
field: '[startDate,endDate]',
|
||||||
label: "字段语法",
|
label: '字段语法',
|
||||||
setter: "dateRange",
|
setter: 'dateRange',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "tod",
|
field: '{value,dd}',
|
||||||
value: 1,
|
value: { value: 1 },
|
||||||
label: "下拉组件",
|
label: '下拉组件',
|
||||||
setter: "select",
|
setter: 'select',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: "测试",
|
label: '测试',
|
||||||
value: 1,
|
value: {
|
||||||
|
value: 1,
|
||||||
|
dd: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试2',
|
||||||
|
value: {
|
||||||
|
value: 2,
|
||||||
|
dd: 223,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
nodeProps: {
|
||||||
|
valueKey: 'value',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
async submit(model) {
|
async submit(model) {
|
||||||
await sleep(3000);
|
return { message: '操作成功' };
|
||||||
return { message: "操作成功" };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,58 @@ export {}
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
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']
|
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||||
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
||||||
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
||||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
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']
|
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
||||||
|
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
|
||||||
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
||||||
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
||||||
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
||||||
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
|
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
|
||||||
|
ADropdownButton: typeof import('@arco-design/web-vue')['DropdownButton']
|
||||||
AEmpty: typeof import('@arco-design/web-vue')['Empty']
|
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']
|
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
|
||||||
ALayout: typeof import('@arco-design/web-vue')['Layout']
|
ALayout: typeof import('@arco-design/web-vue')['Layout']
|
||||||
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent']
|
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent']
|
||||||
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
|
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
|
||||||
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
|
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
|
||||||
ALink: typeof import('@arco-design/web-vue')['Link']
|
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']
|
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
||||||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||||
|
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||||
AniEmpty: typeof import('./../components/empty/AniEmpty.vue')['default']
|
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']
|
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
||||||
|
ASelect: typeof import('@arco-design/web-vue')['Select']
|
||||||
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
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']
|
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']
|
BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default']
|
||||||
BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default']
|
BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default']
|
||||||
BreadPage: typeof import('./../components/breadcrumb/bread-page.vue')['default']
|
BreadPage: typeof import('./../components/breadcrumb/bread-page.vue')['default']
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue