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,9 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
import api from '../api';
import emptyMatchesImg from '../assets/images/empty-matches.svg';
import BannerCarousel from '../components/BannerCarousel.vue';
@@ -49,7 +52,7 @@ function goMatch(id: string) {
<div>
<BannerCarousel :banners="displayBanners" />
<h2 class="section-title">热门赛事</h2>
<h2 class="section-title">{{ t('home.hot_matches') }}</h2>
<div v-for="match in home?.hotMatches || []" :key="match.id" class="card match-card" @click="goMatch(match.id)">
<div class="match-teams">{{ match.homeTeamName }} vs {{ match.awayTeamName }}</div>
<div class="match-time">{{ new Date(match.startTime).toLocaleString() }}</div>
@@ -57,7 +60,7 @@ function goMatch(id: string) {
<div v-if="home && !home.hotMatches?.length" class="empty">
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
<p>暂无赛事</p>
<p>{{ t('home.no_matches') }}</p>
</div>
</div>
</template>

View File

@@ -3,10 +3,13 @@ import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '../stores/auth';
import { useAppLocale } from '../composables/useAppLocale';
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
import RobotVerify from '../components/RobotVerify.vue';
import loginBg from '../assets/images/h5bg.png';
const { t } = useI18n();
const { initFromUser } = useAppLocale();
const auth = useAuthStore();
const router = useRouter();
const captchaRef = ref<InstanceType<typeof RobotVerify> | null>(null);
@@ -25,6 +28,7 @@ async function submit() {
error.value = '';
try {
await auth.login(username.value, password.value);
initFromUser(auth.user?.locale);
router.push('/');
} catch (e: unknown) {
error.value = (e as { response?: { data?: { error?: string } } })?.response?.data?.error || 'Login failed';
@@ -36,6 +40,9 @@ async function submit() {
<template>
<div class="login-page" :style="{ backgroundImage: `url(${loginBg})` }">
<div class="login-lang">
<LocaleSwitcher compact />
</div>
<form @submit.prevent="submit" class="login-form ps-gold-frame">
<label>{{ t('auth.username') }}</label>
<input v-model="username" class="ps-gold-input" required />
@@ -51,7 +58,15 @@ async function submit() {
</template>
<style scoped>
.login-lang {
position: absolute;
top: max(12px, env(safe-area-inset-top));
right: 16px;
z-index: 2;
}
.login-page {
position: relative;
height: 100%;
min-height: 100dvh;
overflow-y: auto;

View File

@@ -24,7 +24,8 @@ interface Market {
id: string;
marketType: string;
period: string;
lineValue?: string;
lineValue?: string | number | null;
allowParlay?: boolean;
selections: Selection[];
}
@@ -212,6 +213,11 @@ function toggleSelection(sel: Selection, market: Market) {
selectionName: sel.selectionName,
odds: parseFloat(sel.odds),
marketType: market.marketType,
lineValue:
market.lineValue != null && market.lineValue !== ''
? parseFloat(String(market.lineValue))
: null,
allowParlay: market.allowParlay,
});
}

View File

@@ -6,16 +6,12 @@ import api from '../api';
import { formatMoney } from '../utils/localeDisplay';
import LocaleFlag from '../components/LocaleFlag.vue';
import { useAuthStore } from '../stores/auth';
import { useAppLocale } from '../composables/useAppLocale';
const { t, locale } = useI18n();
const router = useRouter();
const auth = useAuthStore();
const locales = [
{ code: 'zh-CN', label: '中文' },
{ code: 'en-US', label: 'EN' },
{ code: 'ms-MY', label: 'BM' },
] as const;
const { locales, setLocale, initFromUser } = useAppLocale();
const profile = ref<{
username?: string;
@@ -25,12 +21,11 @@ const profile = ref<{
onMounted(async () => {
const { data } = await api.get('/player/profile');
profile.value = data.data;
initFromUser(data.data?.locale);
});
async function changeLocale(code: string) {
locale.value = code;
localStorage.setItem('locale', code);
await api.post('/player/language', { locale: code });
await setLocale(code);
}
function logout() {
@@ -79,6 +74,19 @@ function logout() {
</button>
</div>
</div>
<div class="settings-cell settings-cell--stack rules-cell">
<div class="cell-head">
<span class="cell-label">{{ t('profile.rules_title') }}</span>
</div>
<div class="rules-body">
<p>{{ t('profile.rules_p1') }}</p>
<p>{{ t('profile.rules_p2') }}</p>
<p>{{ t('profile.rules_p3') }}</p>
<p>{{ t('profile.rules_p4') }}</p>
<p>{{ t('profile.rules_p5') }}</p>
</div>
</div>
</section>
<button type="button" class="logout-btn" @click="logout">
@@ -236,4 +244,19 @@ function logout() {
.logout-btn:active {
background: rgba(255, 69, 58, 0.08);
}
.rules-body {
padding: 0 0 12px;
font-size: 12px;
line-height: 1.55;
color: var(--text-muted);
}
.rules-body p {
margin: 0 0 8px;
}
.rules-body p:last-child {
margin-bottom: 0;
}
</style>