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

@@ -6,33 +6,26 @@ import { useBetSlipStore } from '../stores/betSlip';
import BetSlipDrawer from '../components/BetSlipDrawer.vue';
import CashBalanceChip from '../components/CashBalanceChip.vue';
import UserAvatarMenu from '../components/UserAvatarMenu.vue';
import LocaleFlag from '../components/LocaleFlag.vue';
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
import { useAppLocale } from '../composables/useAppLocale';
import AnnouncementMarquee from '../components/AnnouncementMarquee.vue';
import BottomNavIcon from '../components/BottomNavIcon.vue';
import { computed, onMounted } from 'vue';
import { getLocaleDisplay } from '../utils/localeDisplay';
import { useAnnouncements } from '../composables/useAnnouncements';
const { t, locale } = useI18n();
const { t } = useI18n();
const auth = useAuthStore();
const { initFromUser } = useAppLocale();
const route = useRoute();
const slip = useBetSlipStore();
const locales = [
{ code: 'zh-CN', label: '中文' },
{ code: 'en-US', label: 'EN' },
{ code: 'ms-MY', label: 'BM' },
];
const showAnnouncement = computed(() => !route.path.startsWith('/profile'));
const { items: announcements, load: loadAnnouncements } = useAnnouncements();
onMounted(loadAnnouncements);
function setLocale(code: string) {
locale.value = code;
localStorage.setItem('locale', code);
}
onMounted(() => {
loadAnnouncements();
if (auth.user?.locale) initFromUser(auth.user.locale);
});
</script>
<template>
@@ -40,14 +33,7 @@ function setLocale(code: string) {
<header class="header">
<img src="/logo.png" alt="TheBet365" class="logo" />
<div class="header-actions">
<div class="lang-select-wrap">
<LocaleFlag :locale="locale" :size="18" />
<select :value="locale" @change="setLocale(($event.target as HTMLSelectElement).value)" class="lang-select">
<option v-for="l in locales" :key="l.code" :value="l.code">
{{ getLocaleDisplay(l.code).label }}
</option>
</select>
</div>
<LocaleSwitcher />
<CashBalanceChip v-if="auth.user" />
<UserAvatarMenu v-if="auth.user" />
</div>
@@ -115,25 +101,20 @@ function setLocale(code: string) {
height: 36px; width: auto; display: block;
filter: drop-shadow(0 0 4px rgba(212, 175, 55, 0.2));
}
.header-actions { display: flex; gap: 6px; align-items: center; }
.lang-select-wrap {
.header-actions {
--header-chip-h: 36px;
display: flex;
gap: 6px;
align-items: center;
gap: 5px;
padding: 3px 6px 3px 5px;
border: 1px solid var(--border);
border-radius: 6px;
background: #0d0d0d;
}
.lang-select {
background: transparent;
color: var(--primary-light);
border: none;
padding: 2px 2px 2px 0;
width: auto;
font-size: 11px;
font-weight: 700;
outline: none;
.header-actions :deep(.locale-switch:not(.compact)),
.header-actions :deep(.cash-chip),
.header-actions :deep(.avatar-btn) {
height: var(--header-chip-h);
box-sizing: border-box;
}
.header-actions :deep(.avatar-btn) {
width: var(--header-chip-h);
}
.announce-strip {
flex-shrink: 0;