diff --git a/apps/admin/src/composables/useAdminPlayerTransfer.ts b/apps/admin/src/composables/useAdminPlayerTransfer.ts index dfeb908..d4cd969 100644 --- a/apps/admin/src/composables/useAdminPlayerTransfer.ts +++ b/apps/admin/src/composables/useAdminPlayerTransfer.ts @@ -2,6 +2,7 @@ import { ref, computed } from 'vue'; import { ElMessage } from 'element-plus'; import api from '../api'; import { useAdminLocale } from './useAdminLocale'; +import { resolveApiError } from '../i18n/form-validation'; import { depositAmountCap, parsePlayerAvailable, @@ -36,6 +37,20 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise) { const transferAmountDisabled = computed(() => transferAmountRange.value.max === 0); + /** 有上限时超出额度(勿用 el-input-number :max,否则会静默截断到上限) */ + const transferAmountExceedsCap = computed(() => { + const max = transferAmountRange.value.max; + if (max === undefined) return false; + return transferAmount.value > max; + }); + + const transferAmountCapError = computed(() => { + if (!transferAmountExceedsCap.value) return ''; + return transferType.value === 'deposit' + ? t('err.insufficient_credit') + : t('transfer.context.withdraw_exceed'); + }); + function transferTitle() { const name = transferTarget.value?.username ?? transferTarget.value?.id ?? ''; return transferType.value === 'deposit' @@ -63,8 +78,7 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise) { transferAmount.value = cap > 0 ? Math.min(100, cap) : 0; } } catch (e: unknown) { - const err = e as { response?: { data?: { error?: string } } }; - ElMessage.error(err.response?.data?.error ?? t('msg.load_failed')); + ElMessage.error(resolveApiError(e, t, 'msg.load_failed')); transferVisible.value = false; } finally { transferContextLoading.value = false; @@ -105,10 +119,7 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise) { transferVisible.value = false; await onSuccess?.(); } catch (e: unknown) { - const err = e as { response?: { data?: { error?: string } } }; - const fallback = - transferType.value === 'deposit' ? t('msg.topup_failed') : t('msg.transfer_failed'); - ElMessage.error(err.response?.data?.error ?? fallback); + ElMessage.error(resolveApiError(e, t, transferType.value === 'deposit' ? 'msg.topup_failed' : 'msg.transfer_failed')); } finally { transferLoading.value = false; } @@ -125,6 +136,8 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise) { transferContextLoading, transferAmountRange, transferAmountDisabled, + transferAmountExceedsCap, + transferAmountCapError, transferTitle, openTransfer, submitTransfer, diff --git a/apps/admin/src/i18n/form-validation.ts b/apps/admin/src/i18n/form-validation.ts index 4a14010..780a89a 100644 --- a/apps/admin/src/i18n/form-validation.ts +++ b/apps/admin/src/i18n/form-validation.ts @@ -15,7 +15,18 @@ export function resolveFormError(e: unknown, t: (key: string) => string): string return t('msg.form_invalid'); } -/** 从 API 错误响应提取可读文案(Nest 全局过滤器返回 `error` 字段) */ +/** Nest 默认 4xx 响应里 error 常为 "Bad Request" 等泛化文案,真实原因在 message */ +const GENERIC_HTTP_ERRORS = new Set([ + 'Bad Request', + 'Unauthorized', + 'Forbidden', + 'Not Found', + 'Conflict', + 'Unprocessable Entity', + 'Internal Server Error', +]); + +/** 从 API 错误响应提取可读文案(兼容 GlobalExceptionFilter 与 Nest 默认格式) */ export function resolveApiError( err: unknown, t: (key: string) => string, @@ -23,8 +34,13 @@ export function resolveApiError( ): string { const data = (err as { response?: { data?: { error?: string | string[]; message?: string | string[] } } }) ?.response?.data; - const raw = data?.error ?? data?.message; - if (Array.isArray(raw)) return raw.join(';'); - if (typeof raw === 'string' && raw.trim()) return raw; + const msgRaw = data?.message; + const errRaw = data?.error; + const message = Array.isArray(msgRaw) ? msgRaw.join(';') : msgRaw; + const error = Array.isArray(errRaw) ? errRaw.join(';') : errRaw; + if (typeof message === 'string' && message.trim()) { + if (!error || GENERIC_HTTP_ERRORS.has(error)) return message; + } + if (typeof error === 'string' && error.trim() && !GENERIC_HTTP_ERRORS.has(error)) return error; return t(fallbackKey); } diff --git a/apps/admin/src/views/AgentManager.vue b/apps/admin/src/views/AgentManager.vue index f003d0a..bae5764 100644 --- a/apps/admin/src/views/AgentManager.vue +++ b/apps/admin/src/views/AgentManager.vue @@ -3,7 +3,7 @@ import { ref, onMounted, computed } from 'vue'; import { useRouter } from 'vue-router'; import { ElMessage, ElMessageBox } from 'element-plus'; import { useAdminLocale } from '../composables/useAdminLocale'; -import { resolveFormError } from '../i18n/form-validation'; +import { resolveFormError, resolveApiError } from '../i18n/form-validation'; import api from '../api'; import { clearStaffSession } from '../stores/auth'; @@ -43,6 +43,7 @@ import AgentCreditContext from '../components/AgentCreditContext.vue'; import { useAdminPlayerTransfer } from '../composables/useAdminPlayerTransfer'; import { fetchAdminAgentCreditContext, + maxCreditIncreaseAmount, type AgentCreditAdjustContext, } from '../utils/agent-credit-context'; @@ -571,8 +572,7 @@ async function openCredit(userId: string) { try { creditContext.value = await fetchAdminAgentCreditContext(userId); } catch (e: unknown) { - const err = e as { response?: { data?: { error?: string } } }; - ElMessage.error(err.response?.data?.error ?? t('msg.load_failed')); + ElMessage.error(resolveApiError(e, t, 'msg.load_failed')); creditVisible.value = false; } finally { creditContextLoading.value = false; @@ -584,6 +584,11 @@ async function submitCredit() { ElMessage.warning(t('msg.credit_zero')); return; } + const maxInc = maxCreditIncreaseAmount(creditContext.value); + if (creditForm.value.amount > 0 && maxInc !== undefined && creditForm.value.amount > maxInc) { + ElMessage.warning(t('err.insufficient_credit')); + return; + } creditLoading.value = true; try { await api.post(`/admin/agents/${editingId.value}/credit`, { @@ -596,8 +601,7 @@ async function submitCredit() { load(); refreshExpandedParents(); } catch (e: unknown) { - const err = e as { response?: { data?: { error?: string } } }; - ElMessage.error(err.response?.data?.error ?? t('msg.credit_adjust_failed')); + ElMessage.error(resolveApiError(e, t, 'msg.credit_adjust_failed')); } finally { creditLoading.value = false; } @@ -737,6 +741,8 @@ const { transferContextLoading, transferAmountRange, transferAmountDisabled, + transferAmountExceedsCap, + transferAmountCapError, transferTitle, openTransfer, submitTransfer, @@ -1340,11 +1346,10 @@ function creditTypeLabel(type: string) { {{ transferTarget?.id }} - + {{ t('common.confirm') }} diff --git a/apps/admin/src/views/Agents.vue b/apps/admin/src/views/Agents.vue index 31503f5..27d1367 100644 --- a/apps/admin/src/views/Agents.vue +++ b/apps/admin/src/views/Agents.vue @@ -1,7 +1,7 @@