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