feat:统一前端请求层错误回显与长整型解析
This commit is contained in:
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
registry=https://registry.npmmirror.com/
|
||||||
@@ -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
2478
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
frontend/pnpm-workspace.yaml
Normal file
2
frontend/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
30
frontend/src/api/__tests__/json.spec.ts
Normal file
30
frontend/src/api/__tests__/json.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
21
frontend/src/api/json.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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
14
frontend/src/types/json-bigint.d.ts
vendored
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user