feat(admin): 添加比赛管理页面功能优化

refactor(utils): 为工具函数添加注释并导出新模块
feat(utils): 新增防抖和节流工具函数
docs(utils): 为现有工具类添加中文注释

style(admin): 优化比赛配置页面UI样式
feat(admin): 实现比赛基础信息表单双向绑定
feat(admin): 新增页面头部公共组件
feat(admin): 完善分组管理自动生成逻辑
feat(admin): 增强题目配置功能与交互

fix(admin): 修复表单数据更新死循环问题
This commit is contained in:
2026-01-20 10:58:42 +08:00
parent 1e8e2d1495
commit 2639aef69b
13 changed files with 443 additions and 128 deletions

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { NGrid, NGridItem, NSelect } from 'naive-ui'
import { ref } from 'vue'
import { computed, ref } from 'vue'
const questionOptions = [
{ label: '汉字听写 - 根据提示书写汉字', value: 'hanzi' },
@ -10,6 +10,9 @@ const questionOptions = [
{ label: '成语填空 - 四字成语', value: 'chengyu' },
{ label: '反义词挑战', value: 'fanyi' },
{ label: '近义词挑战', value: 'jinyi' },
{ label: '看图猜字', value: 'kantu' },
{ label: '古诗词接龙', value: 'gushi' },
{ label: '名句听写', value: 'mingju' },
]
const pkOptions = [
@ -17,23 +20,45 @@ const pkOptions = [
{ label: '极速成语接龙 - 每题20秒', value: 'speed20' },
]
const scoreOptions = [
{ label: '1分', value: 1 },
{ label: '2分', value: 2 },
{ label: '3分', value: 3 },
{ label: '5分', value: 5 },
{ label: '10分', value: 10 },
]
const timeOptions = [
{ label: '15s', value: 15 },
{ label: '30s', value: 30 },
{ label: '45s', value: 45 },
{ label: '60s', value: 60 },
{ label: '90s', value: 90 },
]
const regularQuestions = ref([
{ value: 'hanzi' },
{ value: 'tongyin' },
{ value: 'pianpang' },
{ value: 'ciyu' },
{ value: 'chengyu' },
{ value: 'fanyi' },
{ value: 'jinyi' },
{ value: 'hanzi', score: 1, time: 15 },
{ value: 'tongyin', score: 1, time: 15 },
{ value: 'pianpang', score: 1, time: 30 },
{ value: 'ciyu', score: 1, time: 30 },
{ value: 'chengyu', score: 1, time: 30 },
{ value: 'fanyi', score: 1, time: 60 },
{ value: 'jinyi', score: 1, time: 30 },
{ value: 'kantu', score: 1, time: 30 },
{ value: 'gushi', score: 1, time: 60 },
{ value: 'mingju', score: 1, time: 60 },
])
const pkQuestions = ref([
{ value: 'speed10' },
{ value: 'speed10' },
{ value: 'speed10' },
{ value: 'speed10' },
{ value: 'speed10' },
{ value: 'speed10', score: 1, time: 60 },
{ value: 'speed10', score: 1, time: 60 },
{ value: 'speed10', score: 1, time: 60 },
{ value: 'speed10', score: 1, time: 60 },
{ value: 'speed10', score: 3, time: 60 },
])
const regularTotalScore = computed(() => regularQuestions.value.reduce((acc, cur) => acc + cur.score, 0))
const pkTotalScore = computed(() => pkQuestions.value.reduce((acc, cur) => acc + cur.score, 0))
</script>
<template>
@ -41,25 +66,76 @@ const pkQuestions = ref([
<NGrid :x-gap="48" :y-gap="24" cols="1 l:2" responsive="screen">
<!-- 左侧常规赛题包 -->
<NGridItem>
<div class="h-full rounded-xl bg-gray-50/50 p-6">
<div class="mb-6 flex items-center gap-2 text-gray-800 font-bold">
<div class="i-carbon-notebook text-lg text-blue-500" />
<span>常规赛题包</span>
<div class="h-full rounded-2xl bg-[#F5F9FF] p-6">
<div class="mb-6 flex items-center justify-between border-b border-blue-100 pb-4">
<div class="flex items-center gap-2 text-gray-800 font-bold">
<icon-ic-outline-archive class="text-xl text-blue-500" />
<span class="text-lg">常规赛题包</span>
</div>
<div class="text-sm text-[#8DA5C3] font-medium">
{{ regularQuestions.length }} / 总计 {{ regularTotalScore }}
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-5">
<div
v-for="(item, idx) in regularQuestions"
:key="idx"
class="flex items-center gap-4"
class="flex items-center gap-3"
>
<span class="w-12 text-xs text-gray-400 font-medium">{{ idx + 1 }}</span>
<span class="w-12 flex-shrink-0 text-sm text-[#8DA5C3] font-bold">{{ idx + 1 }}</span>
<!-- 题型选择 -->
<NSelect
v-model:value="item.value"
:options="questionOptions"
placeholder="请选择题型"
class="flex-1 bg-white"
class="min-w-0 flex-[2]"
:theme-overrides="{
peers: {
InternalSelection: {
borderRadius: '8px',
color: '#FFFFFF',
border: '1px solid #EBF1F9',
},
},
}"
/>
<!-- 分数 -->
<NSelect
v-model:value="item.score"
:options="scoreOptions"
class="w-20 flex-shrink-0"
:theme-overrides="{
peers: {
InternalSelection: {
borderRadius: '8px',
color: '#FFFFFF',
border: '1px solid #EBF1F9',
},
},
}"
/>
<!-- 答题时间 -->
<div class="flex flex-shrink-0 items-center gap-2">
<span class="text-xs text-gray-400">答题时间:</span>
<NSelect
v-model:value="item.time"
:options="timeOptions"
class="w-22"
:theme-overrides="{
peers: {
InternalSelection: {
borderRadius: '8px',
color: '#FFFFFF',
border: '1px solid #EBF1F9',
},
},
}"
/>
</div>
</div>
</div>
</div>
@ -67,25 +143,76 @@ const pkQuestions = ref([
<!-- 右侧加时赛 (PK环节) -->
<NGridItem>
<div class="h-full border border-orange-100/50 rounded-xl bg-orange-50/30 p-6">
<div class="mb-6 flex items-center gap-2 text-gray-800 font-bold">
<div class="i-carbon-timer text-lg text-orange-500" />
<span>加时赛 (PK环节)</span>
<div class="h-full border border-[#FFF0E0] rounded-2xl bg-[#FFF9F5] p-6">
<div class="mb-6 flex items-center justify-between border-b border-orange-100 pb-4">
<div class="flex items-center gap-2 text-[#5E3218] font-bold">
<icon-ic-outline-timer class="text-xl text-[#D96B23]" />
<span class="text-lg">加时赛题包</span>
</div>
<div class="text-sm text-[#D96B23]/60 font-medium">
{{ pkQuestions.length }} / 总计 {{ pkTotalScore }}
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-5">
<div
v-for="(item, idx) in pkQuestions"
:key="idx"
class="flex items-center gap-4"
class="flex items-center gap-3"
>
<span class="w-8 text-xs text-orange-400 font-medium">PK{{ idx + 1 }}</span>
<span class="w-12 flex-shrink-0 text-sm text-[#FF8C38] font-bold">PK{{ idx + 1 }}</span>
<!-- PK规则 -->
<NSelect
v-model:value="item.value"
:options="pkOptions"
placeholder="请选择PK规则"
class="flex-1 bg-white"
class="min-w-0 flex-[2]"
:theme-overrides="{
peers: {
InternalSelection: {
borderRadius: '8px',
color: '#FFFFFF',
border: '1px solid #FFEDD5',
},
},
}"
/>
<!-- 分数 -->
<NSelect
v-model:value="item.score"
:options="scoreOptions"
class="w-20 flex-shrink-0"
:theme-overrides="{
peers: {
InternalSelection: {
borderRadius: '8px',
color: '#FFFFFF',
border: '1px solid #FFEDD5',
},
},
}"
/>
<!-- 答题时间 -->
<div class="flex flex-shrink-0 items-center gap-2">
<span class="text-xs text-[#D96B23]/60">答题时间:</span>
<NSelect
v-model:value="item.time"
:options="timeOptions"
class="w-22"
:theme-overrides="{
peers: {
InternalSelection: {
borderRadius: '8px',
color: '#FFFFFF',
border: '1px solid #FFEDD5',
},
},
}"
/>
</div>
</div>
</div>
</div>