From eeed362320824da1afe977c233459f6c5729e4de Mon Sep 17 00:00:00 2001 From: luoer Date: Wed, 15 Nov 2023 17:58:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- .prettierrc | 4 +- src/components/AnForm/components/Form.tsx | 13 +- src/components/AnForm/components/FormItem.tsx | 164 ++++++++---- .../AnForm/components/FormModal.tsx | 86 +++--- .../AnForm/components/FormSetter.tsx | 17 +- src/components/AnForm/core/useFormModel.ts | 44 +-- src/components/AnForm/core/useFormSubmit.ts | 18 +- src/components/AnForm/core/useModalSubmit.ts | 36 +++ .../AnForm/core/useModalTrigger.tsx | 32 +++ src/components/AnForm/hooks/useForm.tsx | 4 +- src/components/AnForm/hooks/useFormModal.tsx | 10 +- src/components/AnForm/hooks/useItems.ts | 4 +- src/components/AnForm/index.ts | 1 + src/components/AnTable/components/Table.tsx | 250 ++++++++++++++++++ src/components/AnTable/hooks/useModiyForm.ts | 33 +++ src/components/AnTable/hooks/useSearchForm.ts | 73 +++++ src/components/AnTable/hooks/useTable.ts | 44 +++ .../AnTable/hooks/useTableColumn.ts | 80 ++++++ src/hooks/useVisible.ts | 13 + src/pages/home/home.vue | 110 ++++---- src/types/auto-component.d.ts | 30 +++ 22 files changed, 872 insertions(+), 196 deletions(-) create mode 100644 src/components/AnForm/core/useModalSubmit.ts create mode 100644 src/components/AnForm/core/useModalTrigger.tsx create mode 100644 src/components/AnTable/components/Table.tsx create mode 100644 src/components/AnTable/hooks/useModiyForm.ts create mode 100644 src/components/AnTable/hooks/useSearchForm.ts create mode 100644 src/components/AnTable/hooks/useTable.ts create mode 100644 src/components/AnTable/hooks/useTableColumn.ts create mode 100644 src/hooks/useVisible.ts diff --git a/.env b/.env index 3f54697..4c3b1eb 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ # 应用配置 # ===================================================================================== # 网站标题 -VITE_TITLE = 绝弹项目管理 +VITE_TITLE = Appnify # 网站副标题 VITE_SUBTITLE = 快速开发web应用的模板工具 # 部署路径: 当为 ./ 时路由模式需为 hash diff --git a/.prettierrc b/.prettierrc index cd05b78..7f81539 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,7 @@ { "tabWidth": 2, "printWidth": 120, - "bracketSpacing": true + "bracketSpacing": true, + "singleQuote": true, + "arrowParens": "avoid" } diff --git a/src/components/AnForm/components/Form.tsx b/src/components/AnForm/components/Form.tsx index fd62ab8..cb76fad 100644 --- a/src/components/AnForm/components/Form.tsx +++ b/src/components/AnForm/components/Form.tsx @@ -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 { FormContextKey } from "../core/useFormContext"; import { useFormItems } from "../core/useFormItems"; @@ -19,7 +19,7 @@ export const AnForm = defineComponent({ */ model: { type: Object as PropType, - required: true, + default: () => ({}), }, /** * 表单项 @@ -32,7 +32,7 @@ export const AnForm = defineComponent({ * 提交表单 */ submit: { - type: Function as PropType, + type: [String, Function, Object] as PropType, }, /** * 传给Form组件的参数 @@ -45,11 +45,10 @@ export const AnForm = defineComponent({ setup(props, { slots, emit }) { const model = useVModel(props, "model", emit); const items = computed(() => props.items); - const submit = computed(() => props.submit); const formRefes = useFormRef(); const formModel = useFormModel(model, formRefes.clearValidate); const formItems = useFormItems(items, model); - const formSubmit = useFormSubmit({ items, model, validate: formRefes.validate, submit }, formModel.getModel); + const formSubmit = useFormSubmit(props, formRefes.validate, formModel.getModel); const context = { slots, ...formModel, ...formItems, ...formRefes, ...formSubmit }; provide(FormContextKey, context); @@ -74,4 +73,6 @@ export type IAnFormProps = PropType>; export type IAnForm = Pick; -export type IAnFormSubmit = (model: Recordable, items: IAnFormItem) => any; +export type IAnFormSubmitFn = (model: Recordable, items: IAnFormItem[]) => any; + +export type IAnFormSubmit = string | IAnFormSubmitFn; diff --git a/src/components/AnForm/components/FormItem.tsx b/src/components/AnForm/components/FormItem.tsx index 2f686ac..a4c3cbc 100644 --- a/src/components/AnForm/components/FormItem.tsx +++ b/src/components/AnForm/components/FormItem.tsx @@ -1,13 +1,20 @@ -import { FormItem as BaseFormItem, FieldRule, FormItemInstance } from "@arco-design/web-vue"; -import { isFunction } from "lodash-es"; -import { PropType } from "vue"; -import { SetterItem, SetterType, setterMap } from "./FormSetter"; +import { + FormItem as BaseFormItem, + FieldRule, + 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; /** * 表单项 */ export const AnFormItem = defineComponent({ - name: "AnFormItem", + name: 'AnFormItem', props: { /** * 表单项 @@ -32,105 +39,164 @@ export const AnFormItem = defineComponent({ }, }, setup(props) { - const rules = computed(() => props.item.rules?.filter((i) => !i.disable?.(props.model, props.item, props.items))); - const disabled = computed(() => Boolean(props.item.disable?.(props.model, props.item, props.items))); - const label = strOrFnRender(props.item.label, props); - const help = strOrFnRender(props.item.help, props); - const extra = strOrFnRender(props.item.extra, props); + const rules = computed(() => props.item.rules?.filter(i => !i.disable?.(props))); + const disabled = computed(() => Boolean(props.item.disable?.(props))); - const render = () => { - let render = props.item.setter as any; - if (!render) { + const setterSlots = computed(() => { + const slots = props.item.setterSlots; + if (!slots) { return null; } - if (typeof render === "string") { - render = setterMap[render as SetterType]?.render; - if (!render) { - return null; - } - return ; + const items: Recordable = {}; + for (const [name, Slot] of Object.entries(slots)) { + items[name] = () => ; } - if (isFunction(render)) { - return ; + return items; + }); + + const contentRender = () => { + const Slot = props.item.itemSlots?.default; + if (Slot) { + return ; } + + const Setter = setterMap[props.item.setter as SetterType]?.render as any; + if (!Setter) { + return null; + } + + return ( + + {setterSlots.value} + + ); }; + const makeSlot = (name: 'help' | 'extra' | 'label' | 'default') => { + return () => { + const Slot = props.item.itemSlots?.[name]; + return Slot ? () => : null; + }; + }; + + const help = computed(makeSlot('help')); + const extra = computed(makeSlot('extra')); + const label = computed(makeSlot('label')); + + provide(FormItemContextKey, props); + 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 ( - - {{ default: render, label, help, extra }} + + {{ + default: contentRender, + help: help.value, + extra: extra.value, + label: label.value, + }} ); }; }, }); -export function strOrFnRender(fn: any, ...args: any[]) { - if (typeof fn === "string") { - return () => fn; - } - if (typeof fn === "function") { - return fn(...args); - } - return null; -} +export type IAnFormItemBoolFn = (args: IAnFormItemFnProps) => boolean; -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 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 = { /** * 字段名 - * @description 请保持唯一,支持特殊语法 + * @description 字段名唯一,支持特殊语法 * @required */ field: string; /** - * 透传的表单项参数 - * @default null + * 标签 + * @example '昵称' */ - itemProps?: Partial>; + label?: string; /** * 校验规则 + * @example ['email'] */ rules?: IAnFormItemRule[]; /** * 是否可见 + * @example (model) => Boolean(model.id) */ visible?: IAnFormItemBoolFn; /** * 是否禁用 + * @example (model) => Boolean(model.id) */ disable?: IAnFormItemBoolFn; /** - * 标签名 + * 选项 + * @description 适用于下拉框等组件 */ - label?: string | IAnFormItemElemFn; + options?: IAnFormItemOption[] | ((args: IAnFormItemFnProps) => IAnFormItemOption[] | Promise); /** - * 帮助提示 + * 表单项参数 + * @default null */ - help?: string | IAnFormItemElemFn; + itemProps?: Partial>; /** - * 额外内容 + * 表单项插槽 + * @see 1 */ - extra?: string | IAnFormItemElemFn; - - options?: any; - - init?: any; + itemSlots?: IAnFormItemSlots; }; export type IAnFormItem = IAnFormItemBase & SetterItem; diff --git a/src/components/AnForm/components/FormModal.tsx b/src/components/AnForm/components/FormModal.tsx index d6c9594..9790fa0 100644 --- a/src/components/AnForm/components/FormModal.tsx +++ b/src/components/AnForm/components/FormModal.tsx @@ -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 { FormContextKey } from "../core/useFormContext"; -import { useFormItems } from "../core/useFormItems"; -import { useFormModel } from "../core/useFormModel"; -import { useFormRef } from "../core/useFormRef"; -import { useFormSubmit } from "../core/useFormSubmit"; -import { AnFormItem, IAnFormItem } from "./FormItem"; -import { useVModel } from "@vueuse/core"; +import { IAnFormItem } from "./FormItem"; 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: { - type: [Boolean, Function, Object] as PropType, + type: [Boolean, String, Function, Object] as PropType, default: true, }, /** - * 传递给Modal组件的props + * 传递给Modal的props */ modalProps: { type: Object as PropType, @@ -54,7 +52,7 @@ export const AnFormModal = defineComponent({ * 提交表单 */ submit: { - type: Function as PropType, + type: [String, Function] as PropType, }, /** * 传给Form组件的参数 @@ -65,8 +63,22 @@ export const AnFormModal = defineComponent({ }, emits: ["update:model"], setup(props, { slots, emit }) { - const visible = ref(false); const formRef = ref | 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 = () => { if (typeof props.title === "string") { @@ -75,47 +87,53 @@ export const AnFormModal = defineComponent({ return ; }; - const modalTrigger = () => { - if (!props.trigger) { - return null; - } - if (typeof props.trigger === "boolean") { - return ; - } - if (typeof props.trigger === "object") { - return ( - - ); - } - return ; - }; - return { visible, + loading, formRef, + open, + close, + submitForm, modalTitle, modalTrigger, + onClose, }; }, render() { return ( <> - + {{ title: this.modalTitle, default: () => ( this.$emit("update:model", v)} items={this.items} - submit={this.submit} formProps={this.formProps} > ), - ...this.$slots, + footer: () => ( +
+
+
+ + +
+
+ ), }}
@@ -123,16 +141,18 @@ export const AnFormModal = defineComponent({ }, }); -type ModalProps = Omit["$props"], "visible" | "title" | "onBeforeOk">; +type ModalProps = Partial["$props"], "visible" | "title" | "onBeforeOk">>; type ModalType = string | ((model: Recordable, items: IAnFormItem[]) => any); type ModalTrigger = | boolean + | string | ((model: Recordable, items: IAnFormItem[]) => any) | { text?: string; buttonProps?: ButtonInstance["$props"]; + buttonSlots?: Recordable; }; export type FormModalProps = Pick< diff --git a/src/components/AnForm/components/FormSetter.tsx b/src/components/AnForm/components/FormSetter.tsx index 0df10b8..1dbe946 100644 --- a/src/components/AnForm/components/FormSetter.tsx +++ b/src/components/AnForm/components/FormSetter.tsx @@ -1,4 +1,4 @@ -import setterMap from "../setters"; +import setterMap from '../setters'; export type SetterMap = typeof setterMap; @@ -6,13 +6,22 @@ export type SetterType = keyof SetterMap; export type SetterItem = { [key in SetterType]: Partial< - Omit & { + Omit & { /** - * 组件类型 + * 控件类型 + * @example 'input' */ setter: key; + /** + * 控件插槽 + */ + setterSlots: Recordable; + /** + * 控件参数 + */ + setterProps: Recordable; } >; }[SetterType]; -export { setterMap }; \ No newline at end of file +export { setterMap }; diff --git a/src/components/AnForm/core/useFormModel.ts b/src/components/AnForm/core/useFormModel.ts index 290e5ba..f815bec 100644 --- a/src/components/AnForm/core/useFormModel.ts +++ b/src/components/AnForm/core/useFormModel.ts @@ -1,5 +1,5 @@ -import { cloneDeep } from "lodash-es"; -import { Ref } from "vue"; +import { cloneDeep } from 'lodash-es'; +import { Ref } from 'vue'; /** * 表单数据管理 @@ -58,55 +58,33 @@ export function formatModel(model: Recordable) { } function formatModelArray(key: string, value: any, data: Recordable) { - let field = key.replaceAll(/\s/g, ""); - field = field.match(/^\[(.+)\]$/)?.[1] ?? ""; + let field = key.replaceAll(/\s/g, ''); + field = field.match(/^\[(.+)\]$/)?.[1] ?? ''; if (!field) { data[key] = value; return; } - const keys = field.split(","); - keys.forEach((k, i) => { - if (/(.+)?:number$/.test(k)) { - k = k.replace(/:number$/, ""); - data[k] = value?.[i] && Number(value[i]); - return; - } - if (/(.+)?:boolean$/.test(k)) { - k = k.replace(/:boolean$/, ""); - data[k] = value?.[i] && Boolean(value[i]); - return; - } - data[k] = value?.[i]; + field.split(',').forEach((key, index) => { + data[key] = value?.[index]; }); return data; } function formatModelObject(key: string, value: any, data: Recordable) { - let field = key.replaceAll(/\s/g, ""); - field = field.match(/^\{(.+)\}$/)?.[1] ?? ""; + let field = key.replaceAll(/\s/g, ''); + field = field.match(/^\{(.+)\}$/)?.[1] ?? ''; if (!field) { data[key] = value; return; } - const keys = field.split(","); - keys.forEach((k, i) => { - if (/(.+)?:number$/.test(k)) { - k = k.replace(/:number$/, ""); - data[k] = value?.[i] && Number(value[i]); - return; - } - if (/(.+)?:boolean$/.test(k)) { - k = k.replace(/:boolean$/, ""); - data[k] = value?.[i] && Boolean(value[i]); - return; - } - data[k] = value?.[i]; - }); + for (const key of field.split(',')) { + data[key] = value?.[key]; + } return data; } diff --git a/src/components/AnForm/core/useFormSubmit.ts b/src/components/AnForm/core/useFormSubmit.ts index 9add517..c2226fc 100644 --- a/src/components/AnForm/core/useFormSubmit.ts +++ b/src/components/AnForm/core/useFormSubmit.ts @@ -1,16 +1,7 @@ -import { FormInstance, Message } from "@arco-design/web-vue"; -import { Ref } from "vue"; -import { IAnFormItem } from "../components/FormItem"; +import { Message } from "@arco-design/web-vue"; +import { IAnForm } from "../components/Form"; -interface Options { - items: Ref; - model: Ref; - submit: Ref; - validate: FormInstance["validate"]; -} - -export function useFormSubmit(options: Options, getModel: any) { - const { items, submit, validate } = options; +export function useFormSubmit(props: IAnForm, validate: any, getModel: any) { const loading = ref(false); /** @@ -28,10 +19,11 @@ export function useFormSubmit(options: Options, getModel: any) { if (await validate()) { return; } + const submit = typeof props.submit === "string" ? () => null : props.submit; try { loading.value = true; const data = getModel(); - const res = await submit.value?.(data, items.value); + const res = await submit?.(data, props.items ?? []); const msg = res?.data?.message; msg && Message.success(`提示: ${msg}`); } catch { diff --git a/src/components/AnForm/core/useModalSubmit.ts b/src/components/AnForm/core/useModalSubmit.ts new file mode 100644 index 0000000..a216b40 --- /dev/null +++ b/src/components/AnForm/core/useModalSubmit.ts @@ -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) { + 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, + }; +} diff --git a/src/components/AnForm/core/useModalTrigger.tsx b/src/components/AnForm/core/useModalTrigger.tsx new file mode 100644 index 0000000..b113997 --- /dev/null +++ b/src/components/AnForm/core/useModalTrigger.tsx @@ -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 ; + } + 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 ( + + ); + }; + return { modalTrigger }; +} diff --git a/src/components/AnForm/hooks/useForm.tsx b/src/components/AnForm/hooks/useForm.tsx index 61138b9..3d56b14 100644 --- a/src/components/AnForm/hooks/useForm.tsx +++ b/src/components/AnForm/hooks/useForm.tsx @@ -1,7 +1,7 @@ import { FormItem, useItems } from "./useItems"; import { AnForm, IAnForm } from "../components/Form"; -export type UseForm = Partial> & { +export type FormUseOptions = Partial> & { /** * 表单项 */ @@ -11,7 +11,7 @@ export type UseForm = Partial> & { /** * 构建表单组件的参数 */ -export const useForm = (options: UseForm) => { +export const useForm = (options: FormUseOptions) => { const { items: _items = [], model: _model = {}, submit, formProps: _props = {} } = options; const items = useItems(_items, _model, Boolean(options.submit)); const model = ref(_model); diff --git a/src/components/AnForm/hooks/useFormModal.tsx b/src/components/AnForm/hooks/useFormModal.tsx index b269c45..694c947 100644 --- a/src/components/AnForm/hooks/useFormModal.tsx +++ b/src/components/AnForm/hooks/useFormModal.tsx @@ -1,8 +1,8 @@ import { AnFormModal, FormModalProps } from "../components/FormModal"; -import { UseForm, useForm } from "./useForm"; +import { useForm } from "./useForm"; import { FormItem } from "./useItems"; -type FormModalUseOptions = Partial> & { +export type FormModalUseOptions = Partial> & { items: FormItem[]; }; @@ -12,7 +12,9 @@ export function useFormModal(options: FormModalUseOptions) { const title = ref(options.title); const modalProps = ref(options.modalProps); const modalRef = ref | null>(null); + const submit = ref(options.submit); const formRef = computed(() => modalRef.value?.formRef); + const open = (data: Recordable = {}) => modalRef.value?.open(data); const component = () => { return ( @@ -20,10 +22,11 @@ export function useFormModal(options: FormModalUseOptions) { ref={(el: any) => (modalRef.value = el)} title={title.value} trigger={trigger.value} - modalProps={modalProps.value} + modalProps={modalProps.value as any} model={model.value} items={items.value} formProps={formProps.value} + submit={submit.value} > ); }; @@ -35,5 +38,6 @@ export function useFormModal(options: FormModalUseOptions) { component, modalRef, formRef, + open, }; } diff --git a/src/components/AnForm/hooks/useItems.ts b/src/components/AnForm/hooks/useItems.ts index 9ff04c9..fcd4de1 100644 --- a/src/components/AnForm/hooks/useItems.ts +++ b/src/components/AnForm/hooks/useItems.ts @@ -9,7 +9,7 @@ import { setterMap } from "../components/FormSetter"; export type FormItem = Omit & { /** * 默认值 - * @default undefined + * @example 1 */ value?: any; @@ -21,7 +21,7 @@ export type FormItem = Omit & { /** * 校验规则 - * @default undefined + * @example ['email'] */ rules?: Rule[]; }; diff --git a/src/components/AnForm/index.ts b/src/components/AnForm/index.ts index 794980a..92fcbbc 100644 --- a/src/components/AnForm/index.ts +++ b/src/components/AnForm/index.ts @@ -1,3 +1,4 @@ export * from "./components/Form"; export * from "./hooks/useForm"; export * from "./core/useFormContext"; +export * from "./components/FormItem"; diff --git a/src/components/AnTable/components/Table.tsx b/src/components/AnTable/components/Table.tsx new file mode 100644 index 0000000..1956ed3 --- /dev/null +++ b/src/components/AnTable/components/Table.tsx @@ -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, paging: { page: number; size: number }) => Promise; + +/** + * 表格组件 + * @see src/components/table/table.tsx + */ +export const AnTable = defineComponent({ + name: "AnTable", + props: { + /** + * 表格数据 + * @description 可以是数组或者函数 + */ + data: { + type: [Array, Function] as PropType, + }, + /** + * 表格列设置 + */ + columns: { + type: Array as PropType, + default: () => [], + }, + /** + * 分页参数配置 + */ + pagination: { + type: Object as PropType, + }, + /** + * 搜索表单配置 + */ + search: { + type: Object as PropType, + }, + /** + * 新建弹窗配置 + */ + create: { + type: Object as PropType, + }, + /** + * 修改弹窗配置 + */ + modify: { + type: Object as PropType, + }, + /** + * 传递给 Table 组件的属性 + */ + tableProps: { + type: Object as PropType["$props"]>, + }, + }, + setup(props) { + const loading = ref(false); + const tableRef = ref>(); + const searchRef = ref(); + const createRef = ref(); + const modifyRef = ref(); + const renderData = ref([]); + 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 ( +
+ {!this.inlined && this.search && ( +
+ +
+ )} + +
+
+ {this.create && ( + + )} + {this.modify && ( + + )} + {this.$slots.action?.()} +
+
{this.inlined &&
}
+
+ + this.loadData({ current })} + > + {{ + empty: () => , + ...this.$slots, + }} + +
+ ); + }, +}); + +/** + * 表格组件实例 + */ +export type TableInstance = InstanceType; + +/** + * 表格组件参数 + */ +export type TableProps = TableInstance["$props"]; diff --git a/src/components/AnTable/hooks/useModiyForm.ts b/src/components/AnTable/hooks/useModiyForm.ts new file mode 100644 index 0000000..dc6e35d --- /dev/null +++ b/src/components/AnTable/hooks/useModiyForm.ts @@ -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 & { + /** + * 是否继承新建弹窗 + */ + 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]); + } + } + } + } +} diff --git a/src/components/AnTable/hooks/useSearchForm.ts b/src/components/AnTable/hooks/useSearchForm.ts new file mode 100644 index 0000000..60e4872 --- /dev/null +++ b/src/components/AnTable/hooks/useSearchForm.ts @@ -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 & { + /** + * 搜索表单项 + */ + 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, + }; +} diff --git a/src/components/AnTable/hooks/useTable.ts b/src/components/AnTable/hooks/useTable.ts new file mode 100644 index 0000000..7071a43 --- /dev/null +++ b/src/components/AnTable/hooks/useTable.ts @@ -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' + } + ] +}) \ No newline at end of file diff --git a/src/components/AnTable/hooks/useTableColumn.ts b/src/components/AnTable/hooks/useTableColumn.ts new file mode 100644 index 0000000..848f50b --- /dev/null +++ b/src/components/AnTable/hooks/useTableColumn.ts @@ -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([]); + + // 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() {} diff --git a/src/hooks/useVisible.ts b/src/hooks/useVisible.ts new file mode 100644 index 0000000..a41ad26 --- /dev/null +++ b/src/hooks/useVisible.ts @@ -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, + }; +} diff --git a/src/pages/home/home.vue b/src/pages/home/home.vue index 53144b8..6bcd057 100644 --- a/src/pages/home/home.vue +++ b/src/pages/home/home.vue @@ -1,93 +1,105 @@ diff --git a/src/types/auto-component.d.ts b/src/types/auto-component.d.ts index 7172e04..9e3165d 100644 --- a/src/types/auto-component.d.ts +++ b/src/types/auto-component.d.ts @@ -7,28 +7,58 @@ export {} declare module '@vue/runtime-core' { export interface GlobalComponents { + AAlert: typeof import('@arco-design/web-vue')['Alert'] + AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete'] AAvatar: typeof import('@arco-design/web-vue')['Avatar'] ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb'] ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem'] AButton: typeof import('@arco-design/web-vue')['Button'] + ACard: typeof import('@arco-design/web-vue')['Card'] + ACheckbox: typeof import('@arco-design/web-vue')['Checkbox'] + ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup'] AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'] + ADatePicker: typeof import('@arco-design/web-vue')['DatePicker'] ADivider: typeof import('@arco-design/web-vue')['Divider'] ADoption: typeof import('@arco-design/web-vue')['Doption'] ADrawer: typeof import('@arco-design/web-vue')['Drawer'] ADropdown: typeof import('@arco-design/web-vue')['Dropdown'] + ADropdownButton: typeof import('@arco-design/web-vue')['DropdownButton'] AEmpty: typeof import('@arco-design/web-vue')['Empty'] + AForm: typeof import('@arco-design/web-vue')['Form'] + AFormItem: typeof import('@arco-design/web-vue')['FormItem'] + AImage: typeof import('@arco-design/web-vue')['Image'] + AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview'] + AInput: typeof import('@arco-design/web-vue')['Input'] + AInputNumber: typeof import('@arco-design/web-vue')['InputNumber'] AInputSearch: typeof import('@arco-design/web-vue')['InputSearch'] ALayout: typeof import('@arco-design/web-vue')['Layout'] ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent'] ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader'] ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider'] ALink: typeof import('@arco-design/web-vue')['Link'] + AList: typeof import('@arco-design/web-vue')['List'] + AListItem: typeof import('@arco-design/web-vue')['ListItem'] + AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta'] AMenu: typeof import('@arco-design/web-vue')['Menu'] AMenuItem: typeof import('@arco-design/web-vue')['MenuItem'] + AModal: typeof import('@arco-design/web-vue')['Modal'] AniEmpty: typeof import('./../components/empty/AniEmpty.vue')['default'] + APagination: typeof import('@arco-design/web-vue')['Pagination'] + APopover: typeof import('@arco-design/web-vue')['Popover'] + AProgress: typeof import('@arco-design/web-vue')['Progress'] + ARadio: typeof import('@arco-design/web-vue')['Radio'] + ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup'] AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar'] + ASelect: typeof import('@arco-design/web-vue')['Select'] ASpin: typeof import('@arco-design/web-vue')['Spin'] + ASwitch: typeof import('@arco-design/web-vue')['Switch'] + ATabPane: typeof import('@arco-design/web-vue')['TabPane'] + ATabs: typeof import('@arco-design/web-vue')['Tabs'] + ATag: typeof import('@arco-design/web-vue')['Tag'] + ATextarea: typeof import('@arco-design/web-vue')['Textarea'] ATooltip: typeof import('@arco-design/web-vue')['Tooltip'] + ATree: typeof import('@arco-design/web-vue')['Tree'] + AUpload: typeof import('@arco-design/web-vue')['Upload'] BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default'] BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default'] BreadPage: typeof import('./../components/breadcrumb/bread-page.vue')['default']