feat(i18n): 管理端与玩家端三语支持(中/英/马来语)
- 管理后台 adminT 文案库、结算与代理端页面、表单校验 - 玩家端 vue-i18n 补全首页/公告/串关与 ms 文案 - Element Plus ms 语言包与共享 locale 工具
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
export type BetTagType = '' | 'info' | 'success' | 'warning' | 'danger';
|
||||
import { computed } from 'vue';
|
||||
import { adminT, type AdminLocale } from '../i18n/admin-messages';
|
||||
import { getAdminLocale, useAdminLocale } from '../composables/useAdminLocale';
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
PENDING: '待结算',
|
||||
WON: '已赢',
|
||||
LOST: '已输',
|
||||
VOID: '作废',
|
||||
REFUNDED: '已退款',
|
||||
};
|
||||
export type BetTagType = '' | 'info' | 'success' | 'warning' | 'danger';
|
||||
|
||||
const STATUS_TAG: Record<string, BetTagType> = {
|
||||
PENDING: 'warning',
|
||||
@@ -16,13 +12,14 @@ const STATUS_TAG: Record<string, BetTagType> = {
|
||||
REFUNDED: 'info',
|
||||
};
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
SINGLE: '单关',
|
||||
PARLAY: '串关',
|
||||
};
|
||||
function loc(): AdminLocale {
|
||||
return getAdminLocale();
|
||||
}
|
||||
|
||||
export function betStatusLabel(status: string) {
|
||||
return STATUS_LABELS[status] ?? status;
|
||||
const key = `bet.status.${status}`;
|
||||
const v = adminT(loc(), key);
|
||||
return v !== key ? v : status;
|
||||
}
|
||||
|
||||
export function betStatusTagType(status: string): BetTagType {
|
||||
@@ -30,20 +27,44 @@ export function betStatusTagType(status: string): BetTagType {
|
||||
}
|
||||
|
||||
export function betTypeLabel(betType: string) {
|
||||
return TYPE_LABELS[betType] ?? betType;
|
||||
const key = `bet.type.${betType}`;
|
||||
const v = adminT(loc(), key);
|
||||
return v !== key ? v : betType;
|
||||
}
|
||||
|
||||
const SETTLEMENT_LABELS: Record<string, string> = {
|
||||
PENDING: '待结算',
|
||||
SETTLED: '已结算',
|
||||
VOID: '已作废',
|
||||
};
|
||||
|
||||
export function betSettlementLabel(v: string | null | undefined) {
|
||||
if (!v) return '—';
|
||||
return SETTLEMENT_LABELS[v] ?? v;
|
||||
const key = `bet.settlement.${v}`;
|
||||
const label = adminT(loc(), key);
|
||||
return label !== key ? label : v;
|
||||
}
|
||||
|
||||
export function betResultLabel(s: string | null | undefined) {
|
||||
if (!s) return '—';
|
||||
const key = `bet.result.${s}`;
|
||||
const label = adminT(loc(), key);
|
||||
return label !== key ? label : s;
|
||||
}
|
||||
|
||||
export function useBetFilterOptions() {
|
||||
const { t } = useAdminLocale();
|
||||
const statusOptions = computed(() => [
|
||||
{ value: '', label: t('common.all') },
|
||||
{ value: 'PENDING', label: t('bet.status.PENDING') },
|
||||
{ value: 'WON', label: t('bet.status.WON') },
|
||||
{ value: 'LOST', label: t('bet.status.LOST') },
|
||||
{ value: 'VOID', label: t('bet.status.VOID') },
|
||||
{ value: 'REFUNDED', label: t('bet.status.REFUNDED') },
|
||||
]);
|
||||
const typeOptions = computed(() => [
|
||||
{ value: '', label: t('common.all') },
|
||||
{ value: 'SINGLE', label: t('bet.type.SINGLE') },
|
||||
{ value: 'PARLAY', label: t('bet.type.PARLAY') },
|
||||
]);
|
||||
return { statusOptions, typeOptions };
|
||||
}
|
||||
|
||||
/** @deprecated 使用 useBetFilterOptions() */
|
||||
export const BET_STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'PENDING', label: '待结算' },
|
||||
@@ -53,6 +74,7 @@ export const BET_STATUS_OPTIONS = [
|
||||
{ value: 'REFUNDED', label: '已退款' },
|
||||
];
|
||||
|
||||
/** @deprecated 使用 useBetFilterOptions() */
|
||||
export const BET_TYPE_OPTIONS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'SINGLE', label: '单关' },
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import type { AdminLocale } from '../i18n/admin-messages';
|
||||
import { formatAmount, formatAmountFull } from './format-amount';
|
||||
|
||||
export type ChartI18n = {
|
||||
locale?: AdminLocale;
|
||||
betCountSeries: string;
|
||||
axisAmount: string;
|
||||
axisCount: string;
|
||||
countSuffix: string;
|
||||
pieTooltip: string;
|
||||
noData: string;
|
||||
pieEmpty: string;
|
||||
};
|
||||
|
||||
const tooltipBase = {
|
||||
backgroundColor: '#141414',
|
||||
borderColor: '#2a2a2a',
|
||||
@@ -110,7 +122,10 @@ export function buildMultiLineChartOption(
|
||||
export function buildPieChartOption(
|
||||
title: string,
|
||||
segments: PieSegment[],
|
||||
i18n?: Partial<ChartI18n>,
|
||||
): EChartsOption {
|
||||
const loc = i18n?.locale;
|
||||
const noData = i18n?.noData ?? '暂无数据';
|
||||
const data = segments.map((s) => ({
|
||||
name: s.label,
|
||||
value: s.value,
|
||||
@@ -122,7 +137,7 @@ export function buildPieChartOption(
|
||||
tooltip: {
|
||||
...tooltipBase,
|
||||
trigger: 'item',
|
||||
formatter: '{b}:{c}({d}%)',
|
||||
formatter: i18n?.pieTooltip ?? '{b}:{c}({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
@@ -151,14 +166,15 @@ export function buildPieChartOption(
|
||||
label: { fontSize: 12, fontWeight: 'bold' },
|
||||
scaleSize: 6,
|
||||
},
|
||||
data: data.length ? data : [{ name: '暂无数据', value: 1, itemStyle: { color: '#333' } }],
|
||||
data: data.length ? data : [{ name: noData, value: 1, itemStyle: { color: '#333' } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function fmtCount(v: number) {
|
||||
return v.toLocaleString('zh-CN', { maximumFractionDigits: 0 });
|
||||
function fmtCount(v: number, locale?: AdminLocale) {
|
||||
const tag = locale ?? 'zh-CN';
|
||||
return v.toLocaleString(tag, { maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
/** 7 日金额折线 + 注单柱(双 Y 轴),一图看清趋势 */
|
||||
@@ -166,7 +182,11 @@ export function buildCombinedTrendOption(
|
||||
labels: string[],
|
||||
amountSeries: ChartSeries[],
|
||||
betCounts: number[],
|
||||
i18n: ChartI18n,
|
||||
): EChartsOption {
|
||||
const loc = i18n.locale;
|
||||
const betCountName = i18n.betCountSeries;
|
||||
const countSuffix = i18n.countSuffix;
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
color: [...amountSeries.map((s) => s.color), '#fb923c'],
|
||||
@@ -178,9 +198,10 @@ export function buildCombinedTrendOption(
|
||||
return items
|
||||
.map((p) => {
|
||||
const v = Number(p.value ?? 0);
|
||||
const isCount = p.seriesName === '注单笔数';
|
||||
const val = isCount ? `${fmtCount(v)} 笔` : formatAmountFull(v);
|
||||
return `${p.marker ?? ''}${p.seriesName}:${val}`;
|
||||
const isCount = p.seriesName === betCountName;
|
||||
const val = isCount ? `${fmtCount(v, loc)} ${countSuffix}` : formatAmountFull(v, loc);
|
||||
const sep = loc?.startsWith('zh') ? ':' : ': ';
|
||||
return `${p.marker ?? ''}${p.seriesName}${sep}${val}`;
|
||||
})
|
||||
.join('<br/>');
|
||||
},
|
||||
@@ -202,16 +223,16 @@ export function buildCombinedTrendOption(
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额',
|
||||
name: i18n.axisAmount,
|
||||
nameTextStyle: { color: '#666', fontSize: 10 },
|
||||
axisLabel: { ...axisLabel, formatter: (v: number) => formatAmount(v) },
|
||||
axisLabel: { ...axisLabel, formatter: (v: number) => formatAmount(v, 2, loc) },
|
||||
splitLine,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '笔数',
|
||||
name: i18n.axisCount,
|
||||
nameTextStyle: { color: '#666', fontSize: 10 },
|
||||
axisLabel: { ...axisLabel, formatter: (v: number) => fmtCount(v) },
|
||||
axisLabel: { ...axisLabel, formatter: (v: number) => fmtCount(v, loc) },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
@@ -228,7 +249,7 @@ export function buildCombinedTrendOption(
|
||||
lineStyle: { color: s.color, width: 2 },
|
||||
})),
|
||||
{
|
||||
name: '注单笔数',
|
||||
name: betCountName,
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: betCounts,
|
||||
@@ -242,7 +263,10 @@ export function buildCombinedTrendOption(
|
||||
/** 三个饼图并排,占一张图 */
|
||||
export function buildTriplePieOption(
|
||||
blocks: { title: string; segments: PieSegment[] }[],
|
||||
i18n?: Partial<ChartI18n>,
|
||||
): EChartsOption {
|
||||
const pieEmpty = i18n?.pieEmpty ?? '暂无';
|
||||
const pieTooltip = i18n?.pieTooltip ?? '{b}:{c}({d}%)';
|
||||
const slots = [
|
||||
{ center: ['18%', '58%'] as [string, string], titleLeft: '14%' },
|
||||
{ center: ['50%', '58%'] as [string, string], titleLeft: '46%' },
|
||||
@@ -254,7 +278,7 @@ export function buildTriplePieOption(
|
||||
tooltip: {
|
||||
...tooltipBase,
|
||||
trigger: 'item',
|
||||
formatter: '{b}:{c}({d}%)',
|
||||
formatter: pieTooltip,
|
||||
},
|
||||
graphic: blocks.map((b, i) => ({
|
||||
type: 'text' as const,
|
||||
@@ -283,7 +307,7 @@ export function buildTriplePieOption(
|
||||
labelLine: { show: false },
|
||||
data: data.length
|
||||
? data
|
||||
: [{ name: '暂无', value: 1, itemStyle: { color: '#333' } }],
|
||||
: [{ name: pieEmpty, value: 1, itemStyle: { color: '#333' } }],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,38 +1,57 @@
|
||||
/** 完整数字(悬停提示、详情对照) */
|
||||
export function formatAmountFull(value: string | number | null | undefined): string {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return '—';
|
||||
return n.toLocaleString('zh-CN', { maximumFractionDigits: 4 });
|
||||
import { getAdminLocale } from '../composables/useAdminLocale';
|
||||
import type { AdminLocale } from '../i18n/admin-messages';
|
||||
|
||||
function resolveLocale(locale?: AdminLocale): AdminLocale {
|
||||
return locale ?? getAdminLocale();
|
||||
}
|
||||
|
||||
function unitPart(abs: number, divisor: number, maxDecimals: number): string {
|
||||
return (abs / divisor).toLocaleString('zh-CN', {
|
||||
function localeTag(locale: AdminLocale): string {
|
||||
return locale;
|
||||
}
|
||||
|
||||
/** 完整数字(悬停提示、详情对照) */
|
||||
export function formatAmountFull(
|
||||
value: string | number | null | undefined,
|
||||
locale?: AdminLocale,
|
||||
): string {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return '—';
|
||||
return n.toLocaleString(localeTag(resolveLocale(locale)), { maximumFractionDigits: 4 });
|
||||
}
|
||||
|
||||
function unitPart(abs: number, divisor: number, maxDecimals: number, locale: AdminLocale): string {
|
||||
return (abs / divisor).toLocaleString(localeTag(locale), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: maxDecimals,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额展示:≥1万用「万」,≥1亿用「亿」,避免表格撑破布局
|
||||
* 金额展示:中文用万/亿;英文用 K/M/B
|
||||
*/
|
||||
export function formatAmount(
|
||||
value: string | number | null | undefined,
|
||||
maxDecimals = 2,
|
||||
locale?: AdminLocale,
|
||||
): string {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return '—';
|
||||
|
||||
const loc = resolveLocale(locale);
|
||||
const sign = n < 0 ? '-' : '';
|
||||
const abs = Math.abs(n);
|
||||
const zh = loc.startsWith('zh');
|
||||
|
||||
if (abs >= 1e8) {
|
||||
return `${sign}${unitPart(abs, 1e8, maxDecimals)}亿`;
|
||||
}
|
||||
if (abs >= 1e4) {
|
||||
return `${sign}${unitPart(abs, 1e4, maxDecimals)}万`;
|
||||
if (zh) {
|
||||
if (abs >= 1e8) return `${sign}${unitPart(abs, 1e8, maxDecimals, loc)}亿`;
|
||||
if (abs >= 1e4) return `${sign}${unitPart(abs, 1e4, maxDecimals, loc)}万`;
|
||||
} else {
|
||||
if (abs >= 1e9) return `${sign}${unitPart(abs, 1e9, maxDecimals, loc)}B`;
|
||||
if (abs >= 1e6) return `${sign}${unitPart(abs, 1e6, maxDecimals, loc)}M`;
|
||||
if (abs >= 1e3) return `${sign}${unitPart(abs, 1e3, maxDecimals, loc)}K`;
|
||||
}
|
||||
|
||||
return n.toLocaleString('zh-CN', {
|
||||
return n.toLocaleString(localeTag(loc), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: maxDecimals,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user