From 89b1de9b02ebfdf48dcb8cbc5b09f8703e767d39 Mon Sep 17 00:00:00 2001 From: luoer Date: Mon, 7 Aug 2023 17:43:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=99=BB=E9=99=86?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC/=E9=80=80=E5=87=BA=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- src/api/instance/api.ts | 25 ++++- src/components/breadcrumb/bread-page.vue | 8 +- src/components/form/form-modal.tsx | 4 +- src/components/table/table.config.tsx | 9 +- src/components/table/use-interface.ts | 71 ++++++------ src/components/table/use-table.tsx | 15 ++- src/pages/[..._all]/index.vue | 4 +- src/pages/_app/index.vue | 15 +-- src/pages/_login/index.vue | 6 +- src/pages/demo/test.vue | 134 ++++++++++++++++++++++- src/pages/home/index.vue | 30 +++-- src/pages/permission/index.vue | 4 +- src/pages/post/index.vue | 92 +++++++++++++++- src/pages/role/index.vue | 2 +- src/pages/user/index.vue | 2 +- src/router/guards/guard-auth.ts | 27 +++-- src/router/guards/guard-nprogress.ts | 3 +- src/router/guards/guard-title.ts | 5 +- src/router/router/index.ts | 8 +- src/router/routes/index.ts | 27 +---- src/types/auto-component.d.ts | 6 + 22 files changed, 362 insertions(+), 137 deletions(-) diff --git a/.env b/.env index ac23070..5556db0 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ # 应用配置 # ===================================================================================== # 网站标题 -VITE_TITLE = 绝弹管理系统 +VITE_TITLE = 绝弹中心 # 网站副标题 VITE_SUBTITLE = 快速开发web应用的模板工具 # API接口前缀:参见 axios 的 baseURL diff --git a/src/api/instance/api.ts b/src/api/instance/api.ts index e7420f2..e385ad4 100644 --- a/src/api/instance/api.ts +++ b/src/api/instance/api.ts @@ -1,14 +1,27 @@ import { IToastOptions, toast } from "@/components"; import { store, useUserStore } from "@/store"; import { Api } from "../service/Api"; +import { Message } from "@arco-design/web-vue"; -const userStore = useUserStore(store); +class Service extends Api { + /** + * 登陆过期处理函数 + */ + tokenExpiredHandler: () => void = () => {}; + /** + * 设置登陆过期后的处理函数 + * @param handler + */ + setTokenExpiredHandler(handler: () => void) { + this.tokenExpiredHandler = handler; + } +} /** * API 接口实例 * @see src/api/instance/instance.ts */ -export const api = new Api({ +export const api = new Service({ baseURL: import.meta.env.VITE_API_PREFIX, }); @@ -17,6 +30,7 @@ export const api = new Api({ */ api.instance.interceptors.request.use( (config) => { + const userStore = useUserStore(store); if (userStore.accessToken) { config.headers.Authorization = `Bearer ${userStore.accessToken}`; } @@ -50,11 +64,18 @@ api.instance.interceptors.response.use( return res; }, (error) => { + const userStore = useUserStore(store); error.config.closeToast?.(); if (error.response) { console.log("response error", error.response); + const code = error.response.data?.code; + if (code === 4050 || code === 4051) { + userStore.clearUser(); + api.tokenExpiredHandler?.(); + } } else if (error.request) { console.log("request error", error.request); + Message.error(`提示:请求失败,检查网络状态或参数格式!`); } return Promise.reject(error); } diff --git a/src/components/breadcrumb/bread-page.vue b/src/components/breadcrumb/bread-page.vue index e276368..4086020 100644 --- a/src/components/breadcrumb/bread-page.vue +++ b/src/components/breadcrumb/bread-page.vue @@ -1,9 +1,11 @@ diff --git a/src/components/form/form-modal.tsx b/src/components/form/form-modal.tsx index 2a4a379..2733f97 100644 --- a/src/components/form/form-modal.tsx +++ b/src/components/form/form-modal.tsx @@ -61,7 +61,7 @@ export const FormModal = defineComponent({ * @description 可返回`{ message }`类型,用于显示提示信息 */ submit: { - type: Function as PropType<(arg: { model: Record; items: IFormItem[] }) => any | Promise>, + type: Function as PropType<(args: { model: any; items: IFormItem[] }) => PromiseLike>, default: () => true, }, /** @@ -191,7 +191,5 @@ export const FormModal = defineComponent({ }); export type FormModalInstance = InstanceType; - export type FormModalProps = FormModalInstance["$props"]; - export default FormModal; diff --git a/src/components/table/table.config.tsx b/src/components/table/table.config.tsx index 1c037bb..fb9780e 100644 --- a/src/components/table/table.config.tsx +++ b/src/components/table/table.config.tsx @@ -45,7 +45,12 @@ export const config = { title: "序号", width: 60, align: "center", - render: ({ rowIndex }: any) => rowIndex + 1, + render: ({ rowIndex }: any) => { + const table = inject("ref:table"); + const page = table.pagination.current; + const size = table.pagination.pageSize; + return size * (page - 1) + rowIndex + 1; + }, }, columnButtonBase: { buttonProps: { @@ -63,5 +68,5 @@ export const config = { getApiErrorMessage(error: any): string { const message = error?.response?.data?.message || error?.message || "请求失败"; return message; - } + }, }; diff --git a/src/components/table/use-interface.ts b/src/components/table/use-interface.ts index 5f3ad01..21a88c7 100644 --- a/src/components/table/use-interface.ts +++ b/src/components/table/use-interface.ts @@ -23,28 +23,23 @@ export interface TableColumnButton { * 按钮文本 */ text?: string; - /** * 操作类型 * @description `delete` 需配置`onClick`属性,`modify` 需配置根对象下的 `modify` 属性 */ type?: "delete" | "modify"; - /** * 处理函数 */ - onClick?: (data: UseColumnRenderOptions, openModify?: (model: Record) => void) => void; - + onClick?: (data: UseColumnRenderOptions) => any; /** * 是否禁用按钮 */ disabled?: (data: UseColumnRenderOptions) => boolean; - /** * 是否显示按钮 */ visible?: (data: UseColumnRenderOptions) => boolean; - /** * 传递给按钮的props */ @@ -56,13 +51,41 @@ export interface UseTableColumn extends TableColumnData { * 表格列类型 */ type?: "index" | "button"; - /** * 按钮配置列表 */ buttons?: TableColumnButton[]; } +type ExtendedFormItem = Partial & { + /** + * 继承 `create.items` 中指定 `field` 值的项 + */ + extend?: string; +}; + +type Search = Partial< + Omit & { + /** + * 表单项 + */ + items?: ExtendedFormItem[]; + } +>; + +type Modify = Partial< + Omit & { + /** + * 是否继承 `create` 弹窗配置 + */ + extend: boolean; + /** + * 表单项 + */ + items?: ExtendedFormItem[]; + } +>; + export interface UseTableOptions extends Omit { /** * 表格列配置 @@ -73,24 +96,7 @@ export interface UseTableOptions extends Omit & { - /** - * 表单项 - */ - items?: (Partial & { - /** - * 继承`create.items`中指定field值的项 - */ - extend?: string; - })[]; - } - >; - /** - * common props for `create` and `modify` modal - * @see FormModalProps - */ - common?: Partial; + search?: Search; /** * 新建弹窗配置 */ @@ -98,20 +104,7 @@ export interface UseTableOptions extends Omit & { - /** - * 是否继承`create`弹窗配置 - */ - extend: boolean; - items?: (FormModalProps["items"][number] & { - /** - * 继承`create.items`弹窗配置中指定field值的项 - */ - extend?: string; - })[]; - } - >; + modify?: Modify; /** * 详情弹窗配置 */ diff --git a/src/components/table/use-table.tsx b/src/components/table/use-table.tsx index bc3b679..ccab7f2 100644 --- a/src/components/table/use-table.tsx +++ b/src/components/table/use-table.tsx @@ -28,13 +28,9 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)) const modifyAction = column.buttons.find((i) => i.type === "modify"); if (modifyAction) { const { onClick } = modifyAction; - modifyAction.onClick = (columnData) => { - const fn = (data: Record) => getTable()?.openModifyModal(data); - if (isFunction(onClick)) { - onClick(columnData, fn); - } else { - fn(columnData); - } + modifyAction.onClick = async (columnData) => { + const result = (await onClick?.(columnData)) || columnData; + getTable()?.openModifyModal(result); }; } else { column.buttons.unshift({ @@ -53,7 +49,10 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)) onOk: async () => { try { const resData: any = await action?.onClick?.(data); - resData.msg && Message.success(resData?.msg || ""); + const message = resData?.data?.message; + if (message) { + Message.success(`提示:${message}`); + } getTable()?.loadData(); } catch (error: any) { const message = error.response?.data?.message; diff --git a/src/pages/[..._all]/index.vue b/src/pages/[..._all]/index.vue index e786e59..eaf1a3e 100644 --- a/src/pages/[..._all]/index.vue +++ b/src/pages/[..._all]/index.vue @@ -12,13 +12,13 @@ - 返回上页 + 返回 - 返回首页 + 首页 diff --git a/src/pages/_app/index.vue b/src/pages/_app/index.vue index 5ed49d7..12cba94 100644 --- a/src/pages/_app/index.vue +++ b/src/pages/_app/index.vue @@ -22,7 +22,7 @@ - + {{ userStore.nickname }} @@ -80,6 +80,7 @@ import Menu from "./components/menu.vue"; const appStore = useAppStore(); const userStore = useUserStore(); const isCollapsed = ref(false); +const route = useRoute(); const router = useRouter(); const themeConfig = ref({ visible: false }); const onCollapse = (val: boolean) => { @@ -115,15 +116,9 @@ const userButtons = [ icon: "icon-park-outline-logout", text: "退出登录", onClick: async () => { - userStore.clearUser() - Message.loading({ - content: '提示: 正在退出,请稍后...', - duration: 2000, - onClose: () => { - Message.success(`提示: 已成功退出登录!`) - router.push({ name: "_login" }); - } - }) + userStore.clearUser(); + Message.success("提示:已退出登陆!"); + router.push({ path: "/login", query: { redirect: route.path } }); }, }, ]; diff --git a/src/pages/_login/index.vue b/src/pages/_login/index.vue index d3fdd7a..02c7081 100644 --- a/src/pages/_login/index.vue +++ b/src/pages/_login/index.vue @@ -65,6 +65,7 @@ const meridiem = dayjs.localeData().meridiem(dayjs().hour(), dayjs().minute()); const appStore = useAppStore(); const userStore = useUserStore(); const model = reactive({ username: "juetan", password: "juetan" }); +const route = useRoute(); const router = useRouter(); const loading = ref(false); const formRef = ref>(); @@ -102,9 +103,8 @@ const onSubmitClick = async () => { loading.value = true; const res = await api.auth.login(model); userStore.setUser(res.data.data); - userStore.username = res.data.data.username; - Message.success(`欢迎回来,${res.data.data.username}!`) - router.push({ path: "/" }); + Message.success(`欢迎回来,${res.data.data.username}!`); + router.push({ path: (route.query.redirect as string) || "/" }); } catch (error: any) { const message = error?.response?.data?.message; if (message) { diff --git a/src/pages/demo/test.vue b/src/pages/demo/test.vue index 037306b..8721447 100644 --- a/src/pages/demo/test.vue +++ b/src/pages/demo/test.vue @@ -1,10 +1,140 @@ - + { diff --git a/src/pages/home/index.vue b/src/pages/home/index.vue index 7d6e358..969b6a4 100644 --- a/src/pages/home/index.vue +++ b/src/pages/home/index.vue @@ -17,9 +17,9 @@ const table = useTable({ return []; }, columns: [ - // { - // type: 'index' - // }, + { + type: "index", + }, { title: "姓名", dataIndex: "username", @@ -64,7 +64,17 @@ const table = useTable({ buttons: [], }, ], - common: { + search: { + items: [ + { + field: "username", + label: "姓名", + type: "input", + }, + ], + }, + create: { + title: "新建用户", modalProps: { width: 432, maskClosable: false, @@ -124,18 +134,6 @@ const table = useTable({ }, }, ], - }, - search: { - items: [ - { - field: "username", - label: "姓名", - type: "input", - }, - ], - }, - create: { - title: "新建用户", submit: ({ model }) => { return api.user.addUser(model as any, { type: ContentType.FormData, diff --git a/src/pages/permission/index.vue b/src/pages/permission/index.vue index 7b51db0..232a787 100644 --- a/src/pages/permission/index.vue +++ b/src/pages/permission/index.vue @@ -95,7 +95,7 @@ const table = useTable({ ); - } + }, }, ], modalProps: { @@ -106,7 +106,7 @@ const table = useTable({ layout: "vertical", }, submit: ({ model }) => { - return api.permission.addPermission(model as any); + return api.permission.addPermission(model); }, }, modify: { diff --git a/src/pages/post/index.vue b/src/pages/post/index.vue index 0f3625d..09a57c1 100644 --- a/src/pages/post/index.vue +++ b/src/pages/post/index.vue @@ -1,11 +1,101 @@ diff --git a/src/pages/role/index.vue b/src/pages/role/index.vue index afa43dc..58c5187 100644 --- a/src/pages/role/index.vue +++ b/src/pages/role/index.vue @@ -95,7 +95,7 @@ const table = useTable({ ], submit: ({ model }) => { - return api.role.addRole(model as any); + return api.role.addRole(model); }, }, modify: { diff --git a/src/pages/user/index.vue b/src/pages/user/index.vue index 2fb33b7..2eba217 100644 --- a/src/pages/user/index.vue +++ b/src/pages/user/index.vue @@ -130,7 +130,7 @@ const table = useTable({ }, ], submit: ({ model }) => { - console.log(model); + return api.user.addUser(model); }, }, modify: { diff --git a/src/router/guards/guard-auth.ts b/src/router/guards/guard-auth.ts index e36f567..9d75bda 100644 --- a/src/router/guards/guard-auth.ts +++ b/src/router/guards/guard-auth.ts @@ -2,27 +2,32 @@ import { store, useUserStore } from "@/store"; import { Message } from "@arco-design/web-vue"; import { NavigationGuardWithThis } from "vue-router"; -const whitelist = ["/404"]; +const whitelist = ["/:all(.*)*"]; const signoutlist = ["/login"]; -export const authGuard: NavigationGuardWithThis = async function (to, from, next) { +export const authGuard: NavigationGuardWithThis = async function (to) { // 放在外面,pinia-plugin-peristedstate 插件会失效 const userStore = useUserStore(store); if (to.meta?.auth === false) { - return next(); + return true; } - if (whitelist.includes(to.fullPath)) { - return next(); + if (whitelist.includes(to.path) || to.name === "_all") { + return true; } - if (signoutlist.includes(to.fullPath)) { - if (userStore.id) { + if (signoutlist.includes(to.path)) { + if (userStore.accessToken) { Message.warning(`提示:您已登陆,如需重新请退出后再操作!`); - return next(false); + return false; } - return next(); + return true; } if (!userStore.accessToken) { - return next("/login"); + return { + path: "/login", + query: { + redirect: to.path, + }, + }; } - next(); + return true; }; diff --git a/src/router/guards/guard-nprogress.ts b/src/router/guards/guard-nprogress.ts index a57d24b..b7e1f68 100644 --- a/src/router/guards/guard-nprogress.ts +++ b/src/router/guards/guard-nprogress.ts @@ -1,9 +1,8 @@ import { NProgress } from "@/plugins"; import { NavigationGuardWithThis, NavigationHookAfter } from "vue-router"; -const before: NavigationGuardWithThis = function (to, from, next) { +const before: NavigationGuardWithThis = function () { NProgress.start(); - next(); }; const after: NavigationHookAfter = function () { diff --git a/src/router/guards/guard-title.ts b/src/router/guards/guard-title.ts index 72f9d49..72be640 100644 --- a/src/router/guards/guard-title.ts +++ b/src/router/guards/guard-title.ts @@ -1,10 +1,9 @@ import { store, useAppStore } from "@/store"; -import { NavigationGuardWithThis } from "vue-router"; +import { NavigationHookAfter } from "vue-router"; -export const titleGuard: NavigationGuardWithThis = function (to, from, next) { +export const titleGuard: NavigationHookAfter = function (to) { const appStore = useAppStore(store); const title = to.meta.title || appStore.title; const subtitle = appStore.subtitle; document.title = `${title} | ${subtitle}`; - next(); }; diff --git a/src/router/router/index.ts b/src/router/router/index.ts index 7ccf18f..816a751 100644 --- a/src/router/router/index.ts +++ b/src/router/router/index.ts @@ -3,6 +3,7 @@ import { authGuard } from "../guards/guard-auth"; import { nprogressGuard } from "../guards/guard-nprogress"; import { titleGuard } from "../guards/guard-title"; import { routes } from "../routes"; +import { api } from "@/api"; export const router = createRouter({ history: createWebHashHistory(), @@ -17,5 +18,10 @@ export const router = createRouter({ router.beforeEach(nprogressGuard.before); router.afterEach(nprogressGuard.after); -router.beforeEach(titleGuard); router.beforeEach(authGuard); +router.afterEach(titleGuard); + +api.setTokenExpiredHandler(() => { + const redirect = router.currentRoute.value.path; + router.push({ path: "/login", query: { redirect } }); +}); diff --git a/src/router/routes/index.ts b/src/router/routes/index.ts index 1e47dfc..aeecfe4 100644 --- a/src/router/routes/index.ts +++ b/src/router/routes/index.ts @@ -4,9 +4,7 @@ import { RouteRecordRaw } from "vue-router"; const APP_ROUTE_NAME = "_app"; /** - * 转换一维路由为二维路由,其中以 _ 开头的路由为顶级路由,其余为应用路由 - * @param routes 路由配置 - * @returns + * 转换一维路由为二维路由,以 _ 开头的路由为顶级路由,其余为应用路由 */ const transformRoutes = (routes: RouteRecordRaw[]) => { const topRoutes: RouteRecordRaw[] = []; @@ -33,26 +31,7 @@ const transformRoutes = (routes: RouteRecordRaw[]) => { appRoute.children = appRoutes; } - return topRoutes; + return [topRoutes, appRoutes]; }; -/** - * 获取应用路由 - * @param routes 路由配置 - * @returns - */ -const getAppRoutes = (routes: RouteRecordRaw[]) => { - return routes.find((i) => i.name === APP_ROUTE_NAME)?.children || []; -}; - -/** - * 所有路由 - */ -const routes = transformRoutes(generatedRoutes); - -/** - * 应用路由 - */ -const appRoutes = getAppRoutes(routes); - -export { routes, appRoutes }; +export const [routes, appRoutes] = transformRoutes(generatedRoutes); diff --git a/src/types/auto-component.d.ts b/src/types/auto-component.d.ts index e98b15c..dd2d698 100644 --- a/src/types/auto-component.d.ts +++ b/src/types/auto-component.d.ts @@ -14,10 +14,12 @@ declare module '@vue/runtime-core' { ACheckbox: typeof import('@arco-design/web-vue')['Checkbox'] AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'] ADoption: typeof import('@arco-design/web-vue')['Doption'] + ADot: typeof import('@arco-design/web-vue')['Dot'] ADrawer: typeof import('@arco-design/web-vue')['Drawer'] ADropdown: typeof import('@arco-design/web-vue')['Dropdown'] AForm: typeof import('@arco-design/web-vue')['Form'] AFormItem: typeof import('@arco-design/web-vue')['FormItem'] + AImage: typeof import('@arco-design/web-vue')['Image'] AInput: typeof import('@arco-design/web-vue')['Input'] AInputPassword: typeof import('@arco-design/web-vue')['InputPassword'] ALayout: typeof import('@arco-design/web-vue')['Layout'] @@ -25,8 +27,12 @@ declare module '@vue/runtime-core' { 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'] + APopover: typeof import('@arco-design/web-vue')['Popover'] ASpace: typeof import('@arco-design/web-vue')['Space'] ASubMenu: typeof import('@arco-design/web-vue')['SubMenu'] ATag: typeof import('@arco-design/web-vue')['Tag']