feat(admin,api,player): 赛事分组管理、盘口独立页与多语言展示优化

- 管理端按联赛展示单场,新增赛事/单场流程与列表展开状态保持

- 盘口赔率迁至独立页面,保存按钮仅在有修改时高亮

- API 新增联赛列表与子场查询,按 locale 返回队名并修复编译

- 波胆其它选项与促销标签等 i18n 补齐,文案更易懂
This commit is contained in:
2026-06-04 16:25:03 +08:00
parent c68abadceb
commit cc737e2924
39 changed files with 3330 additions and 378 deletions

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import CountryFlagSelect from './outright/CountryFlagSelect.vue';
import {
countryFlagUrl,
resolveCountryCode,
type BuiltinCountry,
} from '../data/builtinCountries';
import { useAdminLocale } from '../composables/useAdminLocale';
const props = defineProps<{
modelValue: string;
teamCode?: string;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string];
pick: [country: BuiltinCountry];
}>();
const { t } = useAdminLocale();
const countryCode = ref('');
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);
},
{ immediate: true },
);
watch(countryCode, (code, prev) => {
if (!code && prev && isFlagUrl.value) {
emit('update:modelValue', '');
}
});
function onCountryPick(country: BuiltinCountry) {
emit('update:modelValue', countryFlagUrl(country));
emit('pick', country);
}
function onCustomUrlInput(value: string) {
emit('update:modelValue', value);
}
</script>
<template>
<div class="logo-url-field">
<CountryFlagSelect
v-model="countryCode"
hide-preview
class="flag-part"
@pick="onCountryPick"
/>
<el-input
:model-value="modelValue"
size="small"
:placeholder="t('matchEditor.ph.logo_url')"
clearable
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;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
}
.flag-part {
flex: 0 0 200px;
min-width: 0;
}
.url-part {
flex: 1;
min-width: 0;
}
.logo-preview {
flex-shrink: 0;
width: 32px;
height: 22px;
object-fit: contain;
border-radius: 3px;
background: #0d0d0d;
border: 1px solid #2a2a2a;
}
</style>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import {
BUILTIN_COUNTRIES,
countryFlagUrl,
countryDisplayName,
countryOptionLabel,
getBuiltinCountry,
searchBuiltinCountries,
type BuiltinCountry,
@@ -13,6 +14,7 @@ const props = defineProps<{
modelValue: string;
size?: 'small' | 'default' | 'large';
disabled?: boolean;
hidePreview?: boolean;
}>();
const emit = defineEmits<{
@@ -23,14 +25,12 @@ const emit = defineEmits<{
const { t, locale } = useAdminLocale();
const filterKeyword = ref('');
const options = computed(() => searchBuiltinCountries(filterKeyword.value));
const options = computed(() => searchBuiltinCountries(filterKeyword.value, locale.value));
const selected = computed(() => getBuiltinCountry(props.modelValue));
function optionLabel(c: BuiltinCountry) {
return locale.value === 'en-US'
? `${c.nameEn} (${c.code})`
: `${c.nameZh} (${c.code})`;
return countryOptionLabel(c, locale.value);
}
function onFilter(q: string) {
@@ -67,13 +67,13 @@ function onChange(code: string | undefined) {
>
<div class="country-option">
<img :src="countryFlagUrl(c)" alt="" class="country-option-flag" loading="lazy" />
<span class="country-option-name">{{ c.nameZh }} · {{ c.nameEn }}</span>
<span class="country-option-name">{{ countryDisplayName(c, locale) }}</span>
<span class="country-option-code">{{ c.code }}</span>
</div>
</el-option>
</el-select>
<img
v-if="selected"
v-if="selected && !hidePreview"
:src="countryFlagUrl(selected)"
alt=""
class="country-preview"