feat: 优化登陆跳转/退出跳转

master
luoer 2023-08-07 17:43:22 +08:00
parent 0e39ea474a
commit 89b1de9b02
22 changed files with 362 additions and 137 deletions

2
.env
View File

@ -2,7 +2,7 @@
# 应用配置
# =====================================================================================
# 网站标题
VITE_TITLE = 绝弹管理系统
VITE_TITLE = 绝弹中心
# 网站副标题
VITE_SUBTITLE = 快速开发web应用的模板工具
# API接口前缀参见 axios 的 baseURL

View File

@ -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<unknown> {
/**
*
*/
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);
}

View File

@ -1,9 +1,11 @@
<template>
<div>
<BreadCrumb></BreadCrumb>
<div class="mx-4 mt-4 p-4 bg-white">
<slot></slot>
</div>
<slot name="content">
<div class="mx-4 mt-4 p-4 bg-white">
<slot></slot>
</div>
</slot>
</div>
</template>

View File

@ -61,7 +61,7 @@ export const FormModal = defineComponent({
* @description `{ message }`
*/
submit: {
type: Function as PropType<(arg: { model: Record<string, any>; items: IFormItem[] }) => any | Promise<any>>,
type: Function as PropType<(args: { model: any; items: IFormItem[] }) => PromiseLike<any>>,
default: () => true,
},
/**
@ -191,7 +191,5 @@ export const FormModal = defineComponent({
});
export type FormModalInstance = InstanceType<typeof FormModal>;
export type FormModalProps = FormModalInstance["$props"];
export default FormModal;

View File

@ -45,7 +45,12 @@ export const config = {
title: "序号",
width: 60,
align: "center",
render: ({ rowIndex }: any) => rowIndex + 1,
render: ({ rowIndex }: any) => {
const table = inject<any>("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;
}
},
};

View File

@ -23,28 +23,23 @@ export interface TableColumnButton {
*
*/
text?: string;
/**
*
* @description `delete` `onClick``modify` `modify`
*/
type?: "delete" | "modify";
/**
*
*/
onClick?: (data: UseColumnRenderOptions, openModify?: (model: Record<string, any>) => 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<IFormItem> & {
/**
* `create.items` `field`
*/
extend?: string;
};
type Search = Partial<
Omit<FormProps, "items"> & {
/**
*
*/
items?: ExtendedFormItem[];
}
>;
type Modify = Partial<
Omit<FormModalProps, "items"> & {
/**
* `create`
*/
extend: boolean;
/**
*
*/
items?: ExtendedFormItem[];
}
>;
export interface UseTableOptions extends Omit<TableProps, "search" | "create" | "modify" | "columns"> {
/**
*
@ -73,24 +96,7 @@ export interface UseTableOptions extends Omit<TableProps, "search" | "create" |
*
* @see FormProps
*/
search?: Partial<
Omit<FormProps, "items"> & {
/**
*
*/
items?: (Partial<IFormItem> & {
/**
* `create.items`field
*/
extend?: string;
})[];
}
>;
/**
* common props for `create` and `modify` modal
* @see FormModalProps
*/
common?: Partial<FormModalProps>;
search?: Search;
/**
*
*/
@ -98,20 +104,7 @@ export interface UseTableOptions extends Omit<TableProps, "search" | "create" |
/**
*
*/
modify?: Partial<
Omit<FormModalProps, "items"> & {
/**
* `create`
*/
extend: boolean;
items?: (FormModalProps["items"][number] & {
/**
* `create.items`field
*/
extend?: string;
})[];
}
>;
modify?: Modify;
/**
*
*/

View File

@ -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<string, any>) => 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;

View File

@ -12,13 +12,13 @@
<template #icon>
<i class="icon-park-outline-back"></i>
</template>
返回上页
返回
</a-button>
<a-button type="outline" @click="router.push('/')">
<template #icon>
<i class="icon-park-outline-home"></i>
</template>
返回首页
首页
</a-button>
</div>
</div>

View File

@ -22,7 +22,7 @@
<a-dropdown>
<span class="cursor-pointer">
<a-avatar :size="28">
<img :src="userStore.avatar" :alt="userStore.nickname">
<img :src="userStore.avatar" :alt="userStore.nickname" />
</a-avatar>
<span class="mx-2">
{{ 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 } });
},
},
];

View File

@ -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<InstanceType<typeof Form>>();
@ -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) {

View File

@ -1,10 +1,140 @@
<template>
<bread-page class="">Demo/test Page</bread-page>
<bread-page id="list-page">
<template #content>
<AList class="mx-5 mt-3 bg-white" :bordered="true">
<template #header>
<div class="flex gap-2 items-center justify-between text-sm bg-gray-50 px-5 py-4">
<div class="flex gap-4">
<ACheckbox></ACheckbox>
<AInput class="inline-block w-80" placeholder="输入名称关键字"></AInput>
</div>
<div class="flex items-center gap-4">
<ADropdown>
<span class="cursor-pointer">
上传者
<i class="icon-park-outline-down"></i>
</span>
<template #content>
<ADoption>
<AInput placeholder="用户名关键字" />
</ADoption>
<ADoption>
<AAvatar :size="20">
<img src="https://picsum.photos/seed/picsum/200/300" alt="" />
</AAvatar>
绝弹土豆
</ADoption>
</template>
</ADropdown>
<ADropdown>
<span class="cursor-pointer">
排序默认
<i class="icon-park-outline-down"></i>
</span>
<template #content>
<ADoption>
<div class="w-40">默认</div>
</ADoption>
<ADoption>
<template #icon>
<i class="icon-park-outline-sort-amount-up"></i>
</template>
按创建时间升序
</ADoption>
<ADoption>
<template #icon>
<i class="icon-park-outline-sort-amount-down"></i>
</template>
按创建时间降序
</ADoption>
<ADoption>
<template #icon>
<i class="icon-park-outline-align-text-top"></i>
</template>
按文件大小升序
</ADoption>
<ADoption>
<template #icon>
<i class="icon-park-outline-align-text-bottom"></i>
</template>
按文件大小降序
</ADoption>
</template>
</ADropdown>
<div class="space-x-1">
<span
class="inline-flex p-1 hover:bg-slate-100 rounded cursor-pointer text-gray-400 hover:text-gray-700"
>
<i class="icon-park-outline-list"></i>
</span>
<span
class="inline-flex p-1 hover:bg-slate-100 rounded cursor-pointer text-gray-400 hover:text-gray-700"
>
<i class="icon-park-outline-insert-table"></i>
</span>
<span
class="inline-flex p-1 hover:bg-slate-100 rounded cursor-pointer text-gray-400 hover:text-gray-700"
>
<i class="icon-park-outline-refresh"></i>
</span>
</div>
</div>
</div>
</template>
<AListItem v-for="i in 10">
<AListItemMeta title="测试图片.png" description="image/png 1.2MB">
<template #avatar>
<ACheckbox class="mr-3"></ACheckbox>
<AImage src="https://picsum.photos/seed/picsum/200/300" height="40">
<img src="https://picsum.photos/seed/picsum/200/300" alt="" />
</AImage>
</template>
<template #title>
<span class="hover:text-blue-500 cursor-pointer">测试图片.png</span>
</template>
<template #description>
<div class="text-xs text-gray-400">image/png 1.2MB</div>
</template>
</AListItemMeta>
<template #actions>
<span class="text-xs text-gray-400">
<i class="icon-park-outline-user !w-[14px] !h-[14px]"></i>
绝弹
</span>
<span class="text-xs text-gray-400">2023-08-17 17:00:01</span>
<ADropdown>
<span class="inline-flex p-1 hover:bg-slate-100 rounded cursor-pointer">
<i class="icon-park-outline-more"></i>
</span>
<template #content>
<ADoption class="w-32"> <div class="w-32">详情</div> </ADoption>
<ADoption class="!text-red-500 !hover-bg-red-50">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
删除
</ADoption>
</template>
</ADropdown>
</template>
</AListItem>
</AList>
</template>
</bread-page>
</template>
<script setup lang="ts"></script>
<style scoped></style>
<style lang="less">
#list-page {
.arco-list-header {
padding: 0;
}
.arco-dropdown-list {
padding: 0 4px;
}
}
</style>
<route lang="json">
{

View File

@ -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,

View File

@ -95,7 +95,7 @@ const table = useTable({
</div>
</div>
);
}
},
},
],
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: {

View File

@ -1,11 +1,101 @@
<template>
<div class="m-4 p-4 bg-white">
Post Page
<Table v-bind="table" />
</div>
</template>
<script setup lang="tsx" name="PostPage">
import { api } from "@/api";
import { Table, useTable } from "@/components";
import { dayjs } from "@/plugins";
const table = useTable({
data: async (model, paging) => {
return api.post.getPosts({ ...model, ...paging });
},
columns: [
{
type: "index",
},
{
title: "文章名称",
dataIndex: "title",
width: 200,
},
{
title: "文章描述",
dataIndex: "description",
},
{
title: "创建时间",
dataIndex: "createdAt",
width: 200,
render: ({ record }) => dayjs(record.createdAt).format(),
},
{
title: "操作",
type: "button",
width: 70,
buttons: [
{
type: "modify",
text: "修改",
},
],
},
],
search: {
items: [
{
extend: "name",
required: false,
},
],
},
create: {
title: "新建文章",
modalProps: {
width: 580,
maskClosable: false,
},
formProps: {
layout: "vertical",
},
items: [
{
field: "title",
label: "文章名称",
type: "input",
required: true,
},
{
field: "slug",
label: "文章标识",
type: "input",
},
{
field: "description",
label: "文章描述",
type: "input",
},
{
field: "content",
label: "文章内容",
type: "textarea",
},
],
submit: ({ model }) => {
return api.role.addRole(model);
},
},
modify: {
extend: true,
title: "修改文章",
submit: ({ model }) => {
return api.post.updatePost(model.id, model);
},
},
});
</script>
<style scoped></style>

View File

@ -95,7 +95,7 @@ const table = useTable({
],
submit: ({ model }) => {
return api.role.addRole(model as any);
return api.role.addRole(model);
},
},
modify: {

View File

@ -130,7 +130,7 @@ const table = useTable({
},
],
submit: ({ model }) => {
console.log(model);
return api.user.addUser(model);
},
},
modify: {

View File

@ -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<undefined> = async function (to, from, next) {
export const authGuard: NavigationGuardWithThis<undefined> = 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;
};

View File

@ -1,9 +1,8 @@
import { NProgress } from "@/plugins";
import { NavigationGuardWithThis, NavigationHookAfter } from "vue-router";
const before: NavigationGuardWithThis<undefined> = function (to, from, next) {
const before: NavigationGuardWithThis<undefined> = function () {
NProgress.start();
next();
};
const after: NavigationHookAfter = function () {

View File

@ -1,10 +1,9 @@
import { store, useAppStore } from "@/store";
import { NavigationGuardWithThis } from "vue-router";
import { NavigationHookAfter } from "vue-router";
export const titleGuard: NavigationGuardWithThis<undefined> = 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();
};

View File

@ -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 } });
});

View File

@ -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);

View File

@ -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']