feat: 优化表格hook的使用

master
luoer 2023-10-16 17:22:40 +08:00
parent 12e1ca4c63
commit 8c0c5037b5
11 changed files with 298 additions and 114 deletions

View File

@ -75,14 +75,10 @@ export const Form = defineComponent({
try { try {
loading.value = true; loading.value = true;
const res = await props.submit?.({ model, items: props.items }); const res = await props.submit?.({ model, items: props.items });
if (res?.message) { res?.message && Message.success(`提示: ${res.message}`);
Message.success(`提示: ${res.message}`);
}
} catch (error: any) { } catch (error: any) {
const message = error?.response?.data?.message || error?.message || "提交失败"; const message = error?.response?.data?.message || error?.message;
if (message) { message && Message.error(`提示: ${message}`);
Message.error(`提示: ${message}`);
}
} finally { } finally {
loading.value = false; loading.value = false;
} }

View File

@ -1,23 +1,61 @@
import { Modal } from "@arco-design/web-vue"; import { Modal } from "@arco-design/web-vue";
import { assign, merge } from "lodash-es"; import { merge } from "lodash-es";
import { reactive } from "vue"; import { Component, Ref, reactive } from "vue";
import { useForm } from "./use-form"; import { useForm } from "./use-form";
import { FormModalProps } from "./form-modal"; import FormModal, { FormModalInstance, FormModalProps } from "./form-modal";
const defaults: Partial<InstanceType<typeof Modal>> = { const defaults: Partial<InstanceType<typeof Modal>> = {
width: 1080, width: 1080,
titleAlign: "start", titleAlign: "start",
closable: false closable: false,
maskClosable: false,
}; };
/** /**
* FormModal * FormModal
* @see src/components/form/use-form-modal.tsx * @see src/components/form/use-form-modal.tsx
*/ */
export const useFormModal = (options: FormModalProps): FormModalProps & { model: Record<string, any> } => { export const useFormModal = (options: Partial<FormModalProps>): FormModalProps => {
const { model, items } = options || {}; const { model = {}, items = [] } = options || {};
const form = useForm({ model, items }); const form = useForm({ model, items });
return reactive(merge({ modalProps: { ...defaults } }, { ...options, ...form })); return reactive(
merge(
{
modalProps: { ...defaults },
formProps: {
layout: "vertical",
},
},
{
...options,
...form,
}
)
);
};
interface Context {
props: any;
modalRef: Ref<FormModalInstance | null>;
open: (args?: Record<string, any>) => Promise<void> | undefined;
}
export const useAniFormModal = (options: Partial<FormModalProps>): [Component, Context] => {
const props = useFormModal(options);
const modalRef = ref<FormModalInstance | null>(null);
const onModalRef = (el: any) => (modalRef.value = el);
const component = defineComponent({
name: "AniFormModalWrapper",
render() {
return <FormModal {...this.$attrs} {...props} ref={onModalRef} />;
},
});
const component1 = (p: any) => <FormModal {...p} {...props} ref={onModalRef} />;
const context = {
props,
modalRef,
open: (args?: Record<string, any>) => modalRef.value?.open(args),
};
return [component1, context];
}; };

View File

@ -166,13 +166,9 @@ export const Table = defineComponent({
)} )}
<div class={`mb-3 flex justify-between ${!this.inlined && "mt-2"}`}> <div class={`mb-3 flex justify-between ${!this.inlined && "mt-2"}`}>
<div class={`${this.create || this.$slots.action ? null : '!hidden'} flex-1 flex gap-2 `}> <div class={`${this.create || this.$slots.action ? null : "!hidden"} flex-1 flex gap-2 `}>
{this.create && ( {this.create && (
<FormModal <FormModal {...(this.create as any)} ref="createRef" onSubmited={this.reloadData}></FormModal>
{...(this.create as any)}
ref="createRef"
onSubmited={this.reloadData}
></FormModal>
)} )}
{this.modify && ( {this.modify && (
<FormModal <FormModal
@ -184,14 +180,13 @@ export const Table = defineComponent({
)} )}
{this.$slots.action?.()} {this.$slots.action?.()}
</div> </div>
<div> <div>{this.inlined && <Form ref="searchRef" {...this.search}></Form>}</div>
{this.inlined && <Form ref="searchRef" {...this.search}></Form>}
</div>
</div> </div>
<BaseTable <BaseTable
row-key="id" row-key="id"
bordered={false} bordered={false}
{...this.$attrs}
{...this.tableProps} {...this.tableProps}
loading={this.loading} loading={this.loading}
pagination={this.pagination} pagination={this.pagination}
@ -204,6 +199,12 @@ export const Table = defineComponent({
}, },
}); });
/**
*
*/
export type TableInstance = InstanceType<typeof Table>; export type TableInstance = InstanceType<typeof Table>;
/**
*
*/
export type TableProps = TableInstance["$props"]; export type TableProps = TableInstance["$props"];

View File

@ -80,7 +80,7 @@ interface TableColumnDropdown {
doptionProps?: Partial<InstanceType<typeof Doption> & Record<string, any>>; doptionProps?: Partial<InstanceType<typeof Doption> & Record<string, any>>;
} }
export interface UseTableColumn extends TableColumnData { export interface TableColumn extends TableColumnData {
/** /**
* *
*/ */
@ -113,7 +113,7 @@ type Search = Partial<
*/ */
items?: SearchFormItem[]; items?: SearchFormItem[];
/** /**
* bu * /
*/ */
button?: boolean button?: boolean
} }
@ -137,7 +137,7 @@ export interface UseTableOptions extends Omit<TableProps, "search" | "create" |
* *
* @see https://arco.design/web-vue/components/table/#tablecolumn * @see https://arco.design/web-vue/components/table/#tablecolumn
*/ */
columns: UseTableColumn[]; columns: TableColumn[];
/** /**
* *
* @see FormProps * @see FormProps

View File

@ -1,9 +1,9 @@
import { delConfirm } from "@/utils"; import { delConfirm } from "@/utils";
import { Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue"; import { Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue";
import { isArray, merge } from "lodash-es"; import { isArray, merge } from "lodash-es";
import { reactive } from "vue"; import { Component, Ref, reactive } from "vue";
import { useFormModal } from "../form"; import { useFormModal } from "../form";
import { TableInstance } from "./table"; import { Table, TableInstance, TableProps } from "./table";
import { config } from "./table.config"; import { config } from "./table.config";
import { UseTableOptions } from "./use-interface"; import { UseTableOptions } from "./use-interface";
@ -18,15 +18,11 @@ const onClick = async (item: any, columnData: any, getTable: any) => {
try { try {
const resData: any = await item?.onClick?.(columnData); const resData: any = await item?.onClick?.(columnData);
const message = resData?.data?.message; const message = resData?.data?.message;
if (message) { message && Message.success(`提示:${message}`);
Message.success(`提示:${message}`);
}
getTable()?.loadData(); getTable()?.loadData();
} catch (error: any) { } catch (error: any) {
const message = error.response?.data?.message; const message = error.response?.data?.message;
if (message) { message && Message.warning(`提示:${message}`);
Message.warning(`提示:${message}`);
}
} }
return; return;
} }
@ -171,13 +167,13 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
continue; continue;
} }
} }
const search = !item.enableLoad ? undefined : () => getTable().reloadData() const search = !item.enableLoad ? undefined : () => getTable().reloadData();
searchItems.push( searchItems.push(
merge( merge(
{ {
nodeProps: { nodeProps: {
onSearch: search, onSearch: search,
onPressEnter: search onPressEnter: search,
}, },
}, },
item item
@ -213,3 +209,54 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
return reactive({ ...options, columns }); return reactive({ ...options, columns });
}; };
/**
*
*/
interface TableContext {
/**
* ()
*/
props: TableProps;
/**
*
*/
tableRef: Ref<TableInstance | null>;
/**
*
*/
refresh: () => void;
/**
*
*/
reload?: () => void;
}
type TableReturnType = [
/**
*
*/
Component,
/**
*
*/
TableContext
];
export const useAniTable = (options: UseTableOptions): TableReturnType => {
const props = useTable(options);
const tableRef = ref<TableInstance | null>(null);
const context = {
props,
tableRef,
refresh: () => tableRef.value?.reloadData(),
};
const aniTable = defineComponent({
name: "AniTableWrapper",
setup() {
const onRef = (el: TableInstance) => (tableRef.value = el);
return () => <Table ref={onRef} {...props}></Table>;
},
});
return [aniTable, context];
};

View File

@ -44,11 +44,15 @@
:breakpoint="'lg'" :breakpoint="'lg'"
@collapse="onCollapse" @collapse="onCollapse"
> >
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-hidden pt-2"> <a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-hidden pt-0.5">
<Menu /> <Menu />
</a-scrollbar> </a-scrollbar>
<template #trigger="{ collapsed }"> <template #trigger="{ collapsed }">
<i :class="`text-gray-400 text-base ${collapsed ? 'icon-park-outline-expand-left' : 'icon-park-outline-expand-right'}`"></i> <i
:class="`text-gray-400 text-base ${
collapsed ? 'icon-park-outline-expand-left' : 'icon-park-outline-expand-right'
}`"
></i>
</template> </template>
</a-layout-sider> </a-layout-sider>
<a-layout class="layout-content flex-1"> <a-layout class="layout-content flex-1">
@ -56,15 +60,17 @@
<div class="h-full flex items-center justify-between gap-2 px-4"> <div class="h-full flex items-center justify-between gap-2 px-4">
<div class="space-x-2"> <div class="space-x-2">
<a-tag <a-tag
v-for="item in tagItems" v-for="item in appStore.pageTags"
:key="item.text" :key="item.id"
:color="item.active ? 'rgb(var(--primary-6))' : ''" :color="item.path === route.fullPath ? 'blue' : undefined"
:closable="item.showClose" :closable="item.closible"
class="cursor-pointer" class="cursor-pointer"
@mouseenter="item.showClose = true" @mouseenter="item.closable && (item.closible = true)"
@mouseleave="item.showClose = false" @mouseleave="item.closable && (item.closible = false)"
@close="appStore.delPageTag(item)"
@click="router.push(item.path)"
> >
{{ item.text }} {{ item.title }}
</a-tag> </a-tag>
</div> </div>
<div> <div>
@ -77,10 +83,7 @@
</div> </div>
</a-layout-header> </a-layout-header>
<a-layout-content class="overflow-x-auto"> <a-layout-content class="overflow-x-auto">
<a-spin :loading="appStore.pageLoding" tip="正在加载中,请稍等..." class="block h-full w-full"> <a-spin :loading="appStore.pageLoding" tip="加载中,请稍等..." class="block h-full w-full">
<template #icon>
<IconSync></IconSync>
</template>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<component :is="Component"></component> <component :is="Component"></component>
</router-view> </router-view>
@ -94,7 +97,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useAppStore, useUserStore } from "@/store"; import { useAppStore, useUserStore } from "@/store";
import { Message } from "@arco-design/web-vue"; import { Message } from "@arco-design/web-vue";
import { IconSync } from "@arco-design/web-vue/es/icon";
import Menu from "./components/menu.vue"; import Menu from "./components/menu.vue";
import userDropdown from "./components/userDropdown.vue"; import userDropdown from "./components/userDropdown.vue";
@ -106,6 +108,21 @@ const router = useRouter();
const themeConfig = ref({ visible: false }); const themeConfig = ref({ visible: false });
const isDev = import.meta.env.DEV; const isDev = import.meta.env.DEV;
watch(
() => route.path,
() => {
console.log("path change");
appStore.addPageTag({
id: route.fullPath,
path: route.path,
title: route.meta.title!,
});
},
{
immediate: true,
}
);
const onCollapse = (val: boolean) => { const onCollapse = (val: boolean) => {
isCollapsed.value = val; isCollapsed.value = val;
}; };

View File

@ -1,8 +1,9 @@
<template> <template>
<BreadPage> <BreadPage>
<div class="min-h-full grid grid-cols-[auto_auto_1fr]"> <template #content>
<div class="w-[200px]"> <div class="overflow-hidden grid grid-cols-[auto_auto_1fr] m-4 p-4 bg-white">
<div class="flex gap-2"> <div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
<div class="flex gap-2 pr-2">
<a-input-search allow-clear placeholder="分组名称..." class="mb-2"></a-input-search> <a-input-search allow-clear placeholder="分组名称..." class="mb-2"></a-input-search>
<a-button> <a-button>
<template #icon> <template #icon>
@ -10,8 +11,10 @@
</template> </template>
</a-button> </a-button>
</div> </div>
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
<ul class="pr-4 pl-0 mt-0">
<li <li
v-for="i in 10" v-for="i in 50"
class="group flex items-center justify-between gap-2 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer" class="group flex items-center justify-between gap-2 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
> >
<div> <div>
@ -20,13 +23,32 @@
<span class="text-xs text-gray-400">(10)</span> <span class="text-xs text-gray-400">(10)</span>
</div> </div>
<div> <div>
<a-button size="small" type="text" class="!hidden !group-hover:inline-block"> <a-dropdown>
<a-button size="small" type="text">
<template #icon> <template #icon>
<i class="icon-park-outline-more-one text-gray-400 hover:text-gray-700"></i> <i class="icon-park-outline-more-one text-gray-400 hover:text-gray-700"></i>
</template> </template>
</a-button> </a-button>
<template #content>
<a-doption @click="typeCtx.open()">
<template #icon>
<i class="icon-park-outline-edit"></i>
</template>
修改
</a-doption>
<a-doption class="!text-red-500">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
删除
</a-doption>
</template>
</a-dropdown>
<type-modal></type-modal>
</div> </div>
</li> </li>
</ul>
</a-scrollbar>
</div> </div>
<a-divider direction="vertical" :margin="16"></a-divider> <a-divider direction="vertical" :margin="16"></a-divider>
<div> <div>
@ -43,17 +65,34 @@
</Table> </Table>
</div> </div>
</div> </div>
</template>
</BreadPage> </BreadPage>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { api } from "@/api"; import { api } from "@/api";
import { Table, useTable } from "@/components"; import { Table, useAniFormModal, useTable } from "@/components";
import { dayjs } from "@/libs/dayjs"; import { dayjs } from "@/libs/dayjs";
import numeral from "numeral"; import numeral from "numeral";
import AniUpload from './components/upload.vue'; import AniUpload from "./components/upload.vue";
const uploadRef = ref<InstanceType<typeof AniUpload>>() const [typeModal, typeCtx] = useAniFormModal({
title: "修改分组",
trigger: false,
modalProps: {
width: 432,
},
items: [
{
field: "name",
label: "分组名称",
type: "input",
},
],
submit: async () => {},
});
const uploadRef = ref<InstanceType<typeof AniUpload>>();
const getIcon = (mimetype: string) => { const getIcon = (mimetype: string) => {
if (mimetype.startsWith("image")) { if (mimetype.startsWith("image")) {
@ -131,7 +170,7 @@ const table = useTable({
hideLabel: true, hideLabel: true,
}, },
nodeProps: { nodeProps: {
placeholder: "素材名称...", placeholder: "素材名称",
}, },
}, },
], ],

View File

@ -1,16 +1,17 @@
<template> <template>
<BreadPage> <BreadPage>
<Table v-bind="table"></Table> <a-button @click="roleCtx.refresh()"></a-button>
<role-table></role-table>
</BreadPage> </BreadPage>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { api } from "@/api"; import { api } from "@/api";
import { Table, useTable } from "@/components"; import { useAniTable } from "@/components";
import { dayjs } from "@/libs"; import { dayjs } from "@/libs";
const table = useTable({ const [roleTable, roleCtx] = useAniTable({
data: async (model, paging) => { data: async () => {
return api.role.getRoles(); return api.role.getRoles();
}, },
columns: [ columns: [
@ -47,7 +48,7 @@ const table = useTable({
text: "修改", text: "修改",
}, },
{ {
text: '分配权限', text: "分配权限",
onClick: ({ record }) => { onClick: ({ record }) => {
console.log(record); console.log(record);
}, },
@ -58,21 +59,21 @@ const table = useTable({
onClick: ({ record }) => { onClick: ({ record }) => {
return api.role.delRole(record.id); return api.role.delRole(record.id);
}, },
} },
], ],
}, },
], ],
search: { search: {
items: [ items: [
{ {
extend: "name", field: "name",
required: false, type: "input",
nodeProps: { nodeProps: {
placeholder: '请输入角色名称' placeholder: "角色名称",
}, },
itemProps: { itemProps: {
hideLabel: true, hideLabel: true,
} },
}, },
], ],
}, },

View File

@ -1,5 +1,6 @@
import generatedRoutes from "virtual:generated-pages"; import generatedRoutes from "virtual:generated-pages";
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from "vue-router";
import { routes as rawRoutes } from "vue-router/auto/routes";
const APP_ROUTE_NAME = "_layout"; const APP_ROUTE_NAME = "_layout";
@ -13,6 +14,9 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
for (const route of routes) { for (const route of routes) {
if ((route.name as string)?.startsWith("_")) { if ((route.name as string)?.startsWith("_")) {
if (route.name === APP_ROUTE_NAME) {
route.children = appRoutes;
}
route.path = route.path.replace("_", ""); route.path = route.path.replace("_", "");
topRoutes.push(route); topRoutes.push(route);
continue; continue;
@ -20,11 +24,6 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
appRoutes.push(route); appRoutes.push(route);
} }
const appRoute = routes.find((i) => i.name === APP_ROUTE_NAME);
if (appRoute) {
appRoute.children = appRoutes;
}
return [topRoutes, appRoutes]; return [topRoutes, appRoutes];
}; };

View File

@ -1,5 +1,14 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
interface PageTag {
id: string;
title: string;
path: string;
closable?: boolean;
closible?: boolean;
actived?: boolean;
}
export const useAppStore = defineStore({ export const useAppStore = defineStore({
id: "app", id: "app",
state: () => ({ state: () => ({
@ -19,6 +28,16 @@ export const useAppStore = defineStore({
* *
*/ */
pageLoding: false, pageLoding: false,
pageTags: [
{
id: "/",
title: "首页",
path: "/",
closable: false,
closible: false,
actived: false,
},
] as PageTag[],
}), }),
actions: { actions: {
/** /**
@ -48,7 +67,35 @@ export const useAppStore = defineStore({
*/ */
setPageLoading(loading: boolean) { setPageLoading(loading: boolean) {
this.pageLoding = loading; this.pageLoding = loading;
},
/**
*
* @param tag
* @returns
*/
addPageTag(tag: PageTag) {
if (this.pageTags.some((i) => i.id === tag.id)) {
return;
} }
this.pageTags.push({
closable: true,
closible: false,
actived: false,
...tag,
});
console.log(this.pageTags);
},
/**
*
* @param tag
*/
delPageTag(tag: PageTag) {
console.log("del page tag");
const index = this.pageTags.findIndex((i) => i.id === tag.id);
if (index > -1) {
this.pageTags.splice(index, 1);
}
},
}, },
persist: !import.meta.env.DEV, persist: !import.meta.env.DEV,
}); });

View File

@ -41,7 +41,6 @@ declare module '@vue/runtime-core' {
AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta'] 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']
AMenuItemGroup: typeof import('@arco-design/web-vue')['MenuItemGroup']
AModal: typeof import('@arco-design/web-vue')['Modal'] AModal: typeof import('@arco-design/web-vue')['Modal']
APagination: typeof import('@arco-design/web-vue')['Pagination'] APagination: typeof import('@arco-design/web-vue')['Pagination']
APopover: typeof import('@arco-design/web-vue')['Popover'] APopover: typeof import('@arco-design/web-vue')['Popover']