feat(player,shared): 内置球员头像名称支持三语显示

This commit is contained in:
2026-06-04 11:55:17 +08:00
parent a8e4ead618
commit c68abadceb
3 changed files with 367 additions and 11 deletions

View File

@@ -1,7 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { BUILTIN_PLAYERS, playerAvatarUrl } from '@thebet365/shared'; import {
BUILTIN_PLAYERS,
playerAvatarUrl,
getPlayerDisplayName,
formatPlayerMeta,
getPlayerSearchTokens,
} from '@thebet365/shared';
const props = defineProps<{ const props = defineProps<{
modelValue: string | null; modelValue: string | null;
@@ -11,18 +17,13 @@ const emit = defineEmits<{
'update:modelValue': [value: string | null]; 'update:modelValue': [value: string | null];
}>(); }>();
const { t } = useI18n(); const { t, locale } = useI18n();
const keyword = ref(''); const keyword = ref('');
const filtered = computed(() => { const filtered = computed(() => {
const q = keyword.value.trim().toLowerCase(); const q = keyword.value.trim().toLowerCase();
if (!q) return BUILTIN_PLAYERS; if (!q) return BUILTIN_PLAYERS;
return BUILTIN_PLAYERS.filter( return BUILTIN_PLAYERS.filter((p) => getPlayerSearchTokens(p).some((token) => token.includes(q)));
(p) =>
p.name.toLowerCase().includes(q) ||
p.country.toLowerCase().includes(q) ||
p.position.toLowerCase().includes(q),
);
}); });
function select(key: string) { function select(key: string) {
@@ -51,9 +52,13 @@ function select(key: string) {
:class="{ active: modelValue === player.id }" :class="{ active: modelValue === player.id }"
@click="select(player.id)" @click="select(player.id)"
> >
<img :src="playerAvatarUrl(player.id) ?? ''" :alt="player.name" class="picker-photo" /> <img
<span class="picker-name">{{ player.name }}</span> :src="playerAvatarUrl(player.id) ?? ''"
<span class="picker-meta">{{ player.position }} · {{ player.country }}</span> :alt="getPlayerDisplayName(player, locale)"
class="picker-photo"
/>
<span class="picker-name">{{ getPlayerDisplayName(player, locale) }}</span>
<span class="picker-meta">{{ formatPlayerMeta(player, locale) }}</span>
</button> </button>
</div> </div>

View File

@@ -118,6 +118,7 @@ export const PARLAY_MAX_LEGS = 5;
export * from './betting-rules'; export * from './betting-rules';
export * from './locale'; export * from './locale';
export * from './builtinPlayers'; export * from './builtinPlayers';
export * from './playerLocale';
export interface ApiResponse<T = unknown> { export interface ApiResponse<T = unknown> {
success: boolean; success: boolean;

View File

@@ -0,0 +1,350 @@
import { resolveTranslationFallback } from './locale';
import type { BuiltinPlayer } from './builtinPlayers';
export type PlayerPositionCode = 'FW' | 'MF' | 'DF' | 'GK';
type LocaleText = Partial<Record<string, string>>;
export type PlayerLocaleMeta = {
name: LocaleText;
position: PlayerPositionCode;
country: string;
};
export const PLAYER_POSITION_LABELS: Record<PlayerPositionCode, LocaleText> = {
FW: { 'zh-CN': '前锋', 'en-US': 'Forward', 'ms-MY': 'Penyerang' },
MF: { 'zh-CN': '中场', 'en-US': 'Midfielder', 'ms-MY': 'Pemain Tengah' },
DF: { 'zh-CN': '后卫', 'en-US': 'Defender', 'ms-MY': 'Pertahan' },
GK: { 'zh-CN': '守门员', 'en-US': 'Goalkeeper', 'ms-MY': 'Penjaga Gol' },
};
export const PLAYER_COUNTRY_LABELS: Record<string, LocaleText> = {
ARG: { 'zh-CN': '阿根廷', 'en-US': 'Argentina', 'ms-MY': 'Argentina' },
ESP: { 'zh-CN': '西班牙', 'en-US': 'Spain', 'ms-MY': 'Sepanyol' },
CRO: { 'zh-CN': '克罗地亚', 'en-US': 'Croatia', 'ms-MY': 'Croatia' },
URU: { 'zh-CN': '乌拉圭', 'en-US': 'Uruguay', 'ms-MY': 'Uruguay' },
TUR: { 'zh-CN': '土耳其', 'en-US': 'Turkey', 'ms-MY': 'Turki' },
USA: { 'zh-CN': '美国', 'en-US': 'USA', 'ms-MY': 'Amerika Syarikat' },
BRA: { 'zh-CN': '巴西', 'en-US': 'Brazil', 'ms-MY': 'Brazil' },
POR: { 'zh-CN': '葡萄牙', 'en-US': 'Portugal', 'ms-MY': 'Portugal' },
ENG: { 'zh-CN': '英格兰', 'en-US': 'England', 'ms-MY': 'England' },
FRA: { 'zh-CN': '法国', 'en-US': 'France', 'ms-MY': 'Perancis' },
NED: { 'zh-CN': '荷兰', 'en-US': 'Netherlands', 'ms-MY': 'Belanda' },
GER: { 'zh-CN': '德国', 'en-US': 'Germany', 'ms-MY': 'Jerman' },
NOR: { 'zh-CN': '挪威', 'en-US': 'Norway', 'ms-MY': 'Norway' },
MEX: { 'zh-CN': '墨西哥', 'en-US': 'Mexico', 'ms-MY': 'Mexico' },
EGY: { 'zh-CN': '埃及', 'en-US': 'Egypt', 'ms-MY': 'Mesir' },
SWE: { 'zh-CN': '瑞典', 'en-US': 'Sweden', 'ms-MY': 'Sweden' },
COL: { 'zh-CN': '哥伦比亚', 'en-US': 'Colombia', 'ms-MY': 'Colombia' },
MAR: { 'zh-CN': '摩洛哥', 'en-US': 'Morocco', 'ms-MY': 'Maghribi' },
BEL: { 'zh-CN': '比利时', 'en-US': 'Belgium', 'ms-MY': 'Belgium' },
};
/** key = BuiltinPlayer.id文件名去掉 .jpg */
export const PLAYER_LOCALE_META: Record<string, PlayerLocaleMeta> = {
'何塞·曼努埃尔·洛佩斯-前锋-阿根廷': {
name: { 'zh-CN': '何塞·曼努埃尔·洛佩斯', 'en-US': 'José Manuel López', 'ms-MY': 'José Manuel López' },
position: 'FW',
country: 'ARG',
},
'佩德里-中场-西班牙': {
name: { 'zh-CN': '佩德里', 'en-US': 'Pedri', 'ms-MY': 'Pedri' },
position: 'MF',
country: 'ESP',
},
'卢卡·莫德里奇-中场-克罗地亚': {
name: { 'zh-CN': '卢卡·莫德里奇', 'en-US': 'Luka Modrić', 'ms-MY': 'Luka Modrić' },
position: 'MF',
country: 'CRO',
},
'华金·皮克雷斯-中场-乌拉圭': {
name: { 'zh-CN': '华金·皮克雷斯', 'en-US': 'Joaquín Piquerez', 'ms-MY': 'Joaquín Piquerez' },
position: 'MF',
country: 'URU',
},
'乔治亚·德·阿拉斯凯塔-中场-乌拉圭': {
name: { 'zh-CN': '乔治亚·德·阿拉斯凯塔', 'en-US': 'Giorgian de Arrascaeta', 'ms-MY': 'Giorgian de Arrascaeta' },
position: 'MF',
country: 'URU',
},
'乌古尔坎·卡基尔-守門員-土耳其': {
name: { 'zh-CN': '乌古尔坎·卡基尔', 'en-US': 'Uğurcan Çakır', 'ms-MY': 'Uğurcan Çakır' },
position: 'GK',
country: 'TUR',
},
'亚历杭德罗·曾德哈斯-前锋-美国': {
name: { 'zh-CN': '亚历杭德罗·曾德哈斯', 'en-US': 'Alejandro Zendejas', 'ms-MY': 'Alejandro Zendejas' },
position: 'FW',
country: 'USA',
},
'恩德里克-前锋-巴西': {
name: { 'zh-CN': '恩德里克', 'en-US': 'Endrick', 'ms-MY': 'Endrick' },
position: 'FW',
country: 'BRA',
},
'克里斯蒂亚诺·罗纳尔多-前锋-葡萄牙': {
name: { 'zh-CN': '克里斯蒂亚诺·罗纳尔多', 'en-US': 'Cristiano Ronaldo', 'ms-MY': 'Cristiano Ronaldo' },
position: 'FW',
country: 'POR',
},
'克里斯蒂安·罗梅罗-后卫-阿根廷': {
name: { 'zh-CN': '克里斯蒂安·罗梅罗', 'en-US': 'Cristian Romero', 'ms-MY': 'Cristian Romero' },
position: 'DF',
country: 'ARG',
},
'内马尔-前锋-巴西': {
name: { 'zh-CN': '内马尔', 'en-US': 'Neymar', 'ms-MY': 'Neymar' },
position: 'FW',
country: 'BRA',
},
'凯南·耶尔德兹-前锋-土耳其': {
name: { 'zh-CN': '凯南·耶尔德兹', 'en-US': 'Kenan Yıldız', 'ms-MY': 'Kenan Yıldız' },
position: 'FW',
country: 'TUR',
},
'卡塞米罗-中场-巴西': {
name: { 'zh-CN': '卡塞米罗', 'en-US': 'Casemiro', 'ms-MY': 'Casemiro' },
position: 'MF',
country: 'BRA',
},
'卢卡斯·帕奎塔-中场-巴西': {
name: { 'zh-CN': '卢卡斯·帕奎塔', 'en-US': 'Lucas Paquetá', 'ms-MY': 'Lucas Paquetá' },
position: 'MF',
country: 'BRA',
},
'基利安·姆巴佩-前锋-法国': {
name: { 'zh-CN': '基利安·姆巴佩', 'en-US': 'Kylian Mbappé', 'ms-MY': 'Kylian Mbappé' },
position: 'FW',
country: 'FRA',
},
'孟菲斯·德派-前锋-荷兰': {
name: { 'zh-CN': '孟菲斯·德派', 'en-US': 'Memphis Depay', 'ms-MY': 'Memphis Depay' },
position: 'FW',
country: 'NED',
},
'奥斯曼·登贝莱-前锋-法国': {
name: { 'zh-CN': '奥斯曼·登贝莱', 'en-US': 'Ousmane Dembélé', 'ms-MY': 'Ousmane Dembélé' },
position: 'FW',
country: 'FRA',
},
'布鲁诺·吉马良斯-中场-巴西': {
name: { 'zh-CN': '布鲁诺·吉马良斯', 'en-US': 'Bruno Guimarães', 'ms-MY': 'Bruno Guimarães' },
position: 'MF',
country: 'BRA',
},
'布鲁诺·费尔南德斯-中场-葡萄牙': {
name: { 'zh-CN': '布鲁诺·费尔南德斯', 'en-US': 'Bruno Fernandes', 'ms-MY': 'Bruno Fernandes' },
position: 'MF',
country: 'POR',
},
'布卡约·萨卡-前锋-英格兰': {
name: { 'zh-CN': '布卡约·萨卡', 'en-US': 'Bukayo Saka', 'ms-MY': 'Bukayo Saka' },
position: 'FW',
country: 'ENG',
},
'德尼兹·居尔-前锋-土耳其': {
name: { 'zh-CN': '德尼兹·居尔', 'en-US': 'Deniz Gül', 'ms-MY': 'Deniz Gül' },
position: 'FW',
country: 'TUR',
},
'德尼兹·温达夫-前锋-德国': {
name: { 'zh-CN': '德尼兹·温达夫', 'en-US': 'Deniz Undav', 'ms-MY': 'Deniz Undav' },
position: 'FW',
country: 'GER',
},
'拉斐尔·迪亚斯·贝洛利-前锋-巴西': {
name: { 'zh-CN': '拉斐尔·迪亚斯·贝洛利', 'en-US': 'Raphinha', 'ms-MY': 'Raphinha' },
position: 'FW',
country: 'BRA',
},
'拉明·亚马尔-前锋-西班牙': {
name: { 'zh-CN': '拉明·亚马尔', 'en-US': 'Lamine Yamal', 'ms-MY': 'Lamine Yamal' },
position: 'FW',
country: 'ESP',
},
'朱利安·阿尔瓦雷斯-前锋-阿根廷': {
name: { 'zh-CN': '朱利安·阿尔瓦雷斯', 'en-US': 'Julián Álvarez', 'ms-MY': 'Julián Álvarez' },
position: 'FW',
country: 'ARG',
},
'梅西-前锋-阿根廷': {
name: { 'zh-CN': '梅西', 'en-US': 'Lionel Messi', 'ms-MY': 'Lionel Messi' },
position: 'FW',
country: 'ARG',
},
'迈克尔·奥利塞-前锋-法国': {
name: { 'zh-CN': '迈克尔·奥利塞', 'en-US': 'Michael Olise', 'ms-MY': 'Michael Olise' },
position: 'FW',
country: 'FRA',
},
'穆罕默德·萨拉赫-前锋-埃及': {
name: { 'zh-CN': '穆罕默德·萨拉赫', 'en-US': 'Mohamed Salah', 'ms-MY': 'Mohamed Salah' },
position: 'FW',
country: 'EGY',
},
'维尼修斯·儒尼奥尔-前锋-巴西': {
name: { 'zh-CN': '维尼修斯·儒尼奥尔', 'en-US': 'Vinícius Júnior', 'ms-MY': 'Vinícius Júnior' },
position: 'FW',
country: 'BRA',
},
'维克托·哲凯赖什-前锋-瑞典': {
name: { 'zh-CN': '维克托·哲凯赖什', 'en-US': 'Viktor Gyökeres', 'ms-MY': 'Viktor Gyökeres' },
position: 'FW',
country: 'SWE',
},
'圣地亚哥·吉梅内斯-前锋-墨西哥': {
name: { 'zh-CN': '圣地亚哥·吉梅内斯', 'en-US': 'Santiago Giménez', 'ms-MY': 'Santiago Giménez' },
position: 'FW',
country: 'MEX',
},
'埃德森·阿尔瓦雷斯-后卫-墨西哥': {
name: { 'zh-CN': '埃德森·阿尔瓦雷斯', 'en-US': 'Edson Álvarez', 'ms-MY': 'Edson Álvarez' },
position: 'DF',
country: 'MEX',
},
'埃贝雷奇·埃泽-中场-英格兰': {
name: { 'zh-CN': '埃贝雷奇·埃泽', 'en-US': 'Eberechi Eze', 'ms-MY': 'Eberechi Eze' },
position: 'MF',
country: 'ENG',
},
'埃尔林·哈兰德-前锋-挪威': {
name: { 'zh-CN': '埃尔林·哈兰德', 'en-US': 'Erling Haaland', 'ms-MY': 'Erling Haaland' },
position: 'FW',
country: 'NOR',
},
'蒂博·库尔图瓦-守門員-比利时': {
name: { 'zh-CN': '蒂博·库尔图瓦', 'en-US': 'Thibaut Courtois', 'ms-MY': 'Thibaut Courtois' },
position: 'GK',
country: 'BEL',
},
'曼努埃尔·诺伊尔-守門員-德国': {
name: { 'zh-CN': '曼努埃尔·诺伊尔', 'en-US': 'Manuel Neuer', 'ms-MY': 'Manuel Neuer' },
position: 'GK',
country: 'GER',
},
'祖德·贝林厄姆-中场-英格兰': {
name: { 'zh-CN': '祖德·贝林厄姆', 'en-US': 'Jude Bellingham', 'ms-MY': 'Jude Bellingham' },
position: 'MF',
country: 'ENG',
},
'伦纳特·卡尔-中场-德国': {
name: { 'zh-CN': '伦纳特·卡尔', 'en-US': 'Lennart Karl', 'ms-MY': 'Lennart Karl' },
position: 'MF',
country: 'GER',
},
'费德里科·巴尔韦德-中场-乌拉圭': {
name: { 'zh-CN': '费德里科·巴尔韦德', 'en-US': 'Federico Valverde', 'ms-MY': 'Federico Valverde' },
position: 'MF',
country: 'URU',
},
'贾马尔·慕斯拉-中场-德国': {
name: { 'zh-CN': '贾马尔·慕斯拉', 'en-US': 'Jamal Musiala', 'ms-MY': 'Jamal Musiala' },
position: 'MF',
country: 'GER',
},
'路易斯·迪亚斯-前锋-哥伦比亚': {
name: { 'zh-CN': '路易斯·迪亚斯', 'en-US': 'Luis Díaz', 'ms-MY': 'Luis Díaz' },
position: 'FW',
country: 'COL',
},
'阿什拉夫·哈基米-后卫-摩洛哥': {
name: { 'zh-CN': '阿什拉夫·哈基米', 'en-US': 'Achraf Hakimi', 'ms-MY': 'Achraf Hakimi' },
position: 'DF',
country: 'MAR',
},
'阿利松·贝克尔-守門員-巴西': {
name: { 'zh-CN': '阿利松·贝克尔', 'en-US': 'Alisson Becker', 'ms-MY': 'Alisson Becker' },
position: 'GK',
country: 'BRA',
},
'阿尔达·居莱尔-前锋-土耳其': {
name: { 'zh-CN': '阿尔达·居莱尔', 'en-US': 'Arda Güler', 'ms-MY': 'Arda Güler' },
position: 'FW',
country: 'TUR',
},
'马西斯·拉扬·切尔基-中场-法国': {
name: { 'zh-CN': '马西斯·拉扬·切尔基', 'en-US': 'Rayan Cherki', 'ms-MY': 'Rayan Cherki' },
position: 'MF',
country: 'FRA',
},
'马库斯·拉什福德-前锋-英格兰': {
name: { 'zh-CN': '马库斯·拉什福德', 'en-US': 'Marcus Rashford', 'ms-MY': 'Marcus Rashford' },
position: 'FW',
country: 'ENG',
},
'哈里·凯恩-前锋-英格兰': {
name: { 'zh-CN': '哈里·凯恩', 'en-US': 'Harry Kane', 'ms-MY': 'Harry Kane' },
position: 'FW',
country: 'ENG',
},
'尼科·威廉斯-前锋-西班牙': {
name: { 'zh-CN': '尼科·威廉斯', 'en-US': 'Nico Williams', 'ms-MY': 'Nico Williams' },
position: 'FW',
country: 'ESP',
},
'巴勃罗·加维-中场-西班牙': {
name: { 'zh-CN': '巴勃罗·加维', 'en-US': 'Pablo Gavi', 'ms-MY': 'Pablo Gavi' },
position: 'MF',
country: 'ESP',
},
'吉列尔莫·奥乔亚-守門員-墨西哥': {
name: { 'zh-CN': '吉列尔莫·奥乔亚', 'en-US': 'Guillermo Ochoa', 'ms-MY': 'Guillermo Ochoa' },
position: 'GK',
country: 'MEX',
},
};
function mapPositionFromZh(position: string): PlayerPositionCode {
if (position.includes('守') || position.includes('门') || position.includes('門')) return 'GK';
if (position.includes('后') || position.includes('後')) return 'DF';
if (position.includes('中')) return 'MF';
return 'FW';
}
export function getPlayerLocaleMeta(player: BuiltinPlayer): PlayerLocaleMeta {
const meta = PLAYER_LOCALE_META[player.id];
if (meta) return meta;
return {
name: { 'zh-CN': player.name },
position: mapPositionFromZh(player.position),
country: '',
};
}
export function getPlayerDisplayName(player: BuiltinPlayer, locale: string): string {
const meta = getPlayerLocaleMeta(player);
return resolveTranslationFallback(meta.name, locale) || player.name;
}
export function getPlayerPositionLabel(player: BuiltinPlayer, locale: string): string {
const meta = getPlayerLocaleMeta(player);
const labels = PLAYER_POSITION_LABELS[meta.position];
return resolveTranslationFallback(labels, locale) || player.position;
}
export function getPlayerCountryLabel(player: BuiltinPlayer, locale: string): string {
const meta = getPlayerLocaleMeta(player);
if (!meta.country) return player.country;
const labels = PLAYER_COUNTRY_LABELS[meta.country];
return resolveTranslationFallback(labels ?? {}, locale) || player.country;
}
export function getPlayerSearchTokens(player: BuiltinPlayer): string[] {
const meta = getPlayerLocaleMeta(player);
const tokens = new Set<string>();
for (const value of Object.values(meta.name)) {
if (value) tokens.add(value.toLowerCase());
}
for (const loc of ['zh-CN', 'en-US', 'ms-MY']) {
tokens.add(getPlayerPositionLabel(player, loc).toLowerCase());
tokens.add(getPlayerCountryLabel(player, loc).toLowerCase());
}
tokens.add(player.name.toLowerCase());
tokens.add(player.position.toLowerCase());
tokens.add(player.country.toLowerCase());
return [...tokens];
}
export function formatPlayerMeta(player: BuiltinPlayer, locale: string): string {
return `${getPlayerPositionLabel(player, locale)} · ${getPlayerCountryLabel(player, locale)}`;
}