Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -208,7 +208,9 @@ export async function fetchRefreshToken(refreshToken: string): Promise<ServiceRe
|
||||
method: 'post',
|
||||
params: { refreshToken },
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
skipAuth: true
|
||||
skipAuth: true,
|
||||
suppressErrorMessage: true,
|
||||
skipTokenRefresh: true
|
||||
});
|
||||
|
||||
if (result.error || !result.data) {
|
||||
|
||||
@@ -10,6 +10,10 @@ declare module 'axios' {
|
||||
* 避免给它们带上过期 access 头被网关拦截。
|
||||
*/
|
||||
skipAuth?: boolean;
|
||||
/** 请求失败时不走通用错误 toast,由调用方自行收敛提示。 */
|
||||
suppressErrorMessage?: boolean;
|
||||
/** 请求失败命中过期 access code 时,不再触发 refresh-token 流程。 */
|
||||
skipTokenRefresh?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
src/service/request/error-message.ts
Normal file
32
src/service/request/error-message.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export const SESSION_EXPIRED_MESSAGE = '登录已失效,请重新登录';
|
||||
|
||||
export interface ErrorMessageSuppressOptions {
|
||||
backendErrorCode: string;
|
||||
suppressErrorMessage?: boolean;
|
||||
logoutCodes: string[];
|
||||
modalLogoutCodes: string[];
|
||||
expiredTokenCodes: string[];
|
||||
}
|
||||
|
||||
export interface BackendFailDeferOptions {
|
||||
suppressErrorMessage?: boolean;
|
||||
skipTokenRefresh?: boolean;
|
||||
}
|
||||
|
||||
export function parseServiceCodes(codes?: string) {
|
||||
return codes?.split(',').filter(Boolean) || [];
|
||||
}
|
||||
|
||||
export function shouldDeferBackendFailToCaller(options: BackendFailDeferOptions) {
|
||||
return Boolean(options.suppressErrorMessage && options.skipTokenRefresh);
|
||||
}
|
||||
|
||||
export function shouldSuppressErrorMessage(options: ErrorMessageSuppressOptions) {
|
||||
if (options.suppressErrorMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const handledCodes = [...options.logoutCodes, ...options.modalLogoutCodes, ...options.expiredTokenCodes];
|
||||
|
||||
return handledCodes.includes(options.backendErrorCode);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { localStg } from '@/utils/storage';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { $t } from '@/locales';
|
||||
import { applyApiEncrypt } from './api-encrypt';
|
||||
import { parseServiceCodes, shouldDeferBackendFailToCaller, shouldSuppressErrorMessage } from './error-message';
|
||||
import { getAuthorization, handleExpiredRequest, notifySessionExpired, showErrorMsg } from './shared';
|
||||
import { withDedupe } from './dedupe';
|
||||
import type { RequestInstanceState } from './type';
|
||||
@@ -48,6 +49,15 @@ export const request = withDedupe(
|
||||
const authStore = useAuthStore();
|
||||
const responseCode = String(response.data.code);
|
||||
|
||||
if (
|
||||
shouldDeferBackendFailToCaller({
|
||||
suppressErrorMessage: response.config.suppressErrorMessage,
|
||||
skipTokenRefresh: response.config.skipTokenRefresh
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
authStore.resetStore();
|
||||
}
|
||||
@@ -61,14 +71,14 @@ export const request = withDedupe(
|
||||
|
||||
// 当后端返回码命中 `logoutCodes` 时,表示登录态已失效,需要提示后退出登录
|
||||
// 走 notifySessionExpired 而不是裸 resetStore:保证并发请求只弹一次 toast、只清一次状态
|
||||
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
|
||||
const logoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_LOGOUT_CODES);
|
||||
if (logoutCodes.includes(responseCode)) {
|
||||
notifySessionExpired();
|
||||
return null;
|
||||
}
|
||||
|
||||
// 当后端返回码命中 `modalLogoutCodes` 时,表示通过弹窗提示后再退出登录
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||
const modalLogoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES);
|
||||
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
|
||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
|
||||
|
||||
@@ -92,8 +102,13 @@ export const request = withDedupe(
|
||||
|
||||
// 当后端返回码命中 `expiredTokenCodes` 时,表示 token 已过期,需要刷新 token
|
||||
// `refreshToken` 接口不能再返回 `expiredTokenCodes` 中的错误码,否则会形成死循环,应返回 `logoutCodes` 或 `modalLogoutCodes`
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
||||
const expiredTokenCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES);
|
||||
if (expiredTokenCodes.includes(responseCode)) {
|
||||
if (response.config.skipTokenRefresh) {
|
||||
notifySessionExpired();
|
||||
return null;
|
||||
}
|
||||
|
||||
const success = await handleExpiredRequest(request.state);
|
||||
if (success) {
|
||||
const Authorization = getAuthorization();
|
||||
@@ -117,15 +132,19 @@ export const request = withDedupe(
|
||||
backendErrorCode = String(error.response?.data?.code || '');
|
||||
}
|
||||
|
||||
// 这类错误信息已经通过弹窗展示,不再重复提示
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||
if (modalLogoutCodes.includes(backendErrorCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// token 过期时会自动刷新并重试请求,这里无需额外提示
|
||||
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
|
||||
if (expiredTokenCodes.includes(backendErrorCode)) {
|
||||
const suppressErrorMessage = Boolean(error.config?.suppressErrorMessage);
|
||||
const logoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_LOGOUT_CODES);
|
||||
const modalLogoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES);
|
||||
const expiredTokenCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES);
|
||||
if (
|
||||
shouldSuppressErrorMessage({
|
||||
backendErrorCode,
|
||||
suppressErrorMessage,
|
||||
logoutCodes,
|
||||
modalLogoutCodes,
|
||||
expiredTokenCodes
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { fetchRefreshToken } from '../api';
|
||||
import { SESSION_EXPIRED_MESSAGE } from './error-message';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
export function getAuthorization() {
|
||||
@@ -51,7 +52,7 @@ export function notifySessionExpired() {
|
||||
if (sessionExpiredNotified) return;
|
||||
sessionExpiredNotified = true;
|
||||
|
||||
window.$message?.error('登录已过期,请重新登录');
|
||||
window.$message?.error(SESSION_EXPIRED_MESSAGE);
|
||||
|
||||
const { resetStore } = useAuthStore();
|
||||
resetStore();
|
||||
|
||||
@@ -51,16 +51,27 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
|
||||
clearAuthStorage();
|
||||
|
||||
authStore.$reset();
|
||||
dictStore.resetDictCache();
|
||||
objectContextStore.$reset();
|
||||
// setup store 没有内置 $reset,需要显式重置内部状态,避免 token / userInfo 残留导致 isLogin 误判。
|
||||
token.value = '';
|
||||
Object.assign(userInfo, {
|
||||
userId: '',
|
||||
userName: '',
|
||||
nickname: '',
|
||||
roles: [],
|
||||
buttons: []
|
||||
});
|
||||
|
||||
if (!route.meta.constant) {
|
||||
dictStore.resetDictCache();
|
||||
objectContextStore.clearContext();
|
||||
|
||||
// 用路由名判断当前是否已在登录页,避免依赖 route.meta.constant ——
|
||||
// workbench 等首页也是常量路由,原写法会让常量路由上的登出请求不跳转。
|
||||
if (route.name !== 'login') {
|
||||
await toLogin();
|
||||
}
|
||||
|
||||
tabStore.cacheTabs();
|
||||
routeStore.resetStore();
|
||||
await routeStore.resetStore();
|
||||
}
|
||||
|
||||
/** Record the user ID of the previous login session Used to compare with the current user ID on next login */
|
||||
|
||||
@@ -149,9 +149,16 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
|
||||
/** 重置 store */
|
||||
async function resetStore() {
|
||||
const routeStore = useRouteStore();
|
||||
|
||||
routeStore.$reset();
|
||||
// setup store 没有内置 $reset,需要显式重置内部状态。
|
||||
// 否则 isInitConstantRoute / isInitAuthRoute 一直停在 true,导致下面 initConstantRoute 早返,
|
||||
// 路由被 resetVueRoutes 摘掉后无法重新注册,菜单和导航都会失效。
|
||||
setIsInitConstantRoute(false);
|
||||
setIsInitAuthRoute(false);
|
||||
constantRoutes.value = [];
|
||||
authRoutes.value = [];
|
||||
menus.value = [];
|
||||
cacheRoutes.value = [];
|
||||
excludeCacheRoutes.value = [];
|
||||
|
||||
resetVueRoutes();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { CircleCheckFilled, DeleteFilled, FolderOpened, VideoPause } from '@element-plus/icons-vue';
|
||||
import { Box, DeleteFilled, VideoPause, VideoPlay } from '@element-plus/icons-vue';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||
import { fetchGetProductOverviewSummary, fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api';
|
||||
@@ -76,14 +76,14 @@ const statusNavMetas: StatusNavMeta[] = [
|
||||
label: '启用产品',
|
||||
description: '当前正常服务中的产品',
|
||||
tone: 'teal',
|
||||
icon: CircleCheckFilled
|
||||
icon: VideoPlay
|
||||
},
|
||||
{
|
||||
key: 'archived',
|
||||
label: '归档产品',
|
||||
description: '已完成阶段目标的产品',
|
||||
tone: 'slate',
|
||||
icon: FolderOpened
|
||||
icon: Box
|
||||
},
|
||||
{
|
||||
key: 'paused',
|
||||
@@ -109,7 +109,7 @@ const operateVisible = ref(false);
|
||||
const editingRow = ref<Api.Product.Product | null>(null);
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const { dictData: directionOptions, getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
||||
const { getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
||||
|
||||
const statusCounts = ref<Record<string, number>>({
|
||||
active: 0,
|
||||
@@ -129,29 +129,6 @@ const statusItems = computed(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
const overviewMetrics = computed(() => [
|
||||
{
|
||||
label: '可见产品',
|
||||
value: Object.values(statusCounts.value).reduce((sum, count) => sum + count, 0),
|
||||
hint: '当前接口可查询到的产品总量'
|
||||
},
|
||||
{
|
||||
label: '当前启用',
|
||||
value: statusCounts.value.active ?? 0,
|
||||
hint: '正在持续服务和维护的产品'
|
||||
},
|
||||
{
|
||||
label: '产品方向',
|
||||
value: directionOptions.value.length,
|
||||
hint: '已加载的方向字典项数量'
|
||||
},
|
||||
{
|
||||
label: '废弃产品',
|
||||
value: statusCounts.value.abandoned ?? 0,
|
||||
hint: '已明确停止建设的产品'
|
||||
}
|
||||
]);
|
||||
|
||||
function getDirectionLabel(directionCode?: string | null) {
|
||||
return getDirectionDictLabel(directionCode, '--');
|
||||
}
|
||||
@@ -288,7 +265,7 @@ async function handleResetSearch() {
|
||||
|
||||
async function handleStatusChange(status: Api.Product.ProductStatusCode) {
|
||||
selectedStatus.value = status;
|
||||
await reloadProductTable(1);
|
||||
await Promise.all([loadOverviewData(), reloadProductTable(1)]);
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
@@ -326,14 +303,6 @@ onMounted(async () => {
|
||||
>
|
||||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||
<ElCard class="product-overview-card card-wrapper">
|
||||
<div class="product-overview-card__stats">
|
||||
<div v-for="item in overviewMetrics" :key="item.label" class="product-overview-card__stat">
|
||||
<span class="product-overview-card__stat-label">{{ item.label }}</span>
|
||||
<strong class="product-overview-card__stat-value">{{ item.value }}</strong>
|
||||
<small class="product-overview-card__stat-hint">{{ item.hint }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-status-panel__list">
|
||||
<button
|
||||
v-for="item in statusItems"
|
||||
@@ -441,45 +410,10 @@ onMounted(async () => {
|
||||
linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 97%));
|
||||
}
|
||||
|
||||
.product-overview-card__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.product-overview-card__stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgb(226 232 240 / 88%);
|
||||
border-radius: 18px;
|
||||
background-color: rgb(255 255 255 / 84%);
|
||||
}
|
||||
|
||||
.product-overview-card__stat-label {
|
||||
color: rgb(100 116 139 / 90%);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.product-overview-card__stat-value {
|
||||
color: rgb(15 23 42 / 94%);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.product-overview-card__stat-hint {
|
||||
color: rgb(100 116 139 / 90%);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.product-status-panel__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.product-status-item {
|
||||
@@ -594,10 +528,4 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.product-overview-card__stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,7 +71,7 @@ const operateVisible = ref(false);
|
||||
const editingRow = ref<Api.Project.Project | null>(null);
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const { dictData: directionOptions, getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
||||
const { getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
||||
const { getLabel: getProjectTypeLabel } = useDict(RDMS_PROJECT_TYPE_DICT_CODE);
|
||||
|
||||
const statusCounts = ref<Record<string, number>>({
|
||||
@@ -243,7 +243,7 @@ async function handleResetSearch() {
|
||||
|
||||
async function handleStatusChange(status: Api.Project.ProjectStatusCode) {
|
||||
selectedStatus.value = status;
|
||||
await reloadProjectTable(1);
|
||||
await Promise.all([loadOverviewData(), reloadProjectTable(1)]);
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
@@ -282,7 +282,6 @@ onMounted(async () => {
|
||||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||
<ProjectOverviewCard
|
||||
:status-counts="statusCounts"
|
||||
:direction-count="directionOptions.length"
|
||||
:selected-status="selectedStatus"
|
||||
@status-change="handleStatusChange"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { CircleCheckFilled, DeleteFilled, DocumentAdd, FolderOpened, VideoPause } from '@element-plus/icons-vue';
|
||||
import { Box, CircleCheckFilled, DeleteFilled, DocumentAdd, VideoPause, VideoPlay } from '@element-plus/icons-vue';
|
||||
|
||||
defineOptions({ name: 'ProjectOverviewCard' });
|
||||
|
||||
@@ -15,7 +15,6 @@ interface StatusNavMeta {
|
||||
|
||||
interface Props {
|
||||
statusCounts: Record<string, number>;
|
||||
directionCount: number;
|
||||
selectedStatus: Api.Project.ProjectStatusCode;
|
||||
}
|
||||
|
||||
@@ -40,7 +39,7 @@ const statusNavMetas: StatusNavMeta[] = [
|
||||
label: '进行中',
|
||||
description: '正在执行的项目',
|
||||
tone: 'teal',
|
||||
icon: CircleCheckFilled
|
||||
icon: VideoPlay
|
||||
},
|
||||
{
|
||||
key: 'paused',
|
||||
@@ -54,7 +53,7 @@ const statusNavMetas: StatusNavMeta[] = [
|
||||
label: '已完成',
|
||||
description: '达成目标的项目',
|
||||
tone: 'teal',
|
||||
icon: FolderOpened
|
||||
icon: CircleCheckFilled
|
||||
},
|
||||
{
|
||||
key: 'cancelled',
|
||||
@@ -68,7 +67,7 @@ const statusNavMetas: StatusNavMeta[] = [
|
||||
label: '归档项目',
|
||||
description: '已收口归档的历史项目',
|
||||
tone: 'slate',
|
||||
icon: FolderOpened
|
||||
icon: Box
|
||||
}
|
||||
];
|
||||
|
||||
@@ -79,29 +78,6 @@ const statusItems = computed(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
const overviewMetrics = computed(() => [
|
||||
{
|
||||
label: '总项目数',
|
||||
value: Object.values(props.statusCounts).reduce((sum, count) => sum + count, 0),
|
||||
hint: '当前接口可查询到的项目总量'
|
||||
},
|
||||
{
|
||||
label: '进行中',
|
||||
value: props.statusCounts.active ?? 0,
|
||||
hint: '正在执行的项目'
|
||||
},
|
||||
{
|
||||
label: '待开始',
|
||||
value: props.statusCounts.pending ?? 0,
|
||||
hint: '等待启动的项目'
|
||||
},
|
||||
{
|
||||
label: '作废项目',
|
||||
value: props.statusCounts.cancelled ?? 0,
|
||||
hint: '已终止或取消推进的项目'
|
||||
}
|
||||
]);
|
||||
|
||||
function handleStatusClick(status: Api.Project.ProjectStatusCode) {
|
||||
emit('status-change', status);
|
||||
}
|
||||
@@ -109,14 +85,6 @@ function handleStatusClick(status: Api.Project.ProjectStatusCode) {
|
||||
|
||||
<template>
|
||||
<ElCard class="project-overview-card card-wrapper">
|
||||
<div class="project-overview-card__stats">
|
||||
<div v-for="item in overviewMetrics" :key="item.label" class="project-overview-card__stat">
|
||||
<span class="project-overview-card__stat-label">{{ item.label }}</span>
|
||||
<strong class="project-overview-card__stat-value">{{ item.value }}</strong>
|
||||
<small class="project-overview-card__stat-hint">{{ item.hint }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-status-panel__list">
|
||||
<button
|
||||
v-for="item in statusItems"
|
||||
@@ -153,45 +121,10 @@ function handleStatusClick(status: Api.Project.ProjectStatusCode) {
|
||||
linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 97%));
|
||||
}
|
||||
|
||||
.project-overview-card__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.project-overview-card__stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgb(226 232 240 / 88%);
|
||||
border-radius: 18px;
|
||||
background-color: rgb(255 255 255 / 84%);
|
||||
}
|
||||
|
||||
.project-overview-card__stat-label {
|
||||
color: rgb(100 116 139 / 90%);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.project-overview-card__stat-value {
|
||||
color: rgb(15 23 42 / 94%);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.project-overview-card__stat-hint {
|
||||
color: rgb(100 116 139 / 90%);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.project-status-panel__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.project-status-item {
|
||||
@@ -293,10 +226,4 @@ function handleStatusClick(status: Api.Project.ProjectStatusCode) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.project-overview-card__stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
78
tests/request-session-expired-message.test.ts
Normal file
78
tests/request-session-expired-message.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
SESSION_EXPIRED_MESSAGE,
|
||||
shouldDeferBackendFailToCaller,
|
||||
shouldSuppressErrorMessage
|
||||
} from '../src/service/request/error-message';
|
||||
|
||||
describe('request session expired message handling', () => {
|
||||
it('uses the unified session expired message', () => {
|
||||
assert.equal(SESSION_EXPIRED_MESSAGE, '登录已失效,请重新登录');
|
||||
});
|
||||
|
||||
it('suppresses raw backend errors when the request is marked silent', () => {
|
||||
assert.equal(
|
||||
shouldSuppressErrorMessage({
|
||||
backendErrorCode: '500',
|
||||
suppressErrorMessage: true,
|
||||
logoutCodes: [],
|
||||
modalLogoutCodes: [],
|
||||
expiredTokenCodes: []
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('lets refresh-token callers handle every backend failure silently', () => {
|
||||
assert.equal(
|
||||
shouldDeferBackendFailToCaller({
|
||||
suppressErrorMessage: true,
|
||||
skipTokenRefresh: true
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('suppresses auth lifecycle error codes handled by request flow', () => {
|
||||
assert.equal(
|
||||
shouldSuppressErrorMessage({
|
||||
backendErrorCode: '401',
|
||||
logoutCodes: ['401'],
|
||||
modalLogoutCodes: [],
|
||||
expiredTokenCodes: []
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
shouldSuppressErrorMessage({
|
||||
backendErrorCode: '7777',
|
||||
logoutCodes: [],
|
||||
modalLogoutCodes: ['7777'],
|
||||
expiredTokenCodes: []
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
shouldSuppressErrorMessage({
|
||||
backendErrorCode: '1002023000',
|
||||
logoutCodes: [],
|
||||
modalLogoutCodes: [],
|
||||
expiredTokenCodes: ['1002023000']
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps normal backend errors visible', () => {
|
||||
assert.equal(
|
||||
shouldSuppressErrorMessage({
|
||||
backendErrorCode: '500',
|
||||
logoutCodes: ['401'],
|
||||
modalLogoutCodes: ['7777'],
|
||||
expiredTokenCodes: ['1002023000']
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user