feat: 优化表格hook的使用
parent
12e1ca4c63
commit
8c0c5037b5
|
|
@ -75,14 +75,10 @@ export const Form = defineComponent({
|
|||
try {
|
||||
loading.value = true;
|
||||
const res = await props.submit?.({ model, items: props.items });
|
||||
if (res?.message) {
|
||||
Message.success(`提示: ${res.message}`);
|
||||
}
|
||||
res?.message && Message.success(`提示: ${res.message}`);
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message || "提交失败";
|
||||
if (message) {
|
||||
Message.error(`提示: ${message}`);
|
||||
}
|
||||
const message = error?.response?.data?.message || error?.message;
|
||||
message && Message.error(`提示: ${message}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,61 @@
|
|||
import { Modal } from "@arco-design/web-vue";
|
||||
import { assign, merge } from "lodash-es";
|
||||
import { reactive } from "vue";
|
||||
import { merge } from "lodash-es";
|
||||
import { Component, Ref, reactive } from "vue";
|
||||
import { useForm } from "./use-form";
|
||||
import { FormModalProps } from "./form-modal";
|
||||
import FormModal, { FormModalInstance, FormModalProps } from "./form-modal";
|
||||
|
||||
const defaults: Partial<InstanceType<typeof Modal>> = {
|
||||
width: 1080,
|
||||
titleAlign: "start",
|
||||
closable: false
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建传给FormModal组件的参数
|
||||
* @see src/components/form/use-form-modal.tsx
|
||||
*/
|
||||
export const useFormModal = (options: FormModalProps): FormModalProps & { model: Record<string, any> } => {
|
||||
const { model, items } = options || {};
|
||||
|
||||
export const useFormModal = (options: Partial<FormModalProps>): FormModalProps => {
|
||||
const { model = {}, items = [] } = options || {};
|
||||
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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -166,13 +166,9 @@ export const Table = defineComponent({
|
|||
)}
|
||||
|
||||
<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 && (
|
||||
<FormModal
|
||||
{...(this.create as any)}
|
||||
ref="createRef"
|
||||
onSubmited={this.reloadData}
|
||||
></FormModal>
|
||||
<FormModal {...(this.create as any)} ref="createRef" onSubmited={this.reloadData}></FormModal>
|
||||
)}
|
||||
{this.modify && (
|
||||
<FormModal
|
||||
|
|
@ -184,14 +180,13 @@ export const Table = defineComponent({
|
|||
)}
|
||||
{this.$slots.action?.()}
|
||||
</div>
|
||||
<div>
|
||||
{this.inlined && <Form ref="searchRef" {...this.search}></Form>}
|
||||
</div>
|
||||
<div>{this.inlined && <Form ref="searchRef" {...this.search}></Form>}</div>
|
||||
</div>
|
||||
|
||||
<BaseTable
|
||||
row-key="id"
|
||||
bordered={false}
|
||||
{...this.$attrs}
|
||||
{...this.tableProps}
|
||||
loading={this.loading}
|
||||
pagination={this.pagination}
|
||||
|
|
@ -204,6 +199,12 @@ export const Table = defineComponent({
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 表格组件实例
|
||||
*/
|
||||
export type TableInstance = InstanceType<typeof Table>;
|
||||
|
||||
/**
|
||||
* 表格组件参数
|
||||
*/
|
||||
export type TableProps = TableInstance["$props"];
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ interface TableColumnDropdown {
|
|||
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[];
|
||||
/**
|
||||
* bu
|
||||
* 显示/隐藏搜索按钮
|
||||
*/
|
||||
button?: boolean
|
||||
}
|
||||
|
|
@ -137,7 +137,7 @@ export interface UseTableOptions extends Omit<TableProps, "search" | "create" |
|
|||
* 表格列配置
|
||||
* @see https://arco.design/web-vue/components/table/#tablecolumn
|
||||
*/
|
||||
columns: UseTableColumn[];
|
||||
columns: TableColumn[];
|
||||
/**
|
||||
* 搜索表单配置
|
||||
* @see FormProps
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { delConfirm } from "@/utils";
|
||||
import { Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue";
|
||||
import { isArray, merge } from "lodash-es";
|
||||
import { reactive } from "vue";
|
||||
import { Component, Ref, reactive } from "vue";
|
||||
import { useFormModal } from "../form";
|
||||
import { TableInstance } from "./table";
|
||||
import { Table, TableInstance, TableProps } from "./table";
|
||||
import { config } from "./table.config";
|
||||
import { UseTableOptions } from "./use-interface";
|
||||
|
||||
|
|
@ -18,15 +18,11 @@ const onClick = async (item: any, columnData: any, getTable: any) => {
|
|||
try {
|
||||
const resData: any = await item?.onClick?.(columnData);
|
||||
const message = resData?.data?.message;
|
||||
if (message) {
|
||||
Message.success(`提示:${message}`);
|
||||
}
|
||||
message && Message.success(`提示:${message}`);
|
||||
getTable()?.loadData();
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message;
|
||||
if (message) {
|
||||
Message.warning(`提示:${message}`);
|
||||
}
|
||||
message && Message.warning(`提示:${message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -171,13 +167,13 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
|
|||
continue;
|
||||
}
|
||||
}
|
||||
const search = !item.enableLoad ? undefined : () => getTable().reloadData()
|
||||
const search = !item.enableLoad ? undefined : () => getTable().reloadData();
|
||||
searchItems.push(
|
||||
merge(
|
||||
{
|
||||
nodeProps: {
|
||||
onSearch: search,
|
||||
onPressEnter: search
|
||||
onPressEnter: search,
|
||||
},
|
||||
},
|
||||
item
|
||||
|
|
@ -213,3 +209,54 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
|
|||
|
||||
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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,11 +44,15 @@
|
|||
:breakpoint="'lg'"
|
||||
@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 />
|
||||
</a-scrollbar>
|
||||
<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>
|
||||
</a-layout-sider>
|
||||
<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="space-x-2">
|
||||
<a-tag
|
||||
v-for="item in tagItems"
|
||||
:key="item.text"
|
||||
:color="item.active ? 'rgb(var(--primary-6))' : ''"
|
||||
:closable="item.showClose"
|
||||
v-for="item in appStore.pageTags"
|
||||
:key="item.id"
|
||||
:color="item.path === route.fullPath ? 'blue' : undefined"
|
||||
:closable="item.closible"
|
||||
class="cursor-pointer"
|
||||
@mouseenter="item.showClose = true"
|
||||
@mouseleave="item.showClose = false"
|
||||
@mouseenter="item.closable && (item.closible = true)"
|
||||
@mouseleave="item.closable && (item.closible = false)"
|
||||
@close="appStore.delPageTag(item)"
|
||||
@click="router.push(item.path)"
|
||||
>
|
||||
{{ item.text }}
|
||||
{{ item.title }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -77,10 +83,7 @@
|
|||
</div>
|
||||
</a-layout-header>
|
||||
<a-layout-content class="overflow-x-auto">
|
||||
<a-spin :loading="appStore.pageLoding" tip="正在加载中,请稍等..." class="block h-full w-full">
|
||||
<template #icon>
|
||||
<IconSync></IconSync>
|
||||
</template>
|
||||
<a-spin :loading="appStore.pageLoding" tip="加载中,请稍等..." class="block h-full w-full">
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component"></component>
|
||||
</router-view>
|
||||
|
|
@ -94,7 +97,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { useAppStore, useUserStore } from "@/store";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { IconSync } from "@arco-design/web-vue/es/icon";
|
||||
import Menu from "./components/menu.vue";
|
||||
import userDropdown from "./components/userDropdown.vue";
|
||||
|
||||
|
|
@ -106,6 +108,21 @@ const router = useRouter();
|
|||
const themeConfig = ref({ visible: false });
|
||||
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) => {
|
||||
isCollapsed.value = val;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<div class="min-h-full grid grid-cols-[auto_auto_1fr]">
|
||||
<div class="w-[200px]">
|
||||
<div class="flex gap-2">
|
||||
<template #content>
|
||||
<div class="overflow-hidden grid grid-cols-[auto_auto_1fr] m-4 p-4 bg-white">
|
||||
<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-button>
|
||||
<template #icon>
|
||||
|
|
@ -10,8 +11,10 @@
|
|||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||
<ul class="pr-4 pl-0 mt-0">
|
||||
<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"
|
||||
>
|
||||
<div>
|
||||
|
|
@ -20,13 +23,32 @@
|
|||
<span class="text-xs text-gray-400">(10)</span>
|
||||
</div>
|
||||
<div>
|
||||
<a-button size="small" type="text" class="!hidden !group-hover:inline-block">
|
||||
<a-dropdown>
|
||||
<a-button size="small" type="text">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-more-one text-gray-400 hover:text-gray-700"></i>
|
||||
</template>
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
<a-divider direction="vertical" :margin="16"></a-divider>
|
||||
<div>
|
||||
|
|
@ -43,17 +65,34 @@
|
|||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from "@/api";
|
||||
import { Table, useTable } from "@/components";
|
||||
import { Table, useAniFormModal, useTable } from "@/components";
|
||||
import { dayjs } from "@/libs/dayjs";
|
||||
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) => {
|
||||
if (mimetype.startsWith("image")) {
|
||||
|
|
@ -131,7 +170,7 @@ const table = useTable({
|
|||
hideLabel: true,
|
||||
},
|
||||
nodeProps: {
|
||||
placeholder: "素材名称...",
|
||||
placeholder: "素材名称",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<Table v-bind="table"></Table>
|
||||
<a-button @click="roleCtx.refresh()">刷新</a-button>
|
||||
<role-table></role-table>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from "@/api";
|
||||
import { Table, useTable } from "@/components";
|
||||
import { useAniTable } from "@/components";
|
||||
import { dayjs } from "@/libs";
|
||||
|
||||
const table = useTable({
|
||||
data: async (model, paging) => {
|
||||
const [roleTable, roleCtx] = useAniTable({
|
||||
data: async () => {
|
||||
return api.role.getRoles();
|
||||
},
|
||||
columns: [
|
||||
|
|
@ -47,7 +48,7 @@ const table = useTable({
|
|||
text: "修改",
|
||||
},
|
||||
{
|
||||
text: '分配权限',
|
||||
text: "分配权限",
|
||||
onClick: ({ record }) => {
|
||||
console.log(record);
|
||||
},
|
||||
|
|
@ -58,21 +59,21 @@ const table = useTable({
|
|||
onClick: ({ record }) => {
|
||||
return api.role.delRole(record.id);
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
extend: "name",
|
||||
required: false,
|
||||
field: "name",
|
||||
type: "input",
|
||||
nodeProps: {
|
||||
placeholder: '请输入角色名称'
|
||||
placeholder: "角色名称",
|
||||
},
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import generatedRoutes from "virtual:generated-pages";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import { routes as rawRoutes } from "vue-router/auto/routes";
|
||||
|
||||
const APP_ROUTE_NAME = "_layout";
|
||||
|
||||
|
|
@ -13,6 +14,9 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
|||
|
||||
for (const route of routes) {
|
||||
if ((route.name as string)?.startsWith("_")) {
|
||||
if (route.name === APP_ROUTE_NAME) {
|
||||
route.children = appRoutes;
|
||||
}
|
||||
route.path = route.path.replace("_", "");
|
||||
topRoutes.push(route);
|
||||
continue;
|
||||
|
|
@ -20,11 +24,6 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
|||
appRoutes.push(route);
|
||||
}
|
||||
|
||||
const appRoute = routes.find((i) => i.name === APP_ROUTE_NAME);
|
||||
if (appRoute) {
|
||||
appRoute.children = appRoutes;
|
||||
}
|
||||
|
||||
return [topRoutes, appRoutes];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
interface PageTag {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
closable?: boolean;
|
||||
closible?: boolean;
|
||||
actived?: boolean;
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore({
|
||||
id: "app",
|
||||
state: () => ({
|
||||
|
|
@ -19,6 +28,16 @@ export const useAppStore = defineStore({
|
|||
* 页面是否加载中,用于路由首次加载
|
||||
*/
|
||||
pageLoding: false,
|
||||
pageTags: [
|
||||
{
|
||||
id: "/",
|
||||
title: "首页",
|
||||
path: "/",
|
||||
closable: false,
|
||||
closible: false,
|
||||
actived: false,
|
||||
},
|
||||
] as PageTag[],
|
||||
}),
|
||||
actions: {
|
||||
/**
|
||||
|
|
@ -48,7 +67,35 @@ export const useAppStore = defineStore({
|
|||
*/
|
||||
setPageLoading(loading: boolean) {
|
||||
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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ declare module '@vue/runtime-core' {
|
|||
AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta']
|
||||
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
||||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||
AMenuItemGroup: typeof import('@arco-design/web-vue')['MenuItemGroup']
|
||||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
||||
|
|
|
|||
Loading…
Reference in New Issue