feat:统一前端请求层错误回显与长整型解析

This commit is contained in:
zhiye.sun
2026-05-21 13:10:01 +08:00
parent 91e6d5bdd3
commit 1ada88c02a
9 changed files with 2602 additions and 1 deletions

1
frontend/.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=https://registry.npmmirror.com/

View File

@@ -14,6 +14,7 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"axios": "^1.13.2", "axios": "^1.13.2",
"element-plus": "^2.11.8", "element-plus": "^2.11.8",
"json-bigint": "^1.0.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"

2478
frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import { parseJsonPreservingLong } from '../json';
describe('parseJsonPreservingLong', () => {
it('should preserve unsafe long ids as strings', () => {
const result = parseJsonPreservingLong<{ id: string; storeCode: string }>(
'{"id":2057302206052372481,"storeCode":"TEXT-1"}',
'application/json',
) as { id: string; storeCode: string };
expect(result.id).toBe('2057302206052372481');
expect(result.storeCode).toBe('TEXT-1');
});
it('should keep safe integers as numbers', () => {
const result = parseJsonPreservingLong<{ total: number }>(
'{"total":12}',
'application/json',
) as { total: number };
expect(result.total).toBe(12);
expect(typeof result.total).toBe('number');
});
it('should skip non json payloads', () => {
const csv = 'id,name\n1,test';
expect(parseJsonPreservingLong(csv, 'text/csv')).toBe(csv);
});
});

View File

@@ -1,9 +1,32 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { request } from '../request'; import { buildErrorDetail, request } from '../request';
describe('request client', () => { describe('request client', () => {
it('uses the backend api prefix', () => { it('uses the backend api prefix', () => {
expect(request.defaults.baseURL).toBe('/api'); expect(request.defaults.baseURL).toBe('/api');
}); });
it('parses oversized numeric ids as strings to avoid precision loss', () => {
const transformResponse = request.defaults.transformResponse;
const transform = Array.isArray(transformResponse) ? transformResponse[0] : transformResponse;
const payload = '{"resultcode":"0","message":null,"data":{"id":2057302206052372481,"storeCode":"TEXT-1"}}';
const parsed = transform
? transform.call({} as never, payload, { 'content-type': 'application/json' } as never)
: JSON.parse(payload);
expect(parsed.data.id).toBe('2057302206052372481');
});
it('formats structured backend errors for dialog display', () => {
const detail = buildErrorDetail({
resultcode: '400',
message: '知识库编码已存在: TEST-1',
data: null,
});
expect(detail).toContain('失败原因:知识库编码已存在: TEST-1');
expect(detail).toContain('错误编码400');
});
}); });

21
frontend/src/api/json.ts Normal file
View File

@@ -0,0 +1,21 @@
import JSONBig from 'json-bigint';
const jsonParser = JSONBig({
storeAsString: true,
useNativeBigInt: false,
});
function looksLikeJsonPayload(data: string) {
const trimmed = data.trim();
return trimmed.startsWith('{') || trimmed.startsWith('[');
}
export function parseJsonPreservingLong<T>(data: unknown, contentType?: string): T | unknown {
if (typeof data !== 'string' || !looksLikeJsonPayload(data)) {
return data;
}
if (contentType && !contentType.toLowerCase().includes('application/json')) {
return data;
}
return jsonParser.parse(data) as T;
}

View File

@@ -1,5 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import { ElMessageBox } from 'element-plus';
import { parseJsonPreservingLong } from './json';
export interface RequestResult<T> { export interface RequestResult<T> {
resultcode: string; resultcode: string;
@@ -10,8 +12,37 @@ export interface RequestResult<T> {
export const request = axios.create({ export const request = axios.create({
baseURL: '/api', baseURL: '/api',
timeout: 15000, timeout: 15000,
transformResponse: [
(data, headers) =>
parseJsonPreservingLong(
data,
typeof headers?.['content-type'] === 'string' ? headers['content-type'] : undefined,
),
],
}); });
export function buildErrorDetail(errorData: RequestResult<unknown>) {
const lines = [
`失败原因:${errorData.message || '未知错误'}`,
`错误编码:${errorData.resultcode || '-'}`,
];
return lines.join('\n');
}
request.interceptors.response.use(
(response) => response,
async (error) => {
const errorData = error?.response?.data as RequestResult<unknown> | undefined;
if (errorData?.resultcode && errorData?.message) {
await ElMessageBox.alert(buildErrorDetail(errorData), '请求失败', {
type: 'error',
confirmButtonText: '知道了',
});
}
return Promise.reject(error);
},
);
export function get<T>(url: string, config?: AxiosRequestConfig) { export function get<T>(url: string, config?: AxiosRequestConfig) {
return request.get<RequestResult<T>>(url, config).then((response) => response.data); return request.get<RequestResult<T>>(url, config).then((response) => response.data);
} }

14
frontend/src/types/json-bigint.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
declare module 'json-bigint' {
interface JsonBigOptions {
storeAsString?: boolean;
useNativeBigInt?: boolean;
alwaysParseAsBig?: boolean;
}
interface JsonBigInstance {
parse<T = unknown>(text: string): T;
stringify(value: unknown): string;
}
export default function JSONBig(options?: JsonBigOptions): JsonBigInstance;
}