diff --git a/src/components/AnTable.1/Table.tsx b/src/components/AnTable.1/Table.tsx new file mode 100644 index 0000000..b221694 --- /dev/null +++ b/src/components/AnTable.1/Table.tsx @@ -0,0 +1,367 @@ +import { AnForm, AnFormInstance, AnFormModal, AnFormModalInstance, AnFormModalProps, AnFormProps, getModel } from '@/components/AnForm'; +import AnEmpty from '@/components/AnEmpty/AnEmpty.vue'; +import { Button, PaginationProps, Table, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue'; +import { isArray, merge } from 'lodash-es'; +import { InjectionKey, PropType, Ref, VNodeChild, defineComponent, ref } from 'vue'; + +type DataFn = (params: { page: number; size: number; [key: string]: any }) => any | Promise; +export type ArcoTableProps = Omit; +export const AnTableContextKey = Symbol('AnTableContextKey') as InjectionKey; +export type TableColumnRender = (data: { record: TableData; column: TableColumnData; rowIndex: number }) => VNodeChild; + +export type ArcoTableSlots = { + /** + * 自定义 th 元素 + */ + th?: (column: TableColumnData) => any; + + /** + * 自定义 thead 元素 + */ + thead?: () => any; + + /** + * 空白展示 + */ + empty?: () => any; + + /** + * 总结行 + */ + 'summary-cell'?: (column: TableColumnData, record: TableData, rowIndex: number) => any; + + /** + * 分页器右侧内容 + */ + 'pagination-right'?: () => any; + + /** + * 分页器左侧内容 + */ + 'pagination-left'?: () => any; + + /** + * 自定义 td 元素 + */ + td?: (column: TableColumnData, record: TableData, rowIndex: number) => any; + + /** + * 自定义 tr 元素 + */ + tr?: (column: TableColumnData, record: TableData, rowIndex: number) => any; + + /** + * 自定义 tbody 元素 + */ + tbody?: () => any; + + /** + * 拖拽锚点图标 + */ + 'drag-handle-icon'?: () => any; + + /** + * 表格底部 + */ + footer?: () => any; + + /** + * 展开行内容 + */ + 'expand-row'?: () => any; + + /** + * 展开行图标 + */ + 'expand-icon'?: () => any; + + /** + * 表格列定义。启用时会屏蔽 columns 属性 + */ + columns?: () => any; +}; + +/** + * 表格组件 + */ +export const AnTable = defineComponent({ + name: 'AnTable', + props: { + /** + * 表格数据 + * @description 数组或函数,函数请返回数组或 `{ data, total }` 对象 + * @example + * ```ts + * async data(params) { + * const res = await api.xxx(params); + * const { data, total } = res; + * return { data, total } + * } + * ``` + */ + data: { + type: [Array, Function] as PropType, + }, + /** + * 表格列 + * @example + * ```ts + * [ + * { + * title: "名字", + * dataIndex: "name" + * } + * ] + * ``` + */ + columns: { + type: Array as PropType, + default: () => [], + }, + /** + * 分页配置 + * @example + * ```ts + * { + * showTotal: true + * } + * ``` + */ + paging: { + type: Object as PropType, + }, + /** + * 搜索表单 + * @example + * ```ts + * [ + * { + * label: "姓名关键字", + * setter: "input", + * } + * ] + * ``` + */ + search: { + type: Object as PropType, + }, + /** + * 新建弹窗 + */ + create: { + type: Object as PropType, + }, + /** + * 修改弹窗 + */ + modify: { + type: Object as PropType, + }, + /** + * 操作按钮 + */ + actions: { + type: Array as PropType, + }, + /** + * 部件 + */ + widgets: { + type: Array as PropType, + }, + /** + * 传递给 Table 组件的属性 + */ + tableProps: { + type: Object as PropType, + }, + /** + * 传递给 Table 组件的插槽 + */ + tableSlots: { + type: Object as PropType, + }, + }, + setup(props) { + const loading = ref(false); + const renderData = ref([]); + const tableRef = ref(null); + const searchRef = ref(null); + const createRef = ref(null); + const modifyRef = ref(null); + const selected = ref([]); + const selectedKeys = computed(() => selected.value.map(i => i[props.tableProps?.rowKey ?? 'id'])); + + const setPaging = (paging: PaginationProps) => { + if (props.paging) { + merge(props.paging, paging); + } + }; + + const resetPaging = () => { + setPaging({ current: 1, pageSize: 10 }); + }; + + const loadData = async () => { + if (!props.data || Array.isArray(props.data)) { + return; + } + + if (await searchRef.value?.validate()) { + return; + } + + const page = props.paging?.current ?? 1; + const size = props.paging?.pageSize ?? 10; + const search = getModel(props.search?.model ?? {}); + + loading.value = true; + try { + const params = { ...search, page, size }; + const resData = await props.data(params); + if (resData) { + let data: TableData[] = []; + let total = 0; + if (isArray(resData)) { + data = resData; + total = resData.length; + } else { + data = resData.data ?? []; + total = resData.total ?? 0; + } + renderData.value = data; + props.paging?.showTotal && (props.paging.total = total); + } + } catch (e) { + console.log('AnTable load fail: ', e); + } + loading.value = false; + }; + + const load = (page?: number, size?: number) => { + if (props.paging) { + page && (props.paging.current = page); + size && (props.paging.pageSize = size); + } + loadData(); + }; + + const reload = () => load(1, 10); + + watchEffect(() => { + if (Array.isArray(props.data)) { + renderData.value = props.data; + resetPaging(); + } + }); + + onMounted(() => { + loadData(); + }); + + return { + loading, + renderData, + selected, + selectedKeys, + tableRef, + searchRef, + createRef, + modifyRef, + load, + reload, + refresh: loadData, + }; + }, + render() { + return ( +
+ {(this.create || this.actions || this.search || this.widgets) && ( +
+ {this.create && } + {this.actions &&
{this.actions.map(action => action())}
} + {this.search && ( +
+ +
+ )} + {this.widgets &&
{this.widgets.map(widget => widget())}
} +
+ )} + + + {{ + empty: () => , + ...this.tableSlots, + }} +
+ + {this.modify && } +
+ ); + }, +}); + +/** + * 表格组件实例 + */ +export type AnTableInstance = InstanceType; + +/** + * 表格组件参数 + */ +export type AnTableProps = Pick; + +export interface AnTableContext { + /** + * 是否加载中 + */ + loading: Ref; + /** + * 表格实例 + */ + tableRef: Ref; + /** + * 搜索表单实例 + */ + searchRef: Ref; + /** + * 新增弹窗实例 + */ + createRef: Ref; + /** + * 修改弹窗实例 + */ + modifyRef: Ref; + /** + * 当前表格数据 + */ + renderData: Ref; + /** + * 加载数据 + */ + loadData: () => Promise; + /** + * 重置加载 + */ + reload: () => Promise; + /** + * 重新加载 + */ + refresh: () => Promise; + /** + * 原表格参数 + */ + props: AnTableProps; + onPageChange: any; + onPageSizeChange: any; +} diff --git a/src/components/AnTable.1/index.ts b/src/components/AnTable.1/index.ts new file mode 100644 index 0000000..ef10c2e --- /dev/null +++ b/src/components/AnTable.1/index.ts @@ -0,0 +1,5 @@ +export * from './Table'; +export * from './useTable'; +export * from './useTableColumns'; +export * from './useSearchForm'; +export * from './useModiyForm'; \ No newline at end of file diff --git a/src/components/AnTable.1/useCreateForm.tsx b/src/components/AnTable.1/useCreateForm.tsx new file mode 100644 index 0000000..e5f1f60 --- /dev/null +++ b/src/components/AnTable.1/useCreateForm.tsx @@ -0,0 +1,21 @@ +import { FormModalUseOptions, useFormModalProps } from '@/components/AnForm'; + +export type UseCreateFormOptions = FormModalUseOptions & {}; + +export function useCreateForm(options: UseCreateFormOptions) { + if (options.width) { + if (!options.modalProps) { + (options as any).modalProps = {}; + } + (options.modalProps as any).width = options.width; + delete options.width; + } + if (options.formClass) { + if (!options.formProps) { + (options as any).formProps = {}; + } + options.formProps!.class = options.formClass; + delete options.formClass; + } + return useFormModalProps(options); +} diff --git a/src/components/AnTable.1/useModiyForm.tsx b/src/components/AnTable.1/useModiyForm.tsx new file mode 100644 index 0000000..0303225 --- /dev/null +++ b/src/components/AnTable.1/useModiyForm.tsx @@ -0,0 +1,88 @@ +import { FormItem, FormModalUseOptions, useFormModalProps, AnFormModalProps } from '@/components/AnForm'; +import { cloneDeep, merge } from 'lodash-es'; +import { ExtendFormItem } from './useSearchForm'; +import { TableUseOptions } from './useTable'; +import { AnTableInstance } from './Table'; + +export type ModifyForm = Omit & { + /** + * 是否继承新建弹窗 + * @default + * ```ts + * false + * ``` + */ + extend?: boolean; + /** + * 表单项 + * ```tsx + * [{ + * extend: 'name', // 从 create.items 中继承 + * }] + * ``` + */ + items?: ExtendFormItem[]; +}; + +export function useModifyForm(options: TableUseOptions, createModel: Recordable, tableRef: Ref): AnFormModalProps | undefined { + const { create, modify, columns } = options; + + if (!modify) { + return undefined; + } + + for(const column of columns ?? []) { + if(column.type === 'button') { + const btn = column.buttons.find(i => i.type === 'modify') + if(!btn) { + column.buttons.unshift({ + text: '修改', + type: 'modify', + onClick({ record }) { + tableRef.value?.modifyRef?.open(record); + } + }) + } + break; + } + } + + let result: FormModalUseOptions = { items: [], model: cloneDeep(createModel) }; + if (modify.extend && create) { + result = merge({}, create); + } + result = merge(result, modify); + + if (modify.items) { + const items: FormItem[] = []; + const createItemMap: Record = {}; + for (const item of create?.items ?? []) { + createItemMap[item.field] = item; + } + for (let item of modify.items ?? []) { + if (item.extend) { + item = merge({}, createItemMap[item.field!] ?? {}, item); + } + items.push(item as any); + } + if (items.length) { + result.items = items; + } + } + + if (modify.width || create?.width) { + if (!result.modalProps) { + (result as any).modalProps = {}; + } + (result.modalProps as any).width = modify.width || create?.width; + } + + if (modify.formClass || create?.formClass) { + if (!result.formProps) { + (result as any).formProps = {}; + } + result.formProps!.class = modify.formClass || create?.formClass; + } + + return useFormModalProps(result); +} diff --git a/src/components/AnTable.1/useSearchForm.tsx b/src/components/AnTable.1/useSearchForm.tsx new file mode 100644 index 0000000..f8c4a7e --- /dev/null +++ b/src/components/AnTable.1/useSearchForm.tsx @@ -0,0 +1,114 @@ +import { defaultsDeep, isArray, merge } from 'lodash-es'; +import { AnFormProps, FormUseOptions, AnFormItemProps, FormItem, useFormItems } from '@/components/AnForm'; +import { AnTableInstance, AnTableProps } from './Table'; + +export type ExtendFormItem = Partial< + FormItem & { + /** + * 从新建弹窗继承表单项 + * @example + * ```ts + * 'name' + * ``` + */ + extend: string; + } +>; + +export type SearchFormItem = ExtendFormItem & { + /** + * 是否点击图标后进行搜索 + * @description 仅 setter: 'search' 类型可用 + * @default + * ```ts + * false + * ``` + */ + searchable?: boolean; + /** + * 是否回车后进行查询 + * @default + * ```ts + * false + * ``` + */ + enterable?: boolean; +}; + +export type SearchForm = Omit & { + /** + * 搜索表单项 + * @example + * ```ts + * [{ + * extend: 'name' // 从 create.items 继承 + * }] + * ``` + */ + items?: SearchFormItem[]; + /** + * 是否隐藏查询按钮 + * @default + * ```tsx + * false + * ``` + */ + hideSearch?: boolean; +}; + +export function useSearchForm(search: SearchForm | SearchFormItem[] | null, extendItems: AnFormItemProps[] = [], tableRef: Ref): AnFormProps | undefined { + if (!search) { + return undefined; + } + + if (isArray(search)) { + search = { items: search }; + } + + const { items: _items = [], hideSearch, model: _model, formProps: _formProps } = search; + const extendMap = extendItems.reduce((m, v) => ((m[v.field] = v), m), {} as Record); + + const props = { + items: [] as AnFormItemProps[], + model: _model ?? {}, + formProps: defaultsDeep({}, _formProps, { layout: 'inline' }), + }; + + const defualts: Partial = { + setter: 'input', + itemProps: { + hideLabel: true, + }, + setterProps: {}, + }; + + const items: AnFormItemProps[] = []; + + for (const _item of _items) { + const { searchable, enterable, field, extend, ...itemRest } = _item; + if ((field || extend) === 'submit' && hideSearch) { + continue; + } + let item: AnFormItemProps = defaultsDeep({ field }, itemRest, defualts); + if (extend) { + const extendItem = extendMap[extend]; + if (extendItem) { + item = merge({}, extendItem, itemRest); + } + } + if (item.setter === 'search') { + Object.assign(item.setterProps!, { + onSearch: () => tableRef.value?.reload(), + onPressEnter: () => tableRef.value?.reload(), + }); + } + if (item.setterProps) { + (item.setterProps as any).placeholder = (item.setterProps as any).placeholder ?? item.label; + } + items.push(item); + } + + props.items = useFormItems(items, props.model); + + return props; +} diff --git a/src/components/AnTable.1/useTable.tsx b/src/components/AnTable.1/useTable.tsx new file mode 100644 index 0000000..a86000d --- /dev/null +++ b/src/components/AnTable.1/useTable.tsx @@ -0,0 +1,184 @@ +import { useFormModalProps } from '@/components/AnForm'; +import { AnTable, AnTableInstance, AnTableProps } from './Table'; +import { ModifyForm, useModifyForm } from './useModiyForm'; +import { SearchForm, SearchFormItem, useSearchForm } from './useSearchForm'; +import { TableColumn, useTableColumns } from './useTableColumns'; +import { UseCreateFormOptions } from './useCreateForm'; +import { Component, FunctionalComponent } from 'vue'; +import { Button } from '@arco-design/web-vue'; +import { isFunction } from 'lodash-es'; + +export interface TableUseOptions extends Pick { + /** + * 唯一ID + * @example + * ```ts + * 'UserTable' + * ``` + */ + id?: string; + /** + * 表格列 + * @example + * ```ts + * [{ + * dataIndex: 'title', + * title: '标题' + * }] + * ``` + */ + columns?: TableColumn[]; + /** + * 搜索表单 + * @example + * ```ts + * [{ + * field: 'name', + * label: '用户名称', + * setter: 'input' + * }] + * ``` + */ + search?: SearchFormItem[] | SearchForm; + /** + * 新建弹窗 + * @example + * ```ts + * { + * title: '添加用户', + * items: [], + * submit: (model) => {} + * } + * ``` + */ + create?: UseCreateFormOptions; + /** + * 修改弹窗 + * @example + * ```ts + * { + * extend: true, // 基于新建弹窗扩展 + * title: '修改用户', + * submit: (model) => {} + * } + * ``` + */ + modify?: ModifyForm; +} + +function useButtons(buttons: any[], tableRef: Ref) { + const result: Component[] = []; + for (const button of buttons) { + if (button.render) { + result.push(button.render); + continue; + } + result.push(() => { + if (button.visible && !button.visible()) { + return null; + } + return ( + + ); + }); + } + return result; +} + +export function useTableProps(options: TableUseOptions, tableRef: Ref): AnTableProps { + const { data, tableProps = {}, tableSlots } = options; + + const paging = { hide: false, showTotal: true, showPageSize: true, ...(options.paging ?? {}) }; + const search = options.search && useSearchForm(options.search, [], tableRef); + const create = options.create && useFormModalProps(options.create); + const modify = options.modify && useModifyForm(options, create?.model ?? {}, tableRef); + const columns = useTableColumns(options.columns ?? [], tableRef); + const actions = options.actions && useButtons(options.actions, tableRef); + const widgets = options.widgets && useButtons(options.widgets, tableRef); + + const onPageChange = tableProps.onPageChange; + const onPageSizeChange = tableProps.onPageSizeChange; + + tableProps.onPageChange = (page: number) => { + onPageChange?.(page); + tableRef.value?.load(page); + }; + + tableProps.onPageSizeChange = (size: number) => { + onPageSizeChange?.(size); + tableRef.value?.load(1, size); + }; + + const props = { + tableProps, + tableSlots, + columns, + data, + search, + paging, + create, + modify, + actions, + widgets, + }; + + return props; +} + +/** + * + * @param options + * @example + * ```html + * + * diff --git a/src/pages/home/index.vue b/src/pages/home/index.vue index e6ad779..7e35cd3 100644 --- a/src/pages/home/index.vue +++ b/src/pages/home/index.vue @@ -1,6 +1,9 @@ -