将玩家端组件与核心页面全面切换为统一主题变量,优化卡片、按钮、弹窗、列表和盘口相关模块的视觉表现,并补充多语言文案以匹配新的交互与布局风格。 Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
2.6 KiB
Vue
130 lines
2.6 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch, onMounted, onUnmounted } from 'vue';
|
|
import { teamFlagUrl } from '../../utils/teamFlag';
|
|
|
|
const props = defineProps<{
|
|
teamCode: string;
|
|
teamName: string;
|
|
odds: string;
|
|
logoUrl?: string | null;
|
|
}>();
|
|
|
|
const emit = defineEmits<{ pick: [] }>();
|
|
|
|
const flag = computed(
|
|
() => props.logoUrl?.trim() || teamFlagUrl(props.teamCode, props.teamName),
|
|
);
|
|
const flagFailed = ref(false);
|
|
const imgVisible = ref(false);
|
|
const cardRef = ref<HTMLElement | null>(null);
|
|
let observer: IntersectionObserver | null = null;
|
|
|
|
function onFlagError() {
|
|
flagFailed.value = true;
|
|
}
|
|
|
|
watch(
|
|
() => [props.teamCode, props.teamName, props.logoUrl] as const,
|
|
() => {
|
|
flagFailed.value = false;
|
|
},
|
|
);
|
|
|
|
function formatOdds(odds: string) {
|
|
const n = parseFloat(odds);
|
|
return Number.isFinite(n) ? n.toFixed(2) : odds;
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (!cardRef.value) return;
|
|
observer = new IntersectionObserver(
|
|
(entries) => {
|
|
const entry = entries[0];
|
|
if (entry.isIntersecting) {
|
|
imgVisible.value = true;
|
|
observer?.disconnect();
|
|
}
|
|
},
|
|
{ rootMargin: '120px' }
|
|
);
|
|
observer.observe(cardRef.value);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
observer?.disconnect();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<button ref="cardRef" type="button" class="option-card" @click="emit('pick')">
|
|
<img
|
|
v-if="imgVisible && flag && !flagFailed"
|
|
:src="flag"
|
|
alt=""
|
|
class="flag"
|
|
@error="onFlagError"
|
|
/>
|
|
<span v-else class="flag-placeholder" aria-hidden="true">⚽</span>
|
|
<span class="name">{{ teamName }}</span>
|
|
<span class="odds">[ {{ formatOdds(odds) }} ]</span>
|
|
</button>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.option-card {
|
|
position: relative;
|
|
width: 100%;
|
|
min-width: 0;
|
|
padding: 8px 4px 6px;
|
|
border-radius: 8px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
text-align: center;
|
|
overflow: hidden;
|
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
}
|
|
|
|
.option-card:active {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 1px rgba(244, 162, 97, 0.12);
|
|
}
|
|
|
|
.flag {
|
|
width: 28px;
|
|
height: 19px;
|
|
object-fit: cover;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.flag-placeholder {
|
|
width: 28px;
|
|
height: 19px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.name {
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
color: var(--text);
|
|
line-height: 1.2;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.odds {
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
color: var(--warning);
|
|
}
|
|
</style>
|