Commit 5465f058ceb7b130e456feaebb17c3beedb092a5
1 parent
d5b76892
feat(user): add user login expiration example
Showing
16 changed files
with
149 additions
and
30 deletions
CHANGELOG.zh_CN.md
mock/demo/account.ts
1 | 1 | import { MockMethod } from 'vite-plugin-mock'; |
2 | -import { resultSuccess } from '../_util'; | |
2 | +import { resultSuccess, resultError } from '../_util'; | |
3 | 3 | |
4 | 4 | const userInfo = { |
5 | 5 | name: 'Vben', |
... | ... | @@ -51,4 +51,12 @@ export default [ |
51 | 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 | 62 | ] as MockMethod[]; | ... | ... |
src/api/demo/account.ts
... | ... | @@ -3,8 +3,11 @@ import { GetAccountInfoModel } from './model/accountModel'; |
3 | 3 | |
4 | 4 | enum Api { |
5 | 5 | ACCOUNT_INFO = '/account/getAccountInfo', |
6 | + SESSION_TIMEOUT = '/user/sessionTimeout', | |
6 | 7 | } |
7 | 8 | |
8 | 9 | // Get personal center-basic settings |
9 | 10 | |
10 | 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 | 5 | import { useRootSetting } from '/@/hooks/setting/useRootSetting'; |
6 | 6 | import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; |
7 | 7 | import { useDesign } from '/@/hooks/web/useDesign'; |
8 | + import { useUserStoreWidthOut } from '/@/store/modules/user'; | |
8 | 9 | |
9 | 10 | import { SettingButtonPositionEnum } from '/@/enums/appEnum'; |
10 | 11 | import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
11 | 12 | |
13 | + import SessionTimeoutLogin from '/@/views/sys/login/SessionTimeoutLogin.vue'; | |
12 | 14 | export default defineComponent({ |
13 | 15 | name: 'LayoutFeatures', |
14 | 16 | components: { |
15 | 17 | BackTop, |
16 | 18 | LayoutLockPage: createAsyncComponent(() => import('/@/views/sys/lock/index.vue')), |
17 | 19 | SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue')), |
20 | + SessionTimeoutLogin, | |
18 | 21 | }, |
19 | 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 | 26 | const { prefixCls } = useDesign('setting-drawer-fearure'); |
28 | 27 | const { getShowHeader } = useHeaderSetting(); |
29 | 28 | |
29 | + const getIsSessionTimeout = computed(() => userStore.getSessionTimeout); | |
30 | + | |
30 | 31 | const getIsFixedSettingDrawer = computed(() => { |
31 | 32 | if (!unref(getShowSettingButton)) { |
32 | 33 | return false; |
... | ... | @@ -44,6 +45,7 @@ |
44 | 45 | getUseOpenBackTop, |
45 | 46 | getIsFixedSettingDrawer, |
46 | 47 | prefixCls, |
48 | + getIsSessionTimeout, | |
47 | 49 | }; |
48 | 50 | }, |
49 | 51 | }); |
... | ... | @@ -53,6 +55,7 @@ |
53 | 55 | <LayoutLockPage /> |
54 | 56 | <BackTop v-if="getUseOpenBackTop" :target="getTarget" /> |
55 | 57 | <SettingDrawer v-if="getIsFixedSettingDrawer" :class="prefixCls" /> |
58 | + <SessionTimeoutLogin v-if="getIsSessionTimeout" /> | |
56 | 59 | </template> |
57 | 60 | |
58 | 61 | <style lang="less"> | ... | ... |
src/locales/lang/en/routes/demo/feat.ts
src/locales/lang/zh_CN/routes/demo/feat.ts
src/router/menus/modules/demo/comp.ts
... | ... | @@ -6,9 +6,6 @@ const menu: MenuModule = { |
6 | 6 | menu: { |
7 | 7 | name: t('routes.demo.comp.comp'), |
8 | 8 | path: '/comp', |
9 | - tag: { | |
10 | - dot: true, | |
11 | - }, | |
12 | 9 | children: [ |
13 | 10 | { |
14 | 11 | path: 'basic', |
... | ... | @@ -191,9 +188,6 @@ const menu: MenuModule = { |
191 | 188 | { |
192 | 189 | name: t('routes.demo.editor.editor'), |
193 | 190 | path: 'editor', |
194 | - tag: { | |
195 | - dot: true, | |
196 | - }, | |
197 | 191 | children: [ |
198 | 192 | { |
199 | 193 | path: 'json', | ... | ... |
src/router/menus/modules/demo/feat.ts
... | ... | @@ -6,7 +6,9 @@ const menu: MenuModule = { |
6 | 6 | menu: { |
7 | 7 | name: t('routes.demo.feat.feat'), |
8 | 8 | path: '/feat', |
9 | - | |
9 | + tag: { | |
10 | + dot: true, | |
11 | + }, | |
10 | 12 | children: [ |
11 | 13 | { |
12 | 14 | path: 'icon', |
... | ... | @@ -17,6 +19,13 @@ const menu: MenuModule = { |
17 | 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 | 29 | path: 'tabs', |
21 | 30 | name: t('routes.demo.feat.tabs'), |
22 | 31 | }, | ... | ... |
src/router/menus/modules/demo/flow.ts
... | ... | @@ -6,17 +6,10 @@ const menu: MenuModule = { |
6 | 6 | menu: { |
7 | 7 | name: t('routes.demo.flow.name'), |
8 | 8 | path: '/flow', |
9 | - tag: { | |
10 | - dot: true, | |
11 | - }, | |
12 | - | |
13 | 9 | children: [ |
14 | 10 | { |
15 | 11 | path: 'flowChart', |
16 | 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 | 12 | icon: 'ion:git-compare-outline', |
13 | 13 | title: t('routes.demo.feat.feat'), |
14 | 14 | }, |
15 | + | |
15 | 16 | children: [ |
16 | 17 | { |
17 | 18 | path: 'icon', |
... | ... | @@ -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 | 42 | path: 'print', |
34 | 43 | name: 'Print', |
35 | 44 | component: () => import('/@/views/demo/feat/print/index.vue'), | ... | ... |
src/store/modules/user.ts
... | ... | @@ -25,6 +25,7 @@ interface UserState { |
25 | 25 | userInfo: Nullable<UserInfo>; |
26 | 26 | token?: string; |
27 | 27 | roleList: RoleEnum[]; |
28 | + sessionTimeout?: boolean; | |
28 | 29 | } |
29 | 30 | |
30 | 31 | export const useUserStore = defineStore({ |
... | ... | @@ -36,6 +37,8 @@ export const useUserStore = defineStore({ |
36 | 37 | token: undefined, |
37 | 38 | // roleList |
38 | 39 | roleList: [], |
40 | + // Whether the login expired | |
41 | + sessionTimeout: false, | |
39 | 42 | }), |
40 | 43 | getters: { |
41 | 44 | getUserInfo(): UserInfo { |
... | ... | @@ -47,9 +50,12 @@ export const useUserStore = defineStore({ |
47 | 50 | getRoleList(): RoleEnum[] { |
48 | 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 | 57 | actions: { |
52 | - setToken(info: string) { | |
58 | + setToken(info: string | undefined) { | |
53 | 59 | this.token = info; |
54 | 60 | setAuthCache(TOKEN_KEY, info); |
55 | 61 | }, |
... | ... | @@ -61,10 +67,14 @@ export const useUserStore = defineStore({ |
61 | 67 | this.userInfo = info; |
62 | 68 | setAuthCache(USER_INFO_KEY, info); |
63 | 69 | }, |
70 | + setSessionTimeout(flag: boolean) { | |
71 | + this.sessionTimeout = flag; | |
72 | + }, | |
64 | 73 | resetState() { |
65 | 74 | this.userInfo = null; |
66 | 75 | this.token = ''; |
67 | 76 | this.roleList = []; |
77 | + this.sessionTimeout = false; | |
68 | 78 | }, |
69 | 79 | /** |
70 | 80 | * @description: login |
... | ... | @@ -85,7 +95,9 @@ export const useUserStore = defineStore({ |
85 | 95 | // get user info |
86 | 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 | 101 | return userInfo; |
90 | 102 | } catch (error) { |
91 | 103 | return null; | ... | ... |
src/utils/http/axios/checkStatus.ts
1 | 1 | import { useMessage } from '/@/hooks/web/useMessage'; |
2 | 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 | 7 | const { createMessage } = useMessage(); |
7 | 8 | |
8 | 9 | const error = createMessage.error!; |
9 | 10 | export function checkStatus(status: number, msg: string): void { |
10 | 11 | const { t } = useI18n(); |
12 | + const userStore = useUserStoreWidthOut(); | |
11 | 13 | switch (status) { |
12 | 14 | case 400: |
13 | 15 | error(`${msg}`); |
... | ... | @@ -17,7 +19,8 @@ export function checkStatus(status: number, msg: string): void { |
17 | 19 | // Return to the current page after successful login. This step needs to be operated on the login page. |
18 | 20 | case 401: |
19 | 21 | error(t('sys.api.errMsg401')); |
20 | - router.push(PageEnum.BASE_LOGIN); | |
22 | + userStore.setToken(undefined); | |
23 | + userStore.setSessionTimeout(true); | |
21 | 24 | break; |
22 | 25 | case 403: |
23 | 26 | error(t('sys.api.errMsg403')); | ... | ... |
src/utils/http/axios/helper.ts
1 | 1 | import { isObject, isString } from '/@/utils/is'; |
2 | 2 | |
3 | +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'; | |
4 | + | |
3 | 5 | export function createNow<T extends boolean>( |
4 | 6 | join: boolean, |
5 | 7 | restful: T |
... | ... | @@ -16,7 +18,6 @@ export function createNow(join: boolean, restful = false): string | object { |
16 | 18 | return { _t: now }; |
17 | 19 | } |
18 | 20 | |
19 | -const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'; | |
20 | 21 | /** |
21 | 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 | 3 | <AppLocalePicker |
4 | 4 | class="absolute top-4 right-4 enter-x text-white xl:text-gray-600" |
5 | 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 | 10 | <span class="-enter-x xl:hidden"> |
10 | 11 | <AppLogo :alwaysShowTitle="true" /> |
... | ... | @@ -31,7 +32,25 @@ |
31 | 32 | <div class="h-full xl:h-auto flex py-5 xl:py-0 xl:my-0 w-full xl:w-6/12"> |
32 | 33 | <div |
33 | 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 | 55 | <LoginForm /> |
37 | 56 | <ForgetPasswordForm /> |
... | ... | @@ -72,6 +91,11 @@ |
72 | 91 | AppLocalePicker, |
73 | 92 | AppDarkModeToggle, |
74 | 93 | }, |
94 | + props: { | |
95 | + sessionTimeout: { | |
96 | + type: Boolean, | |
97 | + }, | |
98 | + }, | |
75 | 99 | setup() { |
76 | 100 | const globSetting = useGlobSetting(); |
77 | 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> | ... | ... |