feat(admin,api,player): settlement stats, team crests, MS fields and list bet summary

This commit is contained in:
2026-06-04 17:30:48 +08:00
parent cc737e2924
commit 9fcee31a9a
27 changed files with 2296 additions and 427 deletions

View File

@@ -2,15 +2,21 @@
import { ref, watch, computed } from 'vue';
import CountryFlagSelect from './outright/CountryFlagSelect.vue';
import {
countryFlagUrl,
countryLogoUrl,
detectCountryLogoKind,
getBuiltinCountry,
hasCountryCrest,
resolveCountryCode,
type BuiltinCountry,
type CountryLogoKind,
} from '../data/builtinCountries';
import { useAdminLocale } from '../composables/useAdminLocale';
const props = defineProps<{
modelValue: string;
teamCode?: string;
/** 表格等窄布局:隐藏 URL 输入框,仅国家 + 国旗/队徽 */
compact?: boolean;
}>();
const emit = defineEmits<{
@@ -20,47 +26,87 @@ const emit = defineEmits<{
const { t } = useAdminLocale();
const countryCode = ref('');
const logoKind = ref<CountryLogoKind>('crest');
const canPickCrest = computed(() => {
const code = countryCode.value;
return code ? hasCountryCrest(code) : false;
});
const isFlagUrl = computed(() => props.modelValue.includes('flagcdn.com'));
const previewUrl = computed(() => props.modelValue.trim() || '');
watch(
() => [props.modelValue, props.teamCode] as const,
([url, code]) => {
if (url && !url.includes('flagcdn.com')) {
countryCode.value = '';
return;
}
countryCode.value = resolveCountryCode(code, url || null);
if (url) {
logoKind.value = detectCountryLogoKind(url, countryCode.value);
}
},
{ immediate: true },
);
watch(countryCode, (code, prev) => {
if (!code && prev && isFlagUrl.value) {
if (!code && prev && (logoKind.value === 'flag' || logoKind.value === 'crest')) {
emit('update:modelValue', '');
}
});
function applyLogoForCountry(country: BuiltinCountry) {
const kind =
logoKind.value === 'crest' && hasCountryCrest(country) ? 'crest' : 'flag';
logoKind.value = kind;
emit('update:modelValue', countryLogoUrl(country, kind));
}
function onCountryPick(country: BuiltinCountry) {
emit('update:modelValue', countryFlagUrl(country));
countryCode.value = country.code;
if (!props.modelValue.trim()) {
logoKind.value = hasCountryCrest(country) ? 'crest' : 'flag';
}
applyLogoForCountry(country);
emit('pick', country);
}
function onLogoKindChange(kind: CountryLogoKind) {
if (kind === 'custom') return;
const country = getBuiltinCountry(countryCode.value);
if (!country) return;
logoKind.value = kind === 'crest' && !hasCountryCrest(country) ? 'flag' : kind;
emit('update:modelValue', countryLogoUrl(country, logoKind.value as 'flag' | 'crest'));
}
function onCustomUrlInput(value: string) {
logoKind.value = detectCountryLogoKind(value, countryCode.value);
emit('update:modelValue', value);
}
</script>
<template>
<div class="logo-url-field">
<CountryFlagSelect
v-model="countryCode"
hide-preview
class="flag-part"
@pick="onCountryPick"
/>
<div class="logo-url-field" :class="{ compact }">
<div class="pick-row">
<CountryFlagSelect
v-model="countryCode"
hide-preview
class="flag-part"
@pick="onCountryPick"
/>
<el-radio-group
v-if="countryCode"
:model-value="logoKind === 'custom' ? 'flag' : logoKind"
size="small"
class="kind-group"
@update:model-value="onLogoKindChange($event as CountryLogoKind)"
>
<el-radio-button value="flag">{{ t('teamLogo.kind.flag') }}</el-radio-button>
<el-radio-button value="crest" :disabled="!canPickCrest">
{{ t('teamLogo.kind.crest') }}
</el-radio-button>
</el-radio-group>
<img v-if="previewUrl" :src="previewUrl" alt="" class="logo-preview" loading="lazy" />
</div>
<el-input
v-if="!compact"
:model-value="modelValue"
size="small"
:placeholder="t('matchEditor.ph.logo_url')"
@@ -68,12 +114,19 @@ function onCustomUrlInput(value: string) {
class="url-part"
@update:model-value="onCustomUrlInput"
/>
<img v-if="previewUrl" :src="previewUrl" alt="" class="logo-preview" loading="lazy" />
</div>
</template>
<style scoped>
.logo-url-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
min-width: 0;
}
.pick-row {
display: flex;
align-items: center;
gap: 8px;
@@ -82,22 +135,30 @@ function onCustomUrlInput(value: string) {
}
.flag-part {
flex: 0 0 200px;
min-width: 0;
flex: 1;
min-width: 120px;
}
.url-part {
flex: 1;
min-width: 0;
.kind-group {
flex-shrink: 0;
}
.logo-preview {
flex-shrink: 0;
width: 32px;
height: 22px;
height: 32px;
object-fit: contain;
border-radius: 3px;
border-radius: 4px;
background: #0d0d0d;
border: 1px solid #2a2a2a;
}
.url-part {
width: 100%;
}
.logo-url-field.compact .flag-part {
flex: 1;
min-width: 0;
}
</style>

View File

@@ -2,9 +2,11 @@
import { computed, ref } from 'vue';
import {
countryFlagUrl,
countryCrestUrl,
countryDisplayName,
countryOptionLabel,
getBuiltinCountry,
hasCountryCrest,
searchBuiltinCountries,
type BuiltinCountry,
} from '../../data/builtinCountries';
@@ -67,6 +69,13 @@ function onChange(code: string | undefined) {
>
<div class="country-option">
<img :src="countryFlagUrl(c)" alt="" class="country-option-flag" loading="lazy" />
<img
v-if="hasCountryCrest(c)"
:src="countryCrestUrl(c)"
alt=""
class="country-option-crest"
loading="lazy"
/>
<span class="country-option-name">{{ countryDisplayName(c, locale) }}</span>
<span class="country-option-code">{{ c.code }}</span>
</div>
@@ -120,6 +129,13 @@ function onChange(code: string | undefined) {
flex-shrink: 0;
}
.country-option-crest {
width: 22px;
height: 22px;
object-fit: contain;
flex-shrink: 0;
}
.country-option-name {
flex: 1;
min-width: 0;