feat(admin,api,player): 优胜赛配置、赛事管理重构与玩家端投注体验优化

管理端拆分赛事/优胜赛 Tab,新增联赛优胜赔率面板(批量、排序、外侧删除);统一 list-chrome 工具栏对齐与列表页布局;Dashboard 失败重试、Users 操作下拉、小屏侧栏等体验修复。

API 扩展优胜赛与赛事目录接口,完善投注与钱包查询;玩家端重构赛事卡片、串关面板、注单/钱包页,新增注单详情、下注成功动画与下拉刷新。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-08 09:55:56 +08:00
parent efff7c27e6
commit 24fa1b275c
66 changed files with 6289 additions and 1426 deletions

View File

@@ -0,0 +1,193 @@
<script setup lang="ts">
import { ref, watch, onUnmounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
const props = defineProps<{ show: boolean }>();
const emit = defineEmits<{ done: [] }>();
const { t } = useI18n();
const canvasRef = ref<HTMLCanvasElement | null>(null);
let raf = 0;
let particles: Particle[] = [];
interface Particle {
x: number; y: number; vx: number; vy: number;
life: number; maxLife: number; size: number;
color: string; shape: number;
}
const COLORS = ['#D4AF37', '#FFD700', '#F0C040', '#C8A02E', '#E6C84C', '#B8960C'];
let w = 0;
let h = 0;
function spawnBurst() {
const cx = w / 2;
const cy = h * 0.38;
for (let i = 0; i < 80; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1.5 + Math.random() * 5;
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
particles.push({
x: cx + (Math.random() - 0.5) * 60,
y: cy + (Math.random() - 0.5) * 20,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 2,
life: 0,
maxLife: 60 + Math.random() * 80,
size: 3 + Math.random() * 5,
color,
shape: Math.floor(Math.random() * 3),
});
}
}
function draw() {
if (!canvasRef.value) return;
const ctx = canvasRef.value.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, w, h);
for (const p of particles) {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.06;
p.life++;
const alpha = 1 - p.life / p.maxLife;
if (alpha <= 0) continue;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
if (p.shape === 0) {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * 0.5, 0, Math.PI * 2);
ctx.fill();
} else if (p.shape === 1) {
ctx.fillRect(p.x - p.size * 0.35, p.y - p.size * 0.35, p.size * 0.7, p.size * 0.7);
} else {
ctx.beginPath();
ctx.moveTo(p.x, p.y - p.size * 0.5);
ctx.lineTo(p.x + p.size * 0.4, p.y + p.size * 0.3);
ctx.lineTo(p.x - p.size * 0.4, p.y + p.size * 0.3);
ctx.closePath();
ctx.fill();
}
}
ctx.globalAlpha = 1;
particles = particles.filter((p) => p.life < p.maxLife);
raf = requestAnimationFrame(draw);
}
function start() {
setTimeout(() => { emit('done'); }, 2200);
void nextTick(() => {
if (!canvasRef.value) return;
const rect = canvasRef.value.getBoundingClientRect();
w = rect.width;
h = rect.height;
particles = [];
spawnBurst();
cancelAnimationFrame(raf);
raf = requestAnimationFrame(draw);
});
}
function stop() {
cancelAnimationFrame(raf);
particles = [];
}
watch(() => props.show, (v) => {
if (v) start();
else stop();
});
onUnmounted(stop);
</script>
<template>
<Teleport to="body">
<div v-if="show" class="bet-success-overlay" @click="emit('done')">
<canvas ref="canvasRef" class="confetti-canvas" />
<div class="success-card">
<svg class="check" viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
<circle class="check-circle" cx="26" cy="26" r="24" fill="none" stroke="#D4AF37" stroke-width="3" />
<path class="check-mark" d="M14 27l7 7 16-16" fill="none" stroke="#D4AF37" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<p class="success-text">{{ t('bet.place_success') }}</p>
</div>
</div>
</Teleport>
</template>
<style scoped>
.bet-success-overlay {
position: fixed;
inset: 0;
z-index: 300;
background: rgba(0, 0, 0, 0.82);
display: flex;
align-items: center;
justify-content: center;
}
.confetti-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.success-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
animation: card-pop 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.2);
}
.check {
width: 72px;
height: 72px;
}
.check-circle {
stroke-dasharray: 151;
stroke-dashoffset: 151;
animation: circle-draw 0.5s 0.15s ease forwards;
}
.check-mark {
stroke-dasharray: 42;
stroke-dashoffset: 42;
animation: check-draw 0.35s 0.45s ease forwards;
}
.success-text {
color: var(--primary-light);
font-size: 18px;
font-weight: 800;
letter-spacing: 0.04em;
animation: text-up 0.4s 0.4s ease both;
}
@keyframes card-pop {
0% { transform: scale(0.6); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes circle-draw {
to { stroke-dashoffset: 0; }
}
@keyframes check-draw {
to { stroke-dashoffset: 0; }
}
@keyframes text-up {
0% { opacity: 0; transform: translateY(8px); }
100% { opacity: 1; transform: translateY(0); }
}
</style>