feat(admin): 从已有玩家升级代理、修复 i18n 与过期 .js 冲突

- 新建一级代理改为选择已有玩家;新建用户可选一级代理

- 操作日志/注单等扁平 key 翻译;清理 src 内误生成 .js,Vite 优先解析 .ts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 15:42:15 +08:00
parent cbfa18d1d3
commit 3b739982a1
27 changed files with 625 additions and 165 deletions

View File

@@ -7,11 +7,10 @@ export type AdminLocale = 'zh-CN' | 'en-US' | 'ms-MY';
export const ADMIN_LOCALES: {
code: AdminLocale;
label: string;
flag: string;
}[] = [
{ code: 'zh-CN', label: '中文', flag: '🇨🇳' },
{ code: 'en-US', label: 'English', flag: '🇺🇸' },
{ code: 'ms-MY', label: 'Bahasa Melayu', flag: '🇲🇾' },
{ code: 'zh-CN', label: '中文' },
{ code: 'en-US', label: 'English' },
{ code: 'ms-MY', label: 'Bahasa Melayu' },
];
const zh: Record<string, string> = {
@@ -458,32 +457,9 @@ const ms: Record<string, string> = {
...adminPagesMs,
};
const messages: Record<AdminLocale, Record<string, string>> = {
/** vue-i18n 文案表(扁平 key与原先 adminT 一致) */
export const adminMessages: Record<AdminLocale, Record<string, string>> = {
'zh-CN': zh,
'en-US': en,
'ms-MY': ms,
};
export function adminT(locale: AdminLocale, key: string, params?: Record<string, string | number>): string {
const chain = [locale, 'en-US', 'zh-CN'];
const seen = new Set<string>();
let raw = key;
for (const loc of chain) {
if (seen.has(loc)) continue;
seen.add(loc);
const v = messages[loc as AdminLocale]?.[key];
if (v) {
raw = v;
break;
}
}
if (!params) return raw;
return Object.entries(params).reduce(
(s, [k, v]) => s.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)),
raw,
);
}
export function adminLocaleTag(locale: AdminLocale): string {
return locale;
}

View File

@@ -62,6 +62,10 @@ export const adminPagesMs: Record<string, string> = {
'user.btn.save_profile': 'Simpan',
'user.btn.confirm_deposit': 'Sahkan tambah baki',
'user.deposit_remark_default': 'Tambah baki admin',
'user.field.account_type': 'Jenis akaun',
'user.type.player': 'Pemain',
'user.type.tier1_agent': 'Ejen peringkat 1',
'user.hint.account_type': 'Ejen guna had kredit; pemain boleh di bawah ejen',
'agent.create_btn': '+ Ejen peringkat 1 baharu',
'agent.filter.username_ph': 'Nama pengguna',
@@ -95,6 +99,9 @@ export const adminPagesMs: Record<string, string> = {
'agent.col.credit_after': 'Selepas',
'agent.col.no_records': 'Tiada rekod',
'agent.btn.confirm_adjust': 'Sahkan',
'agent.field.select_user': 'Pilih pengguna',
'agent.ph.select_user': 'Cari nama pengguna pemain',
'agent.hint.select_user': 'Pilih akaun pemain sedia ada untuk naik taraf ke ejen peringkat 1',
'match.create_btn': '+ Perlawanan baharu',
'match.filter.keyword_ph': 'Nama perlawanan / kod pasukan',
@@ -123,6 +130,7 @@ export const adminPagesMs: Record<string, string> = {
'bet.col.player': 'Pemain',
'bet.col.agent': 'Ejen',
'bet.col.selection': 'Pilihan',
'bet.col.selection_count': 'Bil. pilihan',
'bet.col.stake': 'Stake',
'bet.col.odds': 'Odds',
'bet.col.payout': 'Bayaran',
@@ -147,6 +155,12 @@ export const adminPagesMs: Record<string, string> = {
'audit.col.module': 'Modul',
'audit.col.target_id': 'ID sasaran',
'audit.col.time': 'Masa',
'audit.action.CREATE_PLAYER': 'Cipta pemain',
'audit.action.UPDATE_PLAYER': 'Kemas kini pemain',
'audit.action.CREATE_AGENT': 'Cipta ejen',
'audit.action.UPDATE_AGENT': 'Kemas kini ejen',
'audit.module.USERS': 'Pemain',
'audit.module.AGENTS': 'Ejen',
'cashback.start_date': 'Tarikh mula',
'cashback.end_date': 'Tarikh tamat',
@@ -184,6 +198,9 @@ export const adminPagesMs: Record<string, string> = {
'err.kickoff_required': 'Sila isi masa mula',
'err.teams_required': 'Isi nama pasukan tuan rumah dan pelawat (ZH atau EN)',
'err.league_required': 'Sila isi nama liga',
'err.user_required': 'Sila pilih pengguna',
'err.agent_no_parent': 'Ejen peringkat 1 tidak boleh ada pemain induk',
'err.agent_no_initial_deposit': 'Jangan isi baki permulaan pemain apabila cipta ejen',
'settlement.ht_score': 'Skor separuh masa',
'settlement.ft_score': 'Skor penuh masa',

View File

@@ -62,6 +62,10 @@ export const adminPagesZh: Record<string, string> = {
'user.btn.save_profile': '保存资料',
'user.btn.confirm_deposit': '确认上分',
'user.deposit_remark_default': '管理员上分',
'user.field.account_type': '账号类型',
'user.type.player': '玩家',
'user.type.tier1_agent': '一级代理',
'user.hint.account_type': '代理使用授信额度;玩家可挂靠代理并上分',
'agent.create_btn': '+ 新建一级代理',
'agent.filter.username_ph': '用户名',
@@ -95,6 +99,9 @@ export const adminPagesZh: Record<string, string> = {
'agent.col.credit_after': '变动后',
'agent.col.no_records': '暂无记录',
'agent.btn.confirm_adjust': '确认调整',
'agent.field.select_user': '选择用户',
'agent.ph.select_user': '搜索玩家用户名',
'agent.hint.select_user': '从已有玩家账号中选择,将其设为一级代理(不新建登录账号)',
'match.create_btn': '+ 新增赛事',
'match.filter.keyword_ph': '赛事名 / 球队代码',
@@ -123,6 +130,7 @@ export const adminPagesZh: Record<string, string> = {
'bet.col.player': '玩家',
'bet.col.agent': '所属代理',
'bet.col.selection': '选项',
'bet.col.selection_count': '投注项数',
'bet.col.stake': '投注额',
'bet.col.odds': '赔率',
'bet.col.payout': '派彩',
@@ -147,6 +155,12 @@ export const adminPagesZh: Record<string, string> = {
'audit.col.module': '模块',
'audit.col.target_id': '目标 ID',
'audit.col.time': '时间',
'audit.action.CREATE_PLAYER': '新建玩家',
'audit.action.UPDATE_PLAYER': '更新玩家',
'audit.action.CREATE_AGENT': '新建代理',
'audit.action.UPDATE_AGENT': '更新代理',
'audit.module.USERS': '玩家',
'audit.module.AGENTS': '代理',
'cashback.start_date': '开始日期',
'cashback.end_date': '结束日期',
@@ -184,6 +198,9 @@ export const adminPagesZh: Record<string, string> = {
'err.kickoff_required': '请填写开赛时间',
'err.teams_required': '请填写主客队名称(中文或英文至少一项)',
'err.league_required': '请填写联赛名称',
'err.user_required': '请选择用户',
'err.agent_no_parent': '一级代理不可设置上级玩家',
'err.agent_no_initial_deposit': '设为代理时请勿填写玩家初始余额',
'settlement.ht_score': '半场比分',
'settlement.ft_score': '全场比分',
@@ -301,6 +318,10 @@ export const adminPagesEn: Record<string, string> = {
'user.btn.save_profile': 'Save',
'user.btn.confirm_deposit': 'Confirm top-up',
'user.deposit_remark_default': 'Admin top-up',
'user.field.account_type': 'Account type',
'user.type.player': 'Player',
'user.type.tier1_agent': 'Tier-1 agent',
'user.hint.account_type': 'Agents use credit limits; players can belong to an agent and receive top-ups',
'agent.create_btn': '+ New tier-1 agent',
'agent.filter.username_ph': 'Username',
@@ -334,6 +355,9 @@ export const adminPagesEn: Record<string, string> = {
'agent.col.credit_after': 'After',
'agent.col.no_records': 'No records',
'agent.btn.confirm_adjust': 'Confirm',
'agent.field.select_user': 'Select user',
'agent.ph.select_user': 'Search player username',
'agent.hint.select_user': 'Pick an existing player account to promote to tier-1 agent (no new login)',
'match.create_btn': '+ New match',
'match.filter.keyword_ph': 'Match name / team code',
@@ -362,6 +386,7 @@ export const adminPagesEn: Record<string, string> = {
'bet.col.player': 'Player',
'bet.col.agent': 'Agent',
'bet.col.selection': 'Pick',
'bet.col.selection_count': 'Legs',
'bet.col.stake': 'Stake',
'bet.col.odds': 'Odds',
'bet.col.payout': 'Payout',
@@ -386,6 +411,12 @@ export const adminPagesEn: Record<string, string> = {
'audit.col.module': 'Module',
'audit.col.target_id': 'Target ID',
'audit.col.time': 'Time',
'audit.action.CREATE_PLAYER': 'Create player',
'audit.action.UPDATE_PLAYER': 'Update player',
'audit.action.CREATE_AGENT': 'Create agent',
'audit.action.UPDATE_AGENT': 'Update agent',
'audit.module.USERS': 'Players',
'audit.module.AGENTS': 'Agents',
'cashback.start_date': 'Start date',
'cashback.end_date': 'End date',
@@ -423,6 +454,9 @@ export const adminPagesEn: Record<string, string> = {
'err.kickoff_required': 'Kickoff time is required',
'err.teams_required': 'Enter home and away team names (ZH or EN)',
'err.league_required': 'League name is required',
'err.user_required': 'Please select a user',
'err.agent_no_parent': 'Tier-1 agents cannot have a parent player',
'err.agent_no_initial_deposit': 'Do not set initial player balance when creating an agent',
'settlement.ht_score': 'Half-time score',
'settlement.ft_score': 'Full-time score',

View File

@@ -1,4 +1,4 @@
/** 表单校验错误key 对应 admin-messages / admin-pages */
/** 表单校验错误key 对应 vue-i18n 文案 */
export class FormValidationError extends Error {
readonly key: string;

View File

@@ -0,0 +1,19 @@
import { createI18n } from 'vue-i18n';
import { adminMessages, type AdminLocale } from './admin-messages';
export const ADMIN_LOCALE_STORAGE_KEY = 'admin_locale';
const saved = (localStorage.getItem(ADMIN_LOCALE_STORAGE_KEY) as AdminLocale) || 'zh-CN';
export const i18n = createI18n({
legacy: false,
locale: saved,
fallbackLocale: ['en-US', 'zh-CN'],
messages: adminMessages,
});
export function getAdminLocale(): AdminLocale {
return i18n.global.locale.value as AdminLocale;
}
export default i18n;

View File

@@ -0,0 +1,16 @@
import { adminMessages, type AdminLocale } from './admin-messages';
import { getAdminLocale } from './index';
/** 扁平 key 翻译(避免 vue-i18n 把 audit.action.X 当成嵌套路径) */
export function resolveAdminMessage(key: string, locale?: AdminLocale): string | undefined {
const chain: AdminLocale[] = [];
const primary = locale ?? getAdminLocale();
for (const loc of [primary, 'en-US', 'zh-CN'] as AdminLocale[]) {
if (!chain.includes(loc)) chain.push(loc);
}
for (const loc of chain) {
const v = adminMessages[loc]?.[key];
if (v) return v;
}
return undefined;
}