feat(i18n): 管理端与玩家端三语支持(中/英/马来语)

- 管理后台 adminT 文案库、结算与代理端页面、表单校验
- 玩家端 vue-i18n 补全首页/公告/串关与 ms 文案
- Element Plus ms 语言包与共享 locale 工具
This commit is contained in:
2026-06-03 15:05:36 +08:00
parent 80adc0e928
commit cbfa18d1d3
63 changed files with 3081 additions and 1038 deletions

View File

@@ -1,6 +1,10 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useAdminLocale } from '../composables/useAdminLocale';
import { resolveFormError } from '../i18n/form-validation';
import api from '../api';
const { t } = useAdminLocale();
import { ElMessage } from 'element-plus';
import {
emptyAgentCreateForm,
@@ -102,18 +106,18 @@ async function submitCreate() {
try {
payload = buildCreateAgentPayload(createForm.value);
} catch (e) {
ElMessage.warning(e instanceof Error ? e.message : '请检查表单');
ElMessage.warning(resolveFormError(e, t));
return;
}
createLoading.value = true;
try {
await api.post('/admin/agents', payload);
ElMessage.success('一级代理已创建');
ElMessage.success(t('msg.agent_created'));
createVisible.value = false;
load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? '创建失败');
ElMessage.error(err.response?.data?.error ?? t('msg.create_failed'));
} finally {
createLoading.value = false;
}
@@ -128,12 +132,12 @@ async function submitEdit() {
email: editForm.value.email.trim() || undefined,
cashbackRate: editForm.value.cashbackRate,
});
ElMessage.success('已保存');
ElMessage.success(t('msg.saved'));
editVisible.value = false;
load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? '保存失败');
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
} finally {
editLoading.value = false;
}
@@ -141,7 +145,7 @@ async function submitEdit() {
async function submitCredit() {
if (creditForm.value.amount === 0) {
ElMessage.warning('调整金额不能为 0');
ElMessage.warning(t('msg.credit_zero'));
return;
}
creditLoading.value = true;
@@ -151,12 +155,12 @@ async function submitCredit() {
requestId: `credit-${editingId.value}-${Date.now()}`,
remark: creditForm.value.remark || undefined,
});
ElMessage.success('授信已调整');
ElMessage.success(t('msg.credit_adjusted'));
creditVisible.value = false;
load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? '调整失败');
ElMessage.error(err.response?.data?.error ?? t('msg.credit_adjust_failed'));
} finally {
creditLoading.value = false;
}
@@ -172,13 +176,15 @@ function statusTagType(s: string) {
}
function statusLabel(s: string) {
return s === 'ACTIVE' ? '正常' : s === 'SUSPENDED' ? '停用' : s;
const key = `user.status.${s}`;
const v = t(key);
return v !== key ? v : s;
}
function creditTypeLabel(t: string) {
if (t === 'CREDIT_INCREASE') return '增加';
if (t === 'CREDIT_DECREASE') return '减少';
return t;
function creditTypeLabel(type: string) {
if (type === 'CREDIT_INCREASE') return t('agent.credit.increase');
if (type === 'CREDIT_DECREASE') return t('agent.credit.decrease');
return type;
}
</script>
@@ -186,25 +192,25 @@ function creditTypeLabel(t: string) {
<div class="admin-list-page">
<div class="page-header">
<div>
<h2 class="page-title">代理管理</h2>
<span class="page-desc">创建一级代理调整授信额度查看直属玩家与额度占用</span>
<h2 class="page-title">{{ t('page.agents.title') }}</h2>
<span class="page-desc">{{ t('page.agents.desc') }}</span>
</div>
<el-button type="primary" @click="openCreate">+ 新建一级代理</el-button>
<el-button type="primary" @click="openCreate">{{ t('agent.create_btn') }}</el-button>
</div>
<el-card class="filter-card" shadow="never">
<el-form inline>
<el-form-item label="关键词">
<el-form-item :label="t('common.keyword')">
<el-input
v-model="keyword"
placeholder="用户名"
:placeholder="t('agent.filter.username_ph')"
clearable
style="width: 180px"
@keyup.enter="load"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button type="primary" @click="load">{{ t('common.search') }}</el-button>
</el-form-item>
</el-form>
</el-card>
@@ -213,37 +219,37 @@ function creditTypeLabel(t: string) {
<div class="table-wrap">
<el-table :data="agents" stripe>
<el-table-column prop="userId" label="ID" width="72" />
<el-table-column prop="username" label="用户名" min-width="120" />
<el-table-column label="状态" width="88">
<el-table-column prop="username" :label="t('user.col.username')" min-width="120" />
<el-table-column :label="t('common.status')" width="88">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ statusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="层级" width="72" align="center" />
<el-table-column label="授信 / 已用 / 可用" min-width="168" align="right">
<el-table-column prop="level" :label="t('agent.col.level')" width="72" align="center" />
<el-table-column :label="t('agent.col.credit')" min-width="168" align="right">
<template #default="{ row }">
<el-tooltip :content="creditLineFull(row)" placement="top">
<span class="amount-compact">{{ creditLine(row) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="directPlayerCount" label="直属玩家" width="96" align="center" />
<el-table-column label="返水率" width="88" align="right">
<el-table-column prop="directPlayerCount" :label="t('agent.col.direct_players')" width="96" align="center" />
<el-table-column :label="t('agent.col.cashback')" width="88" align="right">
<template #default="{ row }">{{ row.cashbackRate }}</template>
</el-table-column>
<el-table-column prop="phone" label="手机" min-width="110">
<el-table-column prop="phone" :label="t('agent.col.phone')" min-width="110">
<template #default="{ row }">{{ row.phone ?? '—' }}</template>
</el-table-column>
<el-table-column label="创建时间" min-width="158">
<el-table-column :label="t('agent.col.created')" min-width="158">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right" align="center">
<el-table-column :label="t('common.actions')" width="240" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openDetail(row.userId)">详情</el-button>
<el-button size="small" link type="primary" @click="openEdit(row.userId)">编辑</el-button>
<el-button size="small" link type="primary" @click="openCredit(row)">调额</el-button>
<el-button size="small" link type="primary" @click="openDetail(row.userId)">{{ t('common.detail') }}</el-button>
<el-button size="small" link type="primary" @click="openEdit(row.userId)">{{ t('common.edit') }}</el-button>
<el-button size="small" link type="primary" @click="openCredit(row)">{{ t('common.adjust_credit') }}</el-button>
</template>
</el-table-column>
</el-table>
@@ -262,27 +268,27 @@ function creditTypeLabel(t: string) {
</div>
</el-card>
<el-dialog v-model="createVisible" title="新建一级代理" width="520px" destroy-on-close>
<el-dialog v-model="createVisible" :title="t('agent.dialog.create')" width="520px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="用户名" required>
<el-input v-model="createForm.username" placeholder="登录用户名,唯一" />
<el-form-item :label="t('user.col.username')" required>
<el-input v-model="createForm.username" :placeholder="t('user.ph.username_unique')" />
</el-form-item>
<el-form-item label="登录密码" required>
<el-form-item :label="t('user.field.password')" required>
<el-input v-model="createForm.password" type="text" autocomplete="off" />
</el-form-item>
<el-form-item label="确认密码" required>
<el-form-item :label="t('user.field.confirm_password')" required>
<el-input v-model="createForm.confirmPassword" type="text" autocomplete="off" />
</el-form-item>
<el-form-item label="授信额度" required>
<el-form-item :label="t('agent.field.credit_limit')" required>
<el-input-number
v-model="createForm.creditLimit"
:min="0"
:step="10000"
style="width: 100%"
/>
<div class="field-hint">代理可向直属玩家上分的总额度上限</div>
<div class="field-hint">{{ t('agent.hint.credit_limit') }}</div>
</el-form-item>
<el-form-item label="返水比例">
<el-form-item :label="t('agent.field.cashback_rate')">
<el-input-number
v-model="createForm.cashbackRate"
:min="0"
@@ -291,30 +297,30 @@ function creditTypeLabel(t: string) {
:precision="4"
style="width: 100%"
/>
<div class="field-hint">例如 0.01 表示 1%</div>
<div class="field-hint">{{ t('agent.hint.cashback_example') }}</div>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="createForm.phone" placeholder="选填" />
<el-form-item :label="t('user.field.phone')">
<el-input v-model="createForm.phone" :placeholder="t('common.optional')" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="createForm.email" placeholder="选填" />
<el-form-item :label="t('user.field.email')">
<el-input v-model="createForm.email" :placeholder="t('common.optional')" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
<el-button @click="createVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" :loading="createLoading" @click="submitCreate">{{ t('user.btn.create') }}</el-button>
</template>
</el-dialog>
<el-dialog v-model="editVisible" title="编辑代理" width="480px" destroy-on-close>
<el-dialog v-model="editVisible" :title="t('agent.dialog.edit')" width="480px" destroy-on-close>
<el-form label-width="88px">
<el-form-item label="账号状态">
<el-form-item :label="t('user.field.account_status')">
<el-radio-group v-model="editForm.status">
<el-radio value="ACTIVE">正常</el-radio>
<el-radio value="SUSPENDED">停用</el-radio>
<el-radio value="ACTIVE">{{ t('user.status.ACTIVE') }}</el-radio>
<el-radio value="SUSPENDED">{{ t('user.status.SUSPENDED') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="返水比例">
<el-form-item :label="t('agent.field.cashback_rate')">
<el-input-number
v-model="editForm.cashbackRate"
:min="0"
@@ -324,111 +330,111 @@ function creditTypeLabel(t: string) {
style="width: 100%"
/>
</el-form-item>
<el-form-item label="手机号">
<el-form-item :label="t('user.field.phone')">
<el-input v-model="editForm.phone" />
</el-form-item>
<el-form-item label="邮箱">
<el-form-item :label="t('user.field.email')">
<el-input v-model="editForm.email" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" :loading="editLoading" @click="submitEdit">保存</el-button>
<el-button @click="editVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" :loading="editLoading" @click="submitEdit">{{ t('common.save') }}</el-button>
</template>
</el-dialog>
<el-dialog v-model="creditVisible" title="调整授信额度" width="420px" destroy-on-close>
<el-dialog v-model="creditVisible" :title="t('agent.dialog.credit')" width="420px" destroy-on-close>
<el-form label-width="88px">
<el-form-item label="代理 ID">
<el-form-item :label="t('agent.field.agent_id')">
<el-input :model-value="editingId" disabled />
</el-form-item>
<el-form-item label="调整金额">
<el-form-item :label="t('agent.field.adjust_amount')">
<el-input-number v-model="creditForm.amount" :step="1000" style="width: 100%" />
<div class="field-hint">正数为增加授信负数为减少</div>
<div class="field-hint">{{ t('agent.hint.credit_adjust') }}</div>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="creditForm.remark" placeholder="选填,写入额度流水" />
<el-form-item :label="t('user.field.remark')">
<el-input v-model="creditForm.remark" :placeholder="t('agent.hint.credit_remark')" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="creditVisible = false">取消</el-button>
<el-button type="primary" :loading="creditLoading" @click="submitCredit">确认调整</el-button>
<el-button @click="creditVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" :loading="creditLoading" @click="submitCredit">{{ t('agent.btn.confirm_adjust') }}</el-button>
</template>
</el-dialog>
<el-dialog v-model="detailVisible" title="代理详情" width="640px" destroy-on-close>
<el-dialog v-model="detailVisible" :title="t('agent.dialog.detail')" width="640px" destroy-on-close>
<template v-if="detail">
<el-descriptions :column="2" border size="small" class="detail-block">
<el-descriptions-item label="ID">{{ detail.userId }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ detail.username }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-descriptions-item :label="t('common.col_id')">{{ detail.userId }}</el-descriptions-item>
<el-descriptions-item :label="t('user.col.username')">{{ detail.username }}</el-descriptions-item>
<el-descriptions-item :label="t('common.status')">
<el-tag :type="statusTagType(detail.status)" size="small">
{{ statusLabel(detail.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="层级">L{{ detail.level }}</el-descriptions-item>
<el-descriptions-item label="授信额度">
<el-descriptions-item :label="t('agent.col.level')">L{{ detail.level }}</el-descriptions-item>
<el-descriptions-item :label="t('agent.field.credit_limit')">
{{ formatAmount(detail.creditLimit) }}
<span v-if="shouldCompact(detail.creditLimit)" class="amount-full-hint">
{{ formatAmountFull(detail.creditLimit) }}
</span>
</el-descriptions-item>
<el-descriptions-item label="已用额度">
<el-descriptions-item :label="t('agent.field.used_credit')">
{{ formatAmount(detail.usedCredit) }}
<span v-if="shouldCompact(detail.usedCredit)" class="amount-full-hint">
{{ formatAmountFull(detail.usedCredit) }}
</span>
</el-descriptions-item>
<el-descriptions-item label="可用授信">
<el-descriptions-item :label="t('agent.field.available_credit')">
{{ formatAmount(detail.availableCredit) }}
<span v-if="shouldCompact(detail.availableCredit)" class="amount-full-hint">
{{ formatAmountFull(detail.availableCredit) }}
</span>
</el-descriptions-item>
<el-descriptions-item label="直属玩家">{{ detail.directPlayerCount }} </el-descriptions-item>
<el-descriptions-item label="玩家负债">
<el-descriptions-item :label="t('agent.col.direct_players')">{{ detail.directPlayerCount }} {{ t('common.people') }}</el-descriptions-item>
<el-descriptions-item :label="t('agent.field.player_liability')">
{{ formatAmount(detail.directPlayerLiability) }}
</el-descriptions-item>
<el-descriptions-item label="下级代理敞口">
<el-descriptions-item :label="t('agent.field.sub_agent_exposure')">
{{ formatAmount(detail.childAgentExposure) }}
</el-descriptions-item>
<el-descriptions-item label="返水率">{{ detail.cashbackRate }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ detail.phone ?? '—' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ detail.email ?? '—' }}</el-descriptions-item>
<el-descriptions-item label="最后登录" :span="2">
{{ detail.lastLoginAt ? formatTime(detail.lastLoginAt) : '从未登录' }}
<el-descriptions-item :label="t('agent.col.cashback')">{{ detail.cashbackRate }}</el-descriptions-item>
<el-descriptions-item :label="t('user.field.phone')">{{ detail.phone ?? '—' }}</el-descriptions-item>
<el-descriptions-item :label="t('user.field.email')">{{ detail.email ?? '—' }}</el-descriptions-item>
<el-descriptions-item :label="t('user.col.last_login')" :span="2">
{{ detail.lastLoginAt ? formatTime(detail.lastLoginAt) : t('common.never_login') }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
<el-descriptions-item :label="t('agent.col.created')" :span="2">
{{ formatTime(detail.createdAt) }}
</el-descriptions-item>
</el-descriptions>
<div class="section-title">最近额度变动</div>
<div class="section-title">{{ t('agent.section.credit_log') }}</div>
<el-table
:data="detail.recentCreditTransactions"
size="small"
stripe
empty-text="暂无记录"
:empty-text="t('agent.col.no_records')"
>
<el-table-column label="类型" width="80">
<el-table-column :label="t('agent.col.credit_type')" width="80">
<template #default="{ row }">{{ creditTypeLabel(row.transactionType) }}</template>
</el-table-column>
<el-table-column label="变动" width="96" align="right">
<el-table-column :label="t('agent.col.credit_change')" width="96" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.amount)" placement="top">
<span>{{ formatAmount(row.amount) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="变动后" width="96" align="right">
<el-table-column :label="t('agent.col.credit_after')" width="96" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.creditAfter)" placement="top">
<span>{{ formatAmount(row.creditAfter) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="时间" min-width="150">
<el-table-column prop="remark" :label="t('user.field.remark')" min-width="120" show-overflow-tooltip />
<el-table-column :label="t('audit.col.time')" min-width="150">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
</el-table>