# Conflicts:
#	src/service/api/product.ts
#	src/service/api/project.ts
#	src/typings/api/project.d.ts
This commit is contained in:
dk
2026-05-13 21:20:59 +08:00
59 changed files with 8046 additions and 919 deletions

View File

@@ -14,6 +14,7 @@ interface BackendLoginToken {
interface BackendUserInfoDTO {
userId: string | number;
userName?: string | null;
nickname?: string | null;
roles?: string[] | null;
buttons?: string[] | null;
}
@@ -32,6 +33,7 @@ function mapUserInfo(data: BackendUserInfoDTO): Api.Auth.UserInfo {
return {
userId: String(data.userId ?? ''),
userName: data.userName ?? '',
nickname: data.nickname ?? '',
roles: data.roles ?? [],
buttons: data.buttons ?? []
};

View File

@@ -3,6 +3,13 @@ import { request } from '../request';
const FILE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/file`;
export interface UploadFileResult {
/** infra_file.id 的字符串形式(避免 Long 精度丢失) */
id: string;
/** 文件访问 URL私有桶带签名、公开桶裸 URL */
url: string;
}
/** 上传文件(模式一:后端中转) */
export function uploadFile(file: File, directory?: string) {
const formData = new FormData();
@@ -11,9 +18,38 @@ export function uploadFile(file: File, directory?: string) {
formData.append('directory', directory);
}
return request<string>({
return request<UploadFileResult>({
url: `${FILE_PREFIX}/upload`,
method: 'post',
data: formData
});
}
/**
* 删除文件
*
* 业务表单"取消/关闭/标记删除"场景调用本接口清理孤儿文件。
* 删除已不存在的文件(后端返回错误码 `1001003001`)应由调用方视为成功并吞掉。
*/
export function deleteFile(id: string) {
return request<boolean>({
url: `${FILE_PREFIX}/delete`,
method: 'delete',
params: { id }
});
}
/**
* 下载文件(流)
*
* 走后端代理接口 `/system/file/download?id=xxx`,由后端读取对象存储并以字节流返回。
* 私有桶下不要直接打开 `infra_file.url`,签名地址会过期。
*/
export function downloadFile(id: string) {
return request<Blob, 'blob'>({
url: `${FILE_PREFIX}/download`,
method: 'get',
params: { id },
responseType: 'blob'
});
}

View File

@@ -139,6 +139,18 @@ export async function fetchCreateProduct(data: Api.Product.SaveProductParams) {
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 创建产品(含初始团队,原子接口) */
export async function fetchCreateProductWithTeam(data: Api.Product.CreateProductWithTeamParams) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${PRODUCT_PREFIX}/create-with-team`,
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 更新产品 */
export function fetchUpdateProduct(data: Api.Product.UpdateProductParams) {
return request<boolean>({

View File

@@ -36,14 +36,14 @@ export type ProjectExecutionResponse = Omit<
progressRate?: number | null;
};
export type ExecutionMemberResponse = Omit<Api.Project.ExecutionMember, 'id' | 'executionId' | 'userId'> & {
export type ExecutionAssigneeResponse = Omit<Api.Project.ExecutionAssignee, 'id' | 'executionId' | 'userId'> & {
id: StringIdResponse;
executionId: StringIdResponse;
userId: StringIdResponse;
};
export type ExecutionMemberLogResponse = Omit<
Api.Project.ExecutionMemberLog,
export type ExecutionAssigneeLogResponse = Omit<
Api.Project.ExecutionAssigneeLog,
'id' | 'executionId' | 'userId' | 'operatorUserId'
> & {
id: StringIdResponse;
@@ -52,6 +52,55 @@ export type ExecutionMemberLogResponse = Omit<
operatorUserId: StringIdResponse;
};
type TaskAssigneeRefResponse = Omit<Api.Project.TaskAssigneeRef, 'id' | 'userId'> & {
id: StringIdResponse;
userId: StringIdResponse;
};
/**
* 后端 attachments 项的兼容形态:历史/当前响应字段名是 `id`,前端类型统一用 `fileId`。
* normalizeAttachments 负责把两者归一成 `fileId`。
*/
type AttachmentItemResponse = Omit<Api.Project.AttachmentItem, 'fileId'> & {
fileId?: StringIdResponse;
id?: StringIdResponse;
};
function normalizeAttachments(list?: AttachmentItemResponse[] | null): Api.Project.AttachmentItem[] | null {
if (!list) {
return null;
}
return list.map(item => {
const rawId = item.fileId ?? item.id;
return {
...item,
fileId: rawId === null || rawId === undefined ? '' : String(rawId)
};
});
}
/**
* 5.6 单独接口返的协办人字段(与 5.3 嵌入字段命名口径不一致:返 userNickname 而非 nickname
* 经 normalizeTaskAssignee 归一化后对外统一为 Api.Project.TaskAssigneeRef。
*/
export type TaskAssigneeFromApiResponse = {
id: StringIdResponse;
taskId: StringIdResponse;
userId: StringIdResponse;
userNickname?: string | null;
joinedAt?: string | null;
};
export type TaskAssigneeLogResponse = Omit<
Api.Project.TaskAssigneeLog,
'id' | 'taskId' | 'userId' | 'operatorUserId'
> & {
id: StringIdResponse;
taskId: StringIdResponse;
userId: StringIdResponse;
operatorUserId: StringIdResponse;
};
export type ProjectTaskResponse = Omit<
Api.Project.ProjectTask,
| 'id'
@@ -65,6 +114,8 @@ export type ProjectTaskResponse = Omit<
| 'actualStartDate'
| 'actualEndDate'
| 'progressRate'
| 'assignees'
| 'attachments'
> & {
id: StringIdResponse;
projectId: StringIdResponse;
@@ -77,6 +128,16 @@ export type ProjectTaskResponse = Omit<
actualStartDate?: ProjectLocalDateValue;
actualEndDate?: ProjectLocalDateValue;
progressRate?: number | null;
assignees?: TaskAssigneeRefResponse[] | null;
attachments?: AttachmentItemResponse[] | null;
totalSpentHours?: number | null;
};
export type TaskWorklogResponse = Omit<Api.Project.TaskWorklog, 'id' | 'taskId' | 'userId' | 'attachments'> & {
id: StringIdResponse;
taskId: StringIdResponse;
userId: StringIdResponse;
attachments?: AttachmentItemResponse[] | null;
};
export interface ProjectMemberResponse {
@@ -194,7 +255,7 @@ export function normalizeProjectExecution(response: ProjectExecutionResponse): A
};
}
export function normalizeExecutionMember(response: ExecutionMemberResponse): Api.Project.ExecutionMember {
export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee {
return {
...response,
id: normalizeStringId(response.id),
@@ -207,7 +268,9 @@ export function normalizeExecutionMember(response: ExecutionMemberResponse): Api
};
}
export function normalizeExecutionMemberLog(response: ExecutionMemberLogResponse): Api.Project.ExecutionMemberLog {
export function normalizeExecutionAssigneeLog(
response: ExecutionAssigneeLogResponse
): Api.Project.ExecutionAssigneeLog {
return {
...response,
id: normalizeStringId(response.id),
@@ -239,6 +302,49 @@ export function normalizeProjectTask(response: ProjectTaskResponse): Api.Project
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
taskDesc: response.taskDesc ?? null,
lastStatusReason: response.lastStatusReason ?? null
lastStatusReason: response.lastStatusReason ?? null,
assignees:
response.assignees?.map(item => ({
id: normalizeStringId(item.id),
userId: normalizeStringId(item.userId),
nickname: item.nickname ?? ''
})) ?? null,
attachments: normalizeAttachments(response.attachments),
totalSpentHours: response.totalSpentHours ?? null
};
}
export function normalizeTaskWorklog(response: TaskWorklogResponse): Api.Project.TaskWorklog {
return {
...response,
id: normalizeStringId(response.id),
taskId: normalizeStringId(response.taskId),
userId: normalizeStringId(response.userId),
userNickname: response.userNickname ?? null,
workContent: response.workContent ?? null,
attachments: normalizeAttachments(response.attachments),
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0
};
}
export function normalizeTaskAssignee(response: TaskAssigneeFromApiResponse): Api.Project.TaskAssigneeRef {
return {
id: normalizeStringId(response.id),
userId: normalizeStringId(response.userId),
nickname: response.userNickname ?? '',
joinedAt: response.joinedAt ?? null
};
}
export function normalizeTaskAssigneeLog(response: TaskAssigneeLogResponse): Api.Project.TaskAssigneeLog {
return {
...response,
id: normalizeStringId(response.id),
taskId: normalizeStringId(response.taskId),
userId: normalizeStringId(response.userId),
operatorUserId: normalizeStringId(response.operatorUserId),
userNicknameSnapshot: response.userNicknameSnapshot ?? null,
operatorNicknameSnapshot: response.operatorNicknameSnapshot ?? null,
reason: response.reason ?? null
};
}

View File

@@ -8,19 +8,25 @@ import {
safeJsonRequestConfig
} from './shared';
import {
type ExecutionMemberLogResponse,
type ExecutionMemberResponse,
type ExecutionAssigneeLogResponse,
type ExecutionAssigneeResponse,
type ProjectExecutionResponse,
type ProjectLocalDateValue,
type ProjectMemberResponse,
type ProjectTaskResponse,
type TaskAssigneeFromApiResponse,
type TaskAssigneeLogResponse,
type TaskWorklogResponse,
getProjectLifecycleActions,
normalizeExecutionMember,
normalizeExecutionMemberLog,
normalizeExecutionAssignee,
normalizeExecutionAssigneeLog,
normalizeProjectExecution,
normalizeProjectLocalDate,
normalizeProjectMember,
normalizeProjectTask
normalizeProjectTask,
normalizeTaskAssignee,
normalizeTaskAssigneeLog,
normalizeTaskWorklog
} from './project-shared';
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
@@ -159,6 +165,18 @@ export async function fetchCreateProject(data: Api.Project.SaveProjectParams) {
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 创建项目(含初始团队,原子接口) */
export async function fetchCreateProjectWithTeam(data: Api.Project.CreateProjectWithTeamParams) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/create-with-team`,
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 更新项目 */
export function fetchUpdateProject(data: Api.Project.UpdateProjectParams) {
return request<boolean>({
@@ -340,7 +358,7 @@ export async function fetchGetProjectExecution(projectId: string, executionId: s
}
/** 创建项目执行 */
export async function fetchCreateProjectExecution(projectId: string, data: Api.Project.SaveProjectExecutionParams) {
export async function fetchCreateProjectExecution(projectId: string, data: Api.Project.CreateProjectExecutionParams) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: getExecutionPrefix(projectId),
@@ -355,7 +373,7 @@ export async function fetchCreateProjectExecution(projectId: string, data: Api.P
export function fetchUpdateProjectExecution(
projectId: string,
executionId: string,
data: Api.Project.SaveProjectExecutionParams
data: Api.Project.UpdateProjectExecutionParams
) {
return request<boolean>({
...safeJsonRequestConfig,
@@ -379,6 +397,20 @@ export function fetchChangeProjectExecutionOwner(
});
}
/** 删除项目执行 */
export function fetchDeleteProjectExecution(
projectId: string,
executionId: string,
data: Api.Project.DeleteProjectExecutionParams
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}`,
method: 'delete',
data
});
}
/** 变更项目执行状态 */
export function fetchChangeProjectExecutionStatus(
projectId: string,
@@ -393,28 +425,28 @@ export function fetchChangeProjectExecutionStatus(
});
}
/** 获取项目执行成员 */
export async function fetchGetProjectExecutionMembers(projectId: string, executionId: string) {
const result = await request<ExecutionMemberResponse[]>({
/** 获取项目执行协办人 */
export async function fetchGetProjectExecutionAssignees(projectId: string, executionId: string) {
const result = await request<ExecutionAssigneeResponse[]>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/members`,
url: `${getExecutionPrefix(projectId)}/${executionId}/assignees`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<ExecutionMemberResponse[]>, data =>
data.map(normalizeExecutionMember)
return mapServiceResult(result as ServiceRequestResult<ExecutionAssigneeResponse[]>, data =>
data.map(normalizeExecutionAssignee)
);
}
/** 创建项目执行成员 */
export async function fetchCreateProjectExecutionMember(
/** 创建项目执行协办人 */
export async function fetchCreateProjectExecutionAssignee(
projectId: string,
executionId: string,
data: Api.Project.CreateExecutionMemberParams
data: Api.Project.CreateExecutionAssigneeParams
) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/members`,
url: `${getExecutionPrefix(projectId)}/${executionId}/assignees`,
method: 'post',
data
});
@@ -422,37 +454,40 @@ export async function fetchCreateProjectExecutionMember(
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 移除项目执行成员 */
export function fetchInactiveProjectExecutionMember(
/** 移除项目执行协办人 */
export function fetchInactiveProjectExecutionAssignee(
projectId: string,
executionId: string,
payload: { memberId: string; data: Api.Project.InactiveExecutionMemberParams }
payload: { assigneeId: string; data: Api.Project.InactiveExecutionAssigneeParams }
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/members/${payload.memberId}/inactive`,
url: `${getExecutionPrefix(projectId)}/${executionId}/assignees/${payload.assigneeId}/inactive`,
method: 'post',
data: payload.data
});
}
/** 获取项目执行成员变更历史分页 */
export async function fetchGetProjectExecutionMemberLogPage(
/** 获取项目执行协办人变更历史分页 */
export async function fetchGetProjectExecutionAssigneeLogPage(
projectId: string,
executionId: string,
params?: Api.Project.ExecutionMemberLogSearchParams
params?: Api.Project.ExecutionAssigneeLogSearchParams
) {
const result = await request<Api.Project.PageResult<ExecutionMemberLogResponse>>({
const result = await request<Api.Project.PageResult<ExecutionAssigneeLogResponse>>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/member-logs`,
url: `${getExecutionPrefix(projectId)}/${executionId}/assignee-logs`,
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<Api.Project.PageResult<ExecutionMemberLogResponse>>, data => ({
...data,
list: data.list.map(normalizeExecutionMemberLog)
}));
return mapServiceResult(
result as ServiceRequestResult<Api.Project.PageResult<ExecutionAssigneeLogResponse>>,
data => ({
...data,
list: data.list.map(normalizeExecutionAssigneeLog)
})
);
}
/** 获取项目任务分页 */
@@ -529,6 +564,22 @@ export function fetchUpdateProjectTask(
});
}
/** 删除项目任务 */
// eslint-disable-next-line max-params
export function fetchDeleteProjectTask(
projectId: string,
executionId: string,
taskId: string,
data: Api.Project.DeleteProjectTaskParams
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/${taskId}`,
method: 'delete',
data
});
}
/** 变更项目任务状态 */
export function fetchChangeProjectTaskStatus(
projectId: string,
@@ -543,6 +594,151 @@ export function fetchChangeProjectTaskStatus(
});
}
type TaskWorklogPageResponse = Api.Project.PageResult<TaskWorklogResponse>;
function getWorklogPrefix(projectId: string, executionId: string, taskId: string) {
return `${getTaskPrefix(projectId, executionId)}/${taskId}/worklogs`;
}
/** 获取任务工时分页 */
// eslint-disable-next-line max-params
export async function fetchGetProjectTaskWorklogPage(
projectId: string,
executionId: string,
taskId: string,
params?: Api.Project.TaskWorklogSearchParams
) {
const result = await request<TaskWorklogPageResponse>({
...safeJsonRequestConfig,
url: getWorklogPrefix(projectId, executionId, taskId),
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<TaskWorklogPageResponse>, data => ({
...data,
list: data.list.map(normalizeTaskWorklog)
}));
}
/** 新增任务工时 */
// eslint-disable-next-line max-params
export async function fetchCreateProjectTaskWorklog(
projectId: string,
executionId: string,
taskId: string,
data: Api.Project.SaveTaskWorklogParams
) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: getWorklogPrefix(projectId, executionId, taskId),
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 修改任务工时 */
// eslint-disable-next-line max-params
export function fetchUpdateProjectTaskWorklog(
projectId: string,
executionId: string,
taskId: string,
payload: { worklogId: string; data: Api.Project.SaveTaskWorklogParams }
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getWorklogPrefix(projectId, executionId, taskId)}/${payload.worklogId}`,
method: 'put',
data: payload.data
});
}
/** 删除任务工时 */
// eslint-disable-next-line max-params
export function fetchDeleteProjectTaskWorklog(
projectId: string,
executionId: string,
taskId: string,
worklogId: string
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getWorklogPrefix(projectId, executionId, taskId)}/${worklogId}`,
method: 'delete'
});
}
/** 5.6 获取任务协办人列表(仅当前活跃) */
export async function fetchGetProjectTaskAssignees(projectId: string, executionId: string, taskId: string) {
const result = await request<TaskAssigneeFromApiResponse[]>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/${taskId}/assignees`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<TaskAssigneeFromApiResponse[]>, data =>
data.map(normalizeTaskAssignee)
);
}
/** 5.7 加入任务协办人 */
// eslint-disable-next-line max-params
export async function fetchCreateProjectTaskAssignee(
projectId: string,
executionId: string,
taskId: string,
data: Api.Project.CreateTaskAssigneeParams
) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/${taskId}/assignees`,
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 5.8 退出任务协办人 */
// eslint-disable-next-line max-params
export function fetchInactiveProjectTaskAssignee(
projectId: string,
executionId: string,
taskId: string,
assigneeId: string,
data: Api.Project.InactiveTaskAssigneeParams
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/${taskId}/assignees/${assigneeId}/inactive`,
method: 'post',
data
});
}
/** 5.9 任务协办人变更历史分页 */
// eslint-disable-next-line max-params
export async function fetchGetProjectTaskAssigneeLogPage(
projectId: string,
executionId: string,
taskId: string,
params?: Api.Project.TaskAssigneeLogSearchParams
) {
const result = await request<Api.Project.PageResult<TaskAssigneeLogResponse>>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/${taskId}/assignee-logs`,
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<Api.Project.PageResult<TaskAssigneeLogResponse>>, data => ({
...data,
list: data.list.map(normalizeTaskAssigneeLog)
}));
}
// ========== 项目需求 API ==========
const PROJECT_REQUIREMENT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project/requirement`;

View File

@@ -0,0 +1,79 @@
import type { InternalAxiosRequestConfig } from 'axios';
declare module 'axios' {
interface AxiosRequestConfig {
dedupe?: boolean;
}
}
const WRITE_METHODS = new Set(['POST', 'PUT', 'DELETE', 'PATCH']);
type DedupableConfig = Pick<InternalAxiosRequestConfig, 'method' | 'url' | 'data' | 'params'> & {
dedupe?: boolean;
};
function isFormDataLike(value: unknown): boolean {
if (typeof FormData !== 'undefined' && value instanceof FormData) return true;
if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
return false;
}
function stableJson(value: unknown): string {
if (value === null || value === undefined) return '';
if (typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
const obj = value as Record<string, unknown>;
const keys = Object.keys(obj).sort();
return `{${keys.map(k => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(',')}}`;
}
export function computeDedupeKey(config: DedupableConfig): string | null {
const method = (config.method ?? 'GET').toUpperCase();
if (!WRITE_METHODS.has(method)) return null;
if (config.dedupe === false) return null;
if (isFormDataLike(config.data)) return null;
const url = config.url ?? '';
const paramsPart = stableJson(config.params);
const bodyPart = stableJson(config.data);
return `${method}|${url}?${paramsPart}|${bodyPart}`;
}
const DEFAULT_TTL_MS = 30_000;
export interface WithDedupeOptions {
ttlMs?: number;
now?: () => number;
}
type AnyRequestFn = (...args: any[]) => Promise<unknown>;
export function withDedupe<TFn extends AnyRequestFn>(request: TFn, options: WithDedupeOptions = {}): TFn {
const ttl = options.ttlMs ?? DEFAULT_TTL_MS;
const now = options.now ?? Date.now;
const pending = new Map<string, { promise: Promise<unknown>; expiresAt: number }>();
return new Proxy(request, {
apply(target, thisArg, args: Parameters<TFn>) {
const [config] = args;
const key = computeDedupeKey(config as DedupableConfig);
if (key === null) return Reflect.apply(target, thisArg, args);
const cached = pending.get(key);
if (cached && cached.expiresAt > now()) return cached.promise;
if (cached) pending.delete(key);
const promise = Promise.resolve()
.then(() => Reflect.apply(target, thisArg, args))
.finally(() => {
const current = pending.get(key);
if (current && current.promise === promise) {
pending.delete(key);
}
});
pending.set(key, { promise, expiresAt: now() + ttl });
return promise;
}
}) as TFn;
}

View File

@@ -6,125 +6,128 @@ import { getServiceBaseURL } from '@/utils/service';
import { $t } from '@/locales';
import { applyApiEncrypt } from './api-encrypt';
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
import { withDedupe } from './dedupe';
import type { RequestInstanceState } from './type';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
export const request = createFlatRequest(
{
baseURL,
headers: {
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
}
},
{
defaultState: {
errMsgStack: [],
refreshTokenPromise: null
} as RequestInstanceState,
transform(response: AxiosResponse<App.Service.Response<any>>) {
return response.data.data;
export const request = withDedupe(
createFlatRequest(
{
baseURL,
headers: {
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
}
},
async onRequest(config) {
const Authorization = getAuthorization();
Object.assign(config.headers, { Authorization });
applyApiEncrypt(config);
{
defaultState: {
errMsgStack: [],
refreshTokenPromise: null
} as RequestInstanceState,
transform(response: AxiosResponse<App.Service.Response<any>>) {
return response.data.data;
},
async onRequest(config) {
const Authorization = getAuthorization();
Object.assign(config.headers, { Authorization });
applyApiEncrypt(config);
return config;
},
isBackendSuccess(response) {
// 当后端返回码为 "0"(默认)时,表示请求成功
// 如需调整该逻辑,可修改 `.env` 中的 `VITE_SERVICE_SUCCESS_CODE`
return String(response.data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
},
async onBackendFail(response, instance) {
const authStore = useAuthStore();
const responseCode = String(response.data.code);
return config;
},
isBackendSuccess(response) {
// 当后端返回码为 "0"(默认)时,表示请求成功
// 如需调整该逻辑,可修改 `.env` 中的 `VITE_SERVICE_SUCCESS_CODE`
return String(response.data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
},
async onBackendFail(response, instance) {
const authStore = useAuthStore();
const responseCode = String(response.data.code);
function handleLogout() {
authStore.resetStore();
}
function logoutAndCleanup() {
handleLogout();
window.removeEventListener('beforeunload', handleLogout);
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
}
// 当后端返回码命中 `logoutCodes` 时,表示用户需要退出登录并跳转到登录页
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
if (logoutCodes.includes(responseCode)) {
handleLogout();
return null;
}
// 当后端返回码命中 `modalLogoutCodes` 时,表示通过弹窗提示后再退出登录
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
// 防止用户刷新页面绕过退出逻辑
window.addEventListener('beforeunload', handleLogout);
window.$messageBox
?.confirm(response.data.msg, $t('common.error'), {
confirmButtonText: $t('common.confirm'),
cancelButtonText: $t('common.cancel'),
type: 'error',
closeOnClickModal: false,
closeOnPressEscape: false
})
.then(() => {
logoutAndCleanup();
});
return null;
}
// 当后端返回码命中 `expiredTokenCodes` 时,表示 token 已过期,需要刷新 token
// `refreshToken` 接口不能再返回 `expiredTokenCodes` 中的错误码,否则会形成死循环,应返回 `logoutCodes` 或 `modalLogoutCodes`
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
if (expiredTokenCodes.includes(responseCode)) {
const success = await handleExpiredRequest(request.state);
if (success) {
const Authorization = getAuthorization();
Object.assign(response.config.headers, { Authorization });
return instance.request(response.config) as Promise<AxiosResponse>;
function handleLogout() {
authStore.resetStore();
}
function logoutAndCleanup() {
handleLogout();
window.removeEventListener('beforeunload', handleLogout);
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
}
// 当后端返回码命中 `logoutCodes` 时,表示用户需要退出登录并跳转到登录页
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
if (logoutCodes.includes(responseCode)) {
handleLogout();
return null;
}
// 当后端返回码命中 `modalLogoutCodes` 时,表示通过弹窗提示后再退出登录
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
// 防止用户刷新页面绕过退出逻辑
window.addEventListener('beforeunload', handleLogout);
window.$messageBox
?.confirm(response.data.msg, $t('common.error'), {
confirmButtonText: $t('common.confirm'),
cancelButtonText: $t('common.cancel'),
type: 'error',
closeOnClickModal: false,
closeOnPressEscape: false
})
.then(() => {
logoutAndCleanup();
});
return null;
}
// 当后端返回码命中 `expiredTokenCodes` 时,表示 token 已过期,需要刷新 token
// `refreshToken` 接口不能再返回 `expiredTokenCodes` 中的错误码,否则会形成死循环,应返回 `logoutCodes` 或 `modalLogoutCodes`
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
if (expiredTokenCodes.includes(responseCode)) {
const success = await handleExpiredRequest(request.state);
if (success) {
const Authorization = getAuthorization();
Object.assign(response.config.headers, { Authorization });
return instance.request(response.config) as Promise<AxiosResponse>;
}
}
return null;
},
onError(error) {
// 请求失败时,在这里统一处理错误提示
let message = error.message;
let backendErrorCode = '';
// 获取后端错误信息和错误码
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.msg || message;
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)) {
return;
}
showErrorMsg(request.state, message);
}
return null;
},
onError(error) {
// 请求失败时,在这里统一处理错误提示
let message = error.message;
let backendErrorCode = '';
// 获取后端错误信息和错误码
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.msg || message;
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)) {
return;
}
showErrorMsg(request.state, message);
}
}
)
);
export const demoRequest = createRequest(