Commit 5465f058ceb7b130e456feaebb17c3beedb092a5

Authored by vben
1 parent d5b76892

feat(user): add user login expiration example

CHANGELOG.zh_CN.md
@@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
7 - 新增 `JsonPreview`Json 数据查看组件 7 - 新增 `JsonPreview`Json 数据查看组件
8 - 表格的数据列(column)和操作列(actionColumn)的字段可以根据权限和业务来控制是否显示 8 - 表格的数据列(column)和操作列(actionColumn)的字段可以根据权限和业务来控制是否显示
9 - 新增权限控制表格示例(AuthColumn.vue) 9 - 新增权限控制表格示例(AuthColumn.vue)
  10 +- 新增用户登录过期示例
10 11
11 ### ⚡ Performance Improvements 12 ### ⚡ Performance Improvements
12 13
mock/demo/account.ts
1 import { MockMethod } from 'vite-plugin-mock'; 1 import { MockMethod } from 'vite-plugin-mock';
2 -import { resultSuccess } from '../_util'; 2 +import { resultSuccess, resultError } from '../_util';
3 3
4 const userInfo = { 4 const userInfo = {
5 name: 'Vben', 5 name: 'Vben',
@@ -51,4 +51,12 @@ export default [ @@ -51,4 +51,12 @@ export default [
51 return resultSuccess(userInfo); 51 return resultSuccess(userInfo);
52 }, 52 },
53 }, 53 },
  54 + {
  55 + url: '/basic-api/user/sessionTimeout',
  56 + method: 'post',
  57 + statusCode: 401,
  58 + response: () => {
  59 + return resultError();
  60 + },
  61 + },
54 ] as MockMethod[]; 62 ] as MockMethod[];
src/api/demo/account.ts
@@ -3,8 +3,11 @@ import { GetAccountInfoModel } from './model/accountModel'; @@ -3,8 +3,11 @@ import { GetAccountInfoModel } from './model/accountModel';
3 3
4 enum Api { 4 enum Api {
5 ACCOUNT_INFO = '/account/getAccountInfo', 5 ACCOUNT_INFO = '/account/getAccountInfo',
  6 + SESSION_TIMEOUT = '/user/sessionTimeout',
6 } 7 }
7 8
8 // Get personal center-basic settings 9 // Get personal center-basic settings
9 10
10 export const accountInfoApi = () => defHttp.get<GetAccountInfoModel>({ url: Api.ACCOUNT_INFO }); 11 export const accountInfoApi = () => defHttp.get<GetAccountInfoModel>({ url: Api.ACCOUNT_INFO });
  12 +
  13 +export const sessionTimeoutApi = () => defHttp.post<void>({ url: Api.SESSION_TIMEOUT });
src/layouts/default/feature/index.vue
@@ -5,28 +5,29 @@ @@ -5,28 +5,29 @@
5 import { useRootSetting } from '/@/hooks/setting/useRootSetting'; 5 import { useRootSetting } from '/@/hooks/setting/useRootSetting';
6 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; 6 import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
7 import { useDesign } from '/@/hooks/web/useDesign'; 7 import { useDesign } from '/@/hooks/web/useDesign';
  8 + import { useUserStoreWidthOut } from '/@/store/modules/user';
8 9
9 import { SettingButtonPositionEnum } from '/@/enums/appEnum'; 10 import { SettingButtonPositionEnum } from '/@/enums/appEnum';
10 import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; 11 import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
11 12
  13 + import SessionTimeoutLogin from '/@/views/sys/login/SessionTimeoutLogin.vue';
12 export default defineComponent({ 14 export default defineComponent({
13 name: 'LayoutFeatures', 15 name: 'LayoutFeatures',
14 components: { 16 components: {
15 BackTop, 17 BackTop,
16 LayoutLockPage: createAsyncComponent(() => import('/@/views/sys/lock/index.vue')), 18 LayoutLockPage: createAsyncComponent(() => import('/@/views/sys/lock/index.vue')),
17 SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue')), 19 SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue')),
  20 + SessionTimeoutLogin,
18 }, 21 },
19 setup() { 22 setup() {
20 - const {  
21 - getUseOpenBackTop,  
22 - getShowSettingButton,  
23 - getSettingButtonPosition,  
24 - getFullContent,  
25 - } = useRootSetting();  
26 - 23 + const { getUseOpenBackTop, getShowSettingButton, getSettingButtonPosition, getFullContent } =
  24 + useRootSetting();
  25 + const userStore = useUserStoreWidthOut();
27 const { prefixCls } = useDesign('setting-drawer-fearure'); 26 const { prefixCls } = useDesign('setting-drawer-fearure');
28 const { getShowHeader } = useHeaderSetting(); 27 const { getShowHeader } = useHeaderSetting();
29 28
  29 + const getIsSessionTimeout = computed(() => userStore.getSessionTimeout);
  30 +
30 const getIsFixedSettingDrawer = computed(() => { 31 const getIsFixedSettingDrawer = computed(() => {
31 if (!unref(getShowSettingButton)) { 32 if (!unref(getShowSettingButton)) {
32 return false; 33 return false;
@@ -44,6 +45,7 @@ @@ -44,6 +45,7 @@
44 getUseOpenBackTop, 45 getUseOpenBackTop,
45 getIsFixedSettingDrawer, 46 getIsFixedSettingDrawer,
46 prefixCls, 47 prefixCls,
  48 + getIsSessionTimeout,
47 }; 49 };
48 }, 50 },
49 }); 51 });
@@ -53,6 +55,7 @@ @@ -53,6 +55,7 @@
53 <LayoutLockPage /> 55 <LayoutLockPage />
54 <BackTop v-if="getUseOpenBackTop" :target="getTarget" /> 56 <BackTop v-if="getUseOpenBackTop" :target="getTarget" />
55 <SettingDrawer v-if="getIsFixedSettingDrawer" :class="prefixCls" /> 57 <SettingDrawer v-if="getIsFixedSettingDrawer" :class="prefixCls" />
  58 + <SessionTimeoutLogin v-if="getIsSessionTimeout" />
56 </template> 59 </template>
57 60
58 <style lang="less"> 61 <style lang="less">
src/locales/lang/en/routes/demo/feat.ts
@@ -2,6 +2,7 @@ export default { @@ -2,6 +2,7 @@ export default {
2 feat: 'Page Function', 2 feat: 'Page Function',
3 icon: 'Icon', 3 icon: 'Icon',
4 tabs: 'Tabs', 4 tabs: 'Tabs',
  5 + sessionTimeout: 'Session Timeout',
5 print: 'Print', 6 print: 'Print',
6 contextMenu: 'Context Menu', 7 contextMenu: 'Context Menu',
7 download: 'Download', 8 download: 'Download',
src/locales/lang/zh_CN/routes/demo/feat.ts
1 export default { 1 export default {
2 feat: '功能', 2 feat: '功能',
3 icon: '图标', 3 icon: '图标',
  4 + sessionTimeout: '登录过期',
4 tabs: '标签页操作', 5 tabs: '标签页操作',
5 print: '打印', 6 print: '打印',
6 contextMenu: '右键菜单', 7 contextMenu: '右键菜单',
src/router/menus/modules/demo/comp.ts
@@ -6,9 +6,6 @@ const menu: MenuModule = { @@ -6,9 +6,6 @@ const menu: MenuModule = {
6 menu: { 6 menu: {
7 name: t('routes.demo.comp.comp'), 7 name: t('routes.demo.comp.comp'),
8 path: '/comp', 8 path: '/comp',
9 - tag: {  
10 - dot: true,  
11 - },  
12 children: [ 9 children: [
13 { 10 {
14 path: 'basic', 11 path: 'basic',
@@ -191,9 +188,6 @@ const menu: MenuModule = { @@ -191,9 +188,6 @@ const menu: MenuModule = {
191 { 188 {
192 name: t('routes.demo.editor.editor'), 189 name: t('routes.demo.editor.editor'),
193 path: 'editor', 190 path: 'editor',
194 - tag: {  
195 - dot: true,  
196 - },  
197 children: [ 191 children: [
198 { 192 {
199 path: 'json', 193 path: 'json',
src/router/menus/modules/demo/feat.ts
@@ -6,7 +6,9 @@ const menu: MenuModule = { @@ -6,7 +6,9 @@ const menu: MenuModule = {
6 menu: { 6 menu: {
7 name: t('routes.demo.feat.feat'), 7 name: t('routes.demo.feat.feat'),
8 path: '/feat', 8 path: '/feat',
9 - 9 + tag: {
  10 + dot: true,
  11 + },
10 children: [ 12 children: [
11 { 13 {
12 path: 'icon', 14 path: 'icon',
@@ -17,6 +19,13 @@ const menu: MenuModule = { @@ -17,6 +19,13 @@ const menu: MenuModule = {
17 name: t('routes.demo.feat.ws'), 19 name: t('routes.demo.feat.ws'),
18 }, 20 },
19 { 21 {
  22 + name: t('routes.demo.feat.sessionTimeout'),
  23 + path: 'session-timeout',
  24 + tag: {
  25 + content: 'new',
  26 + },
  27 + },
  28 + {
20 path: 'tabs', 29 path: 'tabs',
21 name: t('routes.demo.feat.tabs'), 30 name: t('routes.demo.feat.tabs'),
22 }, 31 },
src/router/menus/modules/demo/flow.ts
@@ -6,17 +6,10 @@ const menu: MenuModule = { @@ -6,17 +6,10 @@ const menu: MenuModule = {
6 menu: { 6 menu: {
7 name: t('routes.demo.flow.name'), 7 name: t('routes.demo.flow.name'),
8 path: '/flow', 8 path: '/flow',
9 - tag: {  
10 - dot: true,  
11 - },  
12 -  
13 children: [ 9 children: [
14 { 10 {
15 path: 'flowChart', 11 path: 'flowChart',
16 name: t('routes.demo.flow.flowChart'), 12 name: t('routes.demo.flow.flowChart'),
17 - tag: {  
18 - content: 'new',  
19 - },  
20 }, 13 },
21 ], 14 ],
22 }, 15 },
src/router/routes/modules/demo/feat.ts
@@ -12,6 +12,7 @@ const feat: AppRouteModule = { @@ -12,6 +12,7 @@ const feat: AppRouteModule = {
12 icon: 'ion:git-compare-outline', 12 icon: 'ion:git-compare-outline',
13 title: t('routes.demo.feat.feat'), 13 title: t('routes.demo.feat.feat'),
14 }, 14 },
  15 +
15 children: [ 16 children: [
16 { 17 {
17 path: 'icon', 18 path: 'icon',
@@ -30,6 +31,14 @@ const feat: AppRouteModule = { @@ -30,6 +31,14 @@ const feat: AppRouteModule = {
30 }, 31 },
31 }, 32 },
32 { 33 {
  34 + path: 'session-timeout',
  35 + name: 'SessionTimeout',
  36 + component: () => import('/@/views/demo/feat/session-timeout/index.vue'),
  37 + meta: {
  38 + title: t('routes.demo.feat.sessionTimeout'),
  39 + },
  40 + },
  41 + {
33 path: 'print', 42 path: 'print',
34 name: 'Print', 43 name: 'Print',
35 component: () => import('/@/views/demo/feat/print/index.vue'), 44 component: () => import('/@/views/demo/feat/print/index.vue'),
src/store/modules/user.ts
@@ -25,6 +25,7 @@ interface UserState { @@ -25,6 +25,7 @@ interface UserState {
25 userInfo: Nullable<UserInfo>; 25 userInfo: Nullable<UserInfo>;
26 token?: string; 26 token?: string;
27 roleList: RoleEnum[]; 27 roleList: RoleEnum[];
  28 + sessionTimeout?: boolean;
28 } 29 }
29 30
30 export const useUserStore = defineStore({ 31 export const useUserStore = defineStore({
@@ -36,6 +37,8 @@ export const useUserStore = defineStore({ @@ -36,6 +37,8 @@ export const useUserStore = defineStore({
36 token: undefined, 37 token: undefined,
37 // roleList 38 // roleList
38 roleList: [], 39 roleList: [],
  40 + // Whether the login expired
  41 + sessionTimeout: false,
39 }), 42 }),
40 getters: { 43 getters: {
41 getUserInfo(): UserInfo { 44 getUserInfo(): UserInfo {
@@ -47,9 +50,12 @@ export const useUserStore = defineStore({ @@ -47,9 +50,12 @@ export const useUserStore = defineStore({
47 getRoleList(): RoleEnum[] { 50 getRoleList(): RoleEnum[] {
48 return this.roleList.length > 0 ? this.roleList : getAuthCache<RoleEnum[]>(ROLES_KEY); 51 return this.roleList.length > 0 ? this.roleList : getAuthCache<RoleEnum[]>(ROLES_KEY);
49 }, 52 },
  53 + getSessionTimeout(): boolean {
  54 + return !!this.sessionTimeout;
  55 + },
50 }, 56 },
51 actions: { 57 actions: {
52 - setToken(info: string) { 58 + setToken(info: string | undefined) {
53 this.token = info; 59 this.token = info;
54 setAuthCache(TOKEN_KEY, info); 60 setAuthCache(TOKEN_KEY, info);
55 }, 61 },
@@ -61,10 +67,14 @@ export const useUserStore = defineStore({ @@ -61,10 +67,14 @@ export const useUserStore = defineStore({
61 this.userInfo = info; 67 this.userInfo = info;
62 setAuthCache(USER_INFO_KEY, info); 68 setAuthCache(USER_INFO_KEY, info);
63 }, 69 },
  70 + setSessionTimeout(flag: boolean) {
  71 + this.sessionTimeout = flag;
  72 + },
64 resetState() { 73 resetState() {
65 this.userInfo = null; 74 this.userInfo = null;
66 this.token = ''; 75 this.token = '';
67 this.roleList = []; 76 this.roleList = [];
  77 + this.sessionTimeout = false;
68 }, 78 },
69 /** 79 /**
70 * @description: login 80 * @description: login
@@ -85,7 +95,9 @@ export const useUserStore = defineStore({ @@ -85,7 +95,9 @@ export const useUserStore = defineStore({
85 // get user info 95 // get user info
86 const userInfo = await this.getUserInfoAction({ userId }); 96 const userInfo = await this.getUserInfoAction({ userId });
87 97
88 - goHome && (await router.replace(PageEnum.BASE_HOME)); 98 + const sessionTimeout = this.sessionTimeout;
  99 + sessionTimeout && this.setSessionTimeout(false);
  100 + !sessionTimeout && goHome && (await router.replace(PageEnum.BASE_HOME));
89 return userInfo; 101 return userInfo;
90 } catch (error) { 102 } catch (error) {
91 return null; 103 return null;
src/utils/http/axios/checkStatus.ts
1 import { useMessage } from '/@/hooks/web/useMessage'; 1 import { useMessage } from '/@/hooks/web/useMessage';
2 import { useI18n } from '/@/hooks/web/useI18n'; 2 import { useI18n } from '/@/hooks/web/useI18n';
3 -import router from '/@/router';  
4 -import { PageEnum } from '/@/enums/pageEnum'; 3 +// import router from '/@/router';
  4 +// import { PageEnum } from '/@/enums/pageEnum';
  5 +import { useUserStoreWidthOut } from '/@/store/modules/user';
5 6
6 const { createMessage } = useMessage(); 7 const { createMessage } = useMessage();
7 8
8 const error = createMessage.error!; 9 const error = createMessage.error!;
9 export function checkStatus(status: number, msg: string): void { 10 export function checkStatus(status: number, msg: string): void {
10 const { t } = useI18n(); 11 const { t } = useI18n();
  12 + const userStore = useUserStoreWidthOut();
11 switch (status) { 13 switch (status) {
12 case 400: 14 case 400:
13 error(`${msg}`); 15 error(`${msg}`);
@@ -17,7 +19,8 @@ export function checkStatus(status: number, msg: string): void { @@ -17,7 +19,8 @@ export function checkStatus(status: number, msg: string): void {
17 // Return to the current page after successful login. This step needs to be operated on the login page. 19 // Return to the current page after successful login. This step needs to be operated on the login page.
18 case 401: 20 case 401:
19 error(t('sys.api.errMsg401')); 21 error(t('sys.api.errMsg401'));
20 - router.push(PageEnum.BASE_LOGIN); 22 + userStore.setToken(undefined);
  23 + userStore.setSessionTimeout(true);
21 break; 24 break;
22 case 403: 25 case 403:
23 error(t('sys.api.errMsg403')); 26 error(t('sys.api.errMsg403'));
src/utils/http/axios/helper.ts
1 import { isObject, isString } from '/@/utils/is'; 1 import { isObject, isString } from '/@/utils/is';
2 2
  3 +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
  4 +
3 export function createNow<T extends boolean>( 5 export function createNow<T extends boolean>(
4 join: boolean, 6 join: boolean,
5 restful: T 7 restful: T
@@ -16,7 +18,6 @@ export function createNow(join: boolean, restful = false): string | object { @@ -16,7 +18,6 @@ export function createNow(join: boolean, restful = false): string | object {
16 return { _t: now }; 18 return { _t: now };
17 } 19 }
18 20
19 -const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';  
20 /** 21 /**
21 * @description: Format request parameter time 22 * @description: Format request parameter time
22 */ 23 */
src/views/demo/feat/session-timeout/index.vue 0 → 100644
  1 +<template>
  2 + <PageWrapper
  3 + title="登录过期示例"
  4 + content="用户登录过期示例,不再跳转登录页,直接生成页面覆盖当前页面,方便保持过期前的用户状态!"
  5 + >
  6 + <a-button type="primary" @click="test">点击触发用户登录过期</a-button>
  7 + </PageWrapper>
  8 +</template>
  9 +<script lang="ts">
  10 + import { defineComponent } from 'vue';
  11 + import { PageWrapper } from '/@/components/Page';
  12 +
  13 + import { sessionTimeoutApi } from '/@/api/demo/account';
  14 +
  15 + export default defineComponent({
  16 + name: 'TestSessionTimeout',
  17 + components: { PageWrapper },
  18 + setup() {
  19 + async function test() {
  20 + await sessionTimeoutApi();
  21 + }
  22 + return { test };
  23 + },
  24 + });
  25 +</script>
src/views/sys/login/Login.vue
@@ -3,8 +3,9 @@ @@ -3,8 +3,9 @@
3 <AppLocalePicker 3 <AppLocalePicker
4 class="absolute top-4 right-4 enter-x text-white xl:text-gray-600" 4 class="absolute top-4 right-4 enter-x text-white xl:text-gray-600"
5 :showText="false" 5 :showText="false"
  6 + v-if="!sessionTimeout"
6 /> 7 />
7 - <AppDarkModeToggle class="absolute top-3 right-7 enter-x" /> 8 + <AppDarkModeToggle class="absolute top-3 right-7 enter-x" v-if="!sessionTimeout" />
8 9
9 <span class="-enter-x xl:hidden"> 10 <span class="-enter-x xl:hidden">
10 <AppLogo :alwaysShowTitle="true" /> 11 <AppLogo :alwaysShowTitle="true" />
@@ -31,7 +32,25 @@ @@ -31,7 +32,25 @@
31 <div class="h-full xl:h-auto flex py-5 xl:py-0 xl:my-0 w-full xl:w-6/12"> 32 <div class="h-full xl:h-auto flex py-5 xl:py-0 xl:my-0 w-full xl:w-6/12">
32 <div 33 <div
33 :class="`${prefixCls}-form`" 34 :class="`${prefixCls}-form`"
34 - class="my-auto mx-auto xl:ml-20 xl:bg-transparent px-5 py-8 sm:px-8 xl:p-4 rounded-md shadow-md xl:shadow-none w-full sm:w-3/4 lg:w-2/4 xl:w-auto enter-x relative" 35 + class="
  36 + my-auto
  37 + mx-auto
  38 + xl:ml-20
  39 + xl:bg-transparent
  40 + px-5
  41 + py-8
  42 + sm:px-8
  43 + xl:p-4
  44 + rounded-md
  45 + shadow-md
  46 + xl:shadow-none
  47 + w-full
  48 + sm:w-3/4
  49 + lg:w-2/4
  50 + xl:w-auto
  51 + enter-x
  52 + relative
  53 + "
35 > 54 >
36 <LoginForm /> 55 <LoginForm />
37 <ForgetPasswordForm /> 56 <ForgetPasswordForm />
@@ -72,6 +91,11 @@ @@ -72,6 +91,11 @@
72 AppLocalePicker, 91 AppLocalePicker,
73 AppDarkModeToggle, 92 AppDarkModeToggle,
74 }, 93 },
  94 + props: {
  95 + sessionTimeout: {
  96 + type: Boolean,
  97 + },
  98 + },
75 setup() { 99 setup() {
76 const globSetting = useGlobSetting(); 100 const globSetting = useGlobSetting();
77 const { prefixCls } = useDesign('login'); 101 const { prefixCls } = useDesign('login');
src/views/sys/login/SessionTimeoutLogin.vue 0 → 100644
  1 +<template>
  2 + <transition>
  3 + <div :class="prefixCls">
  4 + <Login sessionTimeout />
  5 + </div>
  6 + </transition>
  7 +</template>
  8 +<script lang="ts">
  9 + import { defineComponent } from 'vue';
  10 + import Login from './Login.vue';
  11 +
  12 + import { useDesign } from '/@/hooks/web/useDesign';
  13 + export default defineComponent({
  14 + name: 'SessionTimeoutLogin',
  15 + components: { Login },
  16 + setup() {
  17 + const { prefixCls } = useDesign('st-login');
  18 + return { prefixCls };
  19 + },
  20 + });
  21 +</script>
  22 +<style lang="less" scoped>
  23 + @prefix-cls: ~'@{namespace}-st-login';
  24 +
  25 + .@{prefix-cls} {
  26 + position: fixed;
  27 + z-index: 9999999;
  28 + width: 100%;
  29 + height: 100%;
  30 + background: @component-background;
  31 + }
  32 +</style>