袁聪的前端调修

This commit is contained in:
2026-09-14 01:07:43 +08:00
parent 680c2a0749
commit 7a49a1c2c8
23 changed files with 2150 additions and 51 deletions
+69 -3
View File
@@ -55,7 +55,33 @@ const ENDPOINTS = Object.freeze({
ADVISOR_GOAL: { method: 'GET', path: '/api/v1/advisor/investment-goals/current' },
ADVISOR_ANALYSIS: { method: 'POST', path: '/api/v1/advisor/portfolio-analysis' },
OFFSITE_MAILS: { method: 'GET', path: '/api/v1/offsite-fund/mails' },
OFFSITE_MAIL: { method: 'GET', path: '/api/v1/offsite-fund/mails/{mailId}' },
OFFSITE_MAIL_DELETE: { method: 'POST', path: '/api/v1/offsite-fund/mails/{mailId}/deletions', idempotent: true },
OFFSITE_RECOGNITION: { method: 'GET', path: '/api/v1/offsite-fund/mails/{mailId}/recognition-fields' },
OFFSITE_RECOGNITION_SAVE: { method: 'PUT', path: '/api/v1/offsite-fund/mails/{mailId}/recognition-fields', idempotent: true },
OFFSITE_NL2SQL_FIELDS: { method: 'GET', path: '/api/v1/offsite-fund/documents/{taskId}/nl2sql-fields' },
OFFSITE_NL2SQL_FIELDS_SAVE: { method: 'PUT', path: '/api/v1/offsite-fund/documents/{taskId}/nl2sql-fields', idempotent: true },
OFFSITE_RULE_RESULTS: { method: 'GET', path: '/api/v1/offsite-fund/documents/{taskId}/rule-results' },
OFFSITE_RULE_RECALCULATE: { method: 'POST', path: '/api/v1/offsite-fund/documents/{taskId}/rule-results/recalculations', idempotent: true },
OFFSITE_MAILBOX: { method: 'GET', path: '/api/v1/offsite-fund/mailbox-status' },
OFFSITE_MAILBOX_RECOVER: { method: 'POST', path: '/api/v1/offsite-fund/mailbox-status/recoveries', idempotent: true },
OFFSITE_ATTACHMENT_FILE: { method: 'GET', path: '/api/v1/offsite-fund/attachments/{attachmentId}/file' },
OFFSITE_CONFIRM: { method: 'POST', path: '/api/v1/offsite-fund/documents/{taskId}/confirmations', idempotent: true },
OFFSITE_RECOGNITION_RETRY: { method: 'POST', path: '/api/v1/offsite-fund/documents/{taskId}/recognition-retries', idempotent: true },
OFFSITE_NOTIFICATION_CREATE: { method: 'POST', path: '/api/v1/offsite-fund/documents/{taskId}/notifications', idempotent: true },
OFFSITE_NOTIFICATION_SEND: { method: 'POST', path: '/api/v1/offsite-fund/notifications/{notificationId}/send', idempotent: true },
OFFSITE_SETTLEMENT_RECALCULATE: { method: 'POST', path: '/api/v1/offsite-fund/settlement-statistics/recalculate', idempotent: true },
OFFSITE_TRIGGER_NL2SQL: { method: 'POST', path: '/api/tasks/{taskId}/trigger-agent-nl2sql', idempotent: true },
PROMOTION_CREATE: { method: 'POST', path: '/api/v1/fund-promotion-materials', idempotent: true },
PROMOTION_TASK: { method: 'GET', path: '/api/v1/fund-promotion-materials/{taskNo}' },
PROMOTION_INPUTS: { method: 'PUT', path: '/api/v1/fund-promotion-materials/{taskNo}/inputs', idempotent: true },
PROMOTION_ATTACHMENT: { method: 'POST', path: '/api/v1/fund-promotion-materials/{taskNo}/attachments', formData: true, idempotent: true },
PROMOTION_GENERATE: { method: 'POST', path: '/api/v1/fund-promotion-materials/{taskNo}/generations', idempotent: true },
PROMOTION_CHECKS: { method: 'GET', path: '/api/v1/fund-promotion-materials/{taskNo}/compliance-checks' },
PROMOTION_REVIEW: { method: 'POST', path: '/api/v1/fund-promotion-materials/{taskNo}/reviews', idempotent: true },
PROMOTION_DELIVERY: { method: 'POST', path: '/api/v1/fund-promotion-materials/{taskNo}/deliveries', idempotent: true },
AGENT_RUN_CREATE: { method: 'POST', path: '/api/v1/agent-runs', idempotent: true },
AGENT_RUN: { method: 'GET', path: '/api/v1/agent-runs/{runId}' },
});
export class ApiError extends Error {
@@ -67,6 +93,7 @@ export class ApiError extends Error {
this.retryable = Boolean(options.retryable);
this.fieldErrors = options.fieldErrors || [];
this.traceId = options.traceId || '';
this.payload = options.payload || null;
}
}
@@ -114,13 +141,15 @@ async function request(endpointId, options = {}) {
body: options.body === undefined || endpoint.method === 'GET'
? undefined
: (endpoint.formData ? options.body : JSON.stringify(options.body)),
cache: 'no-store',
signal: controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (response.status === 401 && endpoint.auth !== false) clearAuthSession();
const responseTraceId = payload.meta?.trace_id || response.headers.get('X-Trace-ID') || traceId;
document.documentElement.dataset.traceId = responseTraceId;
if (!response.ok || payload.error) {
const hasBusinessError = Object.prototype.hasOwnProperty.call(payload, 'code') && payload.code !== 0;
if (!response.ok || payload.error || hasBusinessError) {
const detail = payload.error || {};
const validationDetail = Array.isArray(payload.detail)
? payload.detail
@@ -129,14 +158,19 @@ async function request(endpointId, options = {}) {
.join(';')
: (typeof payload.detail === 'string' ? payload.detail : '');
const message = detail.message
|| (hasBusinessError ? payload.message : '')
|| validationDetail
|| (response.status ? `请求失败(HTTP ${response.status})` : '请求未完成');
const businessStatus = hasBusinessError && Number(payload.code) >= 400
? Number(payload.code)
: response.status;
const error = new ApiError(message, {
code: detail.code,
status: response.status,
code: detail.code || (hasBusinessError ? `BUSINESS_${payload.code}` : undefined),
status: businessStatus,
retryable: detail.retryable,
fieldErrors: detail.field_errors,
traceId: responseTraceId,
payload,
});
if (response.status === 429 && attempt === 0) await wait(5000);
else if (shouldRetry(error, attempt)) await wait(2000);
@@ -213,10 +247,42 @@ async function stream(endpointId, body, options = {}) {
if (buffer.trim()) dispatch(buffer);
}
async function requestFile(endpointId, options = {}) {
const endpoint = ENDPOINTS[endpointId];
if (!endpoint) throw new ApiError(`未注册端点 ${endpointId}`, { code: 'ENDPOINT_NOT_REGISTERED' });
const traceId = crypto.randomUUID();
const headers = { Accept: '*/*', 'X-Trace-ID': traceId, ...(options.headers || {}) };
const token = getAccessToken();
if (endpoint.auth !== false && token) headers.Authorization = `Bearer ${token}`;
const response = await fetch(`${pathFor(endpoint, options.pathParams)}${new URLSearchParams(options.query || {}).toString() ? `?${new URLSearchParams(options.query).toString()}` : ''}`, {
method: endpoint.method,
headers,
cache: 'no-store',
signal: options.signal,
});
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
const detail = payload.error || {};
if (response.status === 401 && endpoint.auth !== false) clearAuthSession();
throw new ApiError(detail.message || `文件请求失败(HTTP ${response.status})`, {
code: detail.code,
status: response.status,
traceId: payload.meta?.trace_id || response.headers.get('X-Trace-ID') || traceId,
});
}
return {
blob: await response.blob(),
filename: response.headers.get('Content-Disposition') || '',
contentType: response.headers.get('Content-Type') || '',
traceId: response.headers.get('X-Trace-ID') || traceId,
};
}
export const apiClient = Object.freeze({
get(endpointId, options = {}) { return request(endpointId, options); },
post(endpointId, body, options = {}) { return request(endpointId, { ...options, body }); },
upload(endpointId, formData, options = {}) { return request(endpointId, { ...options, body: formData, timeout: options.timeout || 30000 }); },
file(endpointId, options = {}) { return requestFile(endpointId, options); },
stream,
reportError(error) {
window.dispatchEvent(new CustomEvent('portal:error', { detail: { message: error.message, traceId: error.traceId || '' } }));