feat(admin,player,api): 公共管理与优胜冠军国旗、玩家端内容对接

新增公共内容 CRUD 与批量操作;公告滚动合并管理;优胜冠军内置国家选择与单行保存;玩家端统一 usePlayerHome 对接轮播与跑马灯。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 10:25:42 +08:00
parent 27580b2479
commit f76728dc3e
21 changed files with 1966 additions and 136 deletions

View File

@@ -0,0 +1,137 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import {
BUILTIN_COUNTRIES,
countryFlagUrl,
getBuiltinCountry,
searchBuiltinCountries,
type BuiltinCountry,
} from '../../data/builtinCountries';
import { useAdminLocale } from '../../composables/useAdminLocale';
const props = defineProps<{
modelValue: string;
size?: 'small' | 'default' | 'large';
disabled?: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [code: string];
pick: [country: BuiltinCountry];
}>();
const { t, locale } = useAdminLocale();
const filterKeyword = ref('');
const options = computed(() => searchBuiltinCountries(filterKeyword.value));
const selected = computed(() => getBuiltinCountry(props.modelValue));
function optionLabel(c: BuiltinCountry) {
return locale.value === 'en-US'
? `${c.nameEn} (${c.code})`
: `${c.nameZh} (${c.code})`;
}
function onFilter(q: string) {
filterKeyword.value = q;
}
function onChange(code: string | undefined) {
const value = code ?? '';
emit('update:modelValue', value);
const country = getBuiltinCountry(value);
if (country) emit('pick', country);
}
</script>
<template>
<div class="country-flag-select">
<el-select
:model-value="modelValue || undefined"
:size="size ?? 'small'"
:disabled="disabled"
filterable
clearable
:filter-method="onFilter"
:placeholder="t('outright.country_ph')"
class="country-select"
@update:model-value="onChange"
@visible-change="(v: boolean) => { if (v) filterKeyword = ''; }"
>
<el-option
v-for="c in options"
:key="c.code"
:label="optionLabel(c)"
:value="c.code"
>
<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-code">{{ c.code }}</span>
</div>
</el-option>
</el-select>
<img
v-if="selected"
:src="countryFlagUrl(selected)"
alt=""
class="country-preview"
loading="lazy"
/>
</div>
</template>
<style scoped>
.country-flag-select {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
}
.country-select {
flex: 1;
min-width: 0;
}
.country-preview {
width: 32px;
height: 22px;
object-fit: cover;
border-radius: 2px;
flex-shrink: 0;
background: #222;
}
.country-option {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.country-option-flag {
width: 24px;
height: 16px;
object-fit: cover;
border-radius: 2px;
flex-shrink: 0;
}
.country-option-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
.country-option-code {
font-size: 11px;
color: #888;
flex-shrink: 0;
}
</style>