From b6cc12cefed985690a5d08996464608004bf39cd Mon Sep 17 00:00:00 2001 From: liangxu Date: Tue, 12 May 2026 01:31:02 +0800 Subject: [PATCH] feat(admin-web): show centered modal in Chinese when AI points are insufficient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `ai_points_insufficient` / `ai_points_unavailable` Chinese mappings in errors.ts so the toast copy is localized. - Add `showAiPointsInsufficientModal()` helper that renders a centered `Modal.confirm` with localized title/body and a "查看点数明细" action that routes to `/account/ai-points`. The helper is debounced so concurrent failed requests do not stack multiple modals. - Trigger the modal globally from the admin-web response interceptor when the backend returns `ai_points_insufficient`, so every AI-generating endpoint surfaces the same prominent prompt without per-callsite changes. - Drop the English `detail` for ai_points_insufficient in `formatError` so the secondary toast stays pure Chinese. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/admin-web/src/lib/api.ts | 6 ++++++ apps/admin-web/src/lib/errors.ts | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/apps/admin-web/src/lib/api.ts b/apps/admin-web/src/lib/api.ts index 5e7eeb1..7cb8ddb 100644 --- a/apps/admin-web/src/lib/api.ts +++ b/apps/admin-web/src/lib/api.ts @@ -315,6 +315,12 @@ apiClient.raw.interceptors.response.use( markStoredMembershipBlocked(error.message) } + if (error instanceof ApiClientError && error.message === 'ai_points_insufficient') { + void import('./errors').then(({ showAiPointsInsufficientModal }) => { + showAiPointsInsufficientModal() + }) + } + return Promise.reject(error) }, ) diff --git a/apps/admin-web/src/lib/errors.ts b/apps/admin-web/src/lib/errors.ts index 3601758..9020080 100644 --- a/apps/admin-web/src/lib/errors.ts +++ b/apps/admin-web/src/lib/errors.ts @@ -1,5 +1,8 @@ +import { Modal } from 'ant-design-vue' import { ApiClientError } from '@geo/http-client' +import { router } from '@/router' + const authSessionErrorMessages = new Set([ 'not_authenticated', 'missing_bearer_token', @@ -28,6 +31,8 @@ const errorMessageMap: Record = { tenant_scope_missing: '租户上下文缺失', query_failed: '数据读取失败', quota_insufficient: '生成额度不足', + ai_points_insufficient: 'AI 点数余额不足', + ai_points_unavailable: 'AI 点数服务暂不可用,请稍后重试', trial_plan_expired: '免费试用已到期,请联系管理员开通', subscription_required: '当前租户尚未开通有效套餐,请联系管理员', subscription_inactive: '当前租户套餐已失效,请联系管理员', @@ -89,6 +94,34 @@ const errorMessageMap: Record = { unknown_error: '发生未知错误', } +export function isAiPointsInsufficient(error: unknown): boolean { + return error instanceof ApiClientError && error.message === 'ai_points_insufficient' +} + +let aiPointsModalVisible = false + +export function showAiPointsInsufficientModal(): void { + if (aiPointsModalVisible) { + return + } + aiPointsModalVisible = true + + Modal.confirm({ + title: 'AI 点数不足', + content: '当前账号的 AI 点数余额已耗尽,无法继续生成内容。请升级套餐或等待下月点数重置。', + okText: '查看点数明细', + cancelText: '我知道了', + centered: true, + okButtonProps: { type: 'primary' }, + onOk: () => { + void router.push('/account/ai-points') + }, + afterClose: () => { + aiPointsModalVisible = false + }, + }) +} + export function isHandledAuthError(error: unknown): boolean { return ( error instanceof ApiClientError && @@ -104,6 +137,9 @@ export function formatError(error: unknown): string { if (error instanceof ApiClientError) { const message = errorMessageMap[error.message] ?? error.message.replaceAll('_', ' ') + if (isAiPointsInsufficient(error)) { + return message + } return error.detail ? `${message}: ${error.detail}` : message }