feat(competition): 新增比赛管理功能并优化UI交互

- 添加比赛创建向导,支持分步配置基础信息、分组管理、硬件绑定和题目配置
- 实现比赛卡片组件,支持复制和删除操作
- 优化比赛详情页的表单交互和样式
- 新增全局组件类型定义和图标
- 添加比赛海报上传功能
- 完善比赛状态管理,新增"已结束"状态
This commit is contained in:
2026-01-21 08:41:36 +08:00
parent e6346a771e
commit 3270e24c25
12 changed files with 923 additions and 315 deletions

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { NButton, NCard, NGrid, NGridItem, NInputNumber, NSelect } from 'naive-ui'
import { NButton, NCard, NForm, NFormItem, NGrid, NGridItem, NInputNumber, NModal, NSelect } from 'naive-ui'
import { computed, ref } from 'vue'
defineProps<{ isEdit: boolean }>()
@ -10,6 +11,70 @@ const scoreOptions = [
{ label: '5分', value: '5' },
{ label: '10分', value: '10' },
]
interface Question {
id: string
label: string
type: string
score: string
time: number | null
}
// 初始化常规赛题目数据
const regularQuestions = ref<Question[]>(Array.from({ length: 10 }, (_, i) => ({
id: `reg-${i}`,
label: `${i + 1}`,
type: 'hanzi',
score: '1',
time: 30,
})))
// 初始化PK赛题目数据
const pkQuestions = ref<Question[]>(Array.from({ length: 5 }, (_, i) => ({
id: `pk-${i}`,
label: `PK${i + 1}`,
type: 'hanzi',
score: '1',
time: 30,
})))
const regularTotalScore = computed(() => regularQuestions.value.reduce((acc, cur) => acc + Number(cur.score || 0), 0))
const pkTotalScore = computed(() => pkQuestions.value.reduce((acc, cur) => acc + Number(cur.score || 0), 0))
// 快速配置相关逻辑
const showQuickConfigModal = ref(false)
const quickConfigForm = ref({
score: '1',
time: 30,
})
function handleBatchReset() {
regularQuestions.value.forEach((q) => {
q.score = '1'
q.time = 30
})
pkQuestions.value.forEach((q) => {
q.score = '1'
q.time = 30
})
}
function handleQuickConfig() {
showQuickConfigModal.value = true
}
function applyQuickConfig() {
const { score, time } = quickConfigForm.value
regularQuestions.value.forEach((q) => {
q.score = score
q.time = time
})
pkQuestions.value.forEach((q) => {
q.score = score
q.time = time
})
showQuickConfigModal.value = false
}
</script>
<template>
@ -20,69 +85,102 @@ const scoreOptions = [
</div>
<!-- 编辑模式下显示批量操作按钮 -->
<div v-if="isEdit" class="flex gap-3">
<NButton size="small" secondary>
<NButton size="small" secondary @click="handleBatchReset">
批量重置
</NButton>
<NButton size="small" type="primary" secondary>
<NButton size="small" type="primary" secondary @click="handleQuickConfig">
快速配置
</NButton>
</div>
</div>
<NGrid :x-gap="24" :cols="2">
<!-- 循环渲染两列常规赛和加时 -->
<NGridItem v-for="type in ['regular', 'pk']" :key="type">
<!-- 头部样式处理 -->
<div
class="flex items-center justify-between border-b rounded-t-lg px-4 py-3"
:class="type === 'regular'
? 'bg-blue-50/50 border-blue-100'
: 'bg-orange-50/50 border-orange-100'"
>
<div class="flex items-center gap-2 font-bold" :class="type === 'regular' ? 'text-gray-800' : 'text-[#5E3218]'">
<div :class="type === 'regular' ? 'i-carbon-document-tasks text-blue-500' : 'i-carbon-timer text-[#D96B23]'" />
{{ type === 'regular' ? '常规赛题包' : '加时赛题包' }}
<!-- 常规 -->
<NGridItem>
<div class="flex items-center justify-between border-b border-blue-100 rounded-t-lg bg-blue-50/50 px-4 py-3">
<div class="flex items-center gap-2 text-gray-800 font-bold">
<div class="i-carbon-document-tasks text-blue-500" />
常规赛题包
</div>
<div class="text-xs font-medium" :class="type === 'regular' ? 'text-[#8DA5C3]' : 'text-[#D96B23]/60'">
{{ type === 'regular' ? 10 : 5 }} / 总计 {{ type === 'regular' ? 10 : 7 }}
<div class="text-xs text-[#8DA5C3] font-medium">
{{ regularQuestions.length }} / 总计 {{ regularTotalScore }}
</div>
</div>
<!-- 列表区域 -->
<div
class="flex flex-col gap-3 border border-t-0 rounded-b-lg p-4"
:class="type === 'regular' ? 'border-blue-100 bg-[#F5F9FF]' : 'border-orange-100 bg-[#FFF9F5]'"
>
<div v-for="i in (type === 'regular' ? 10 : 5)" :key="`${type}-${i}`" class="flex items-center gap-2">
<!-- 题号 -->
<div class="w-16 flex-shrink-0 text-xs font-bold" :class="type === 'regular' ? 'text-[#8DA5C3]' : 'text-[#FF8C38]'">
{{ type === 'regular' ? `${i}` : `PK${i}` }}:
<div class="flex flex-col gap-3 border border-t-0 border-blue-100 rounded-b-lg bg-[#F5F9FF] p-4">
<div v-for="item in regularQuestions" :key="item.id" class="flex items-center gap-2">
<div class="w-16 flex-shrink-0 text-xs text-[#8DA5C3] font-bold">
{{ item.label }}:
</div>
<!-- 题型选择 -->
<NSelect
v-model:value="item.type"
size="small"
placeholder="题型"
default-value="hanzi"
:options="[{ label: '汉字书写', value: 'hanzi' }]"
:disabled="!isEdit"
class="min-w-[120px] flex-1"
/>
<!-- 需求4分数下拉选择 -->
<div class="w-20 flex-shrink-0">
<NSelect
v-model:value="item.score"
size="small"
default-value="1"
:options="scoreOptions"
:disabled="!isEdit"
placeholder="分值"
/>
</div>
<!-- 需求4倒计时输入 -->
<div class="w-20 flex-shrink-0">
<NInputNumber
v-model:value="item.time"
size="small"
placeholder="秒"
:show-button="false"
:disabled="!isEdit"
>
<template #suffix>
s
</template>
</NInputNumber>
</div>
</div>
</div>
</NGridItem>
<!-- PK赛 -->
<NGridItem>
<div class="flex items-center justify-between border-b border-orange-100 rounded-t-lg bg-orange-50/50 px-4 py-3">
<div class="flex items-center gap-2 text-[#5E3218] font-bold">
<div class="i-carbon-timer text-[#D96B23]" />
加时赛题包
</div>
<div class="text-xs text-[#D96B23]/60 font-medium">
{{ pkQuestions.length }} / 总计 {{ pkTotalScore }}
</div>
</div>
<div class="flex flex-col gap-3 border border-t-0 border-orange-100 rounded-b-lg bg-[#FFF9F5] p-4">
<div v-for="item in pkQuestions" :key="item.id" class="flex items-center gap-2">
<div class="w-16 flex-shrink-0 text-xs text-[#FF8C38] font-bold">
{{ item.label }}:
</div>
<NSelect
v-model:value="item.type"
size="small"
placeholder="题型"
:options="[{ label: '汉字书写', value: 'hanzi' }]"
:disabled="!isEdit"
class="min-w-[120px] flex-1"
/>
<div class="w-20 flex-shrink-0">
<NSelect
v-model:value="item.score"
size="small"
:options="scoreOptions"
:disabled="!isEdit"
placeholder="分值"
/>
</div>
<div class="w-20 flex-shrink-0">
<NInputNumber
v-model:value="item.time"
size="small"
placeholder="秒"
:show-button="false"
@ -97,5 +195,36 @@ const scoreOptions = [
</div>
</NGridItem>
</NGrid>
<!-- 快速配置弹窗 -->
<NModal
v-model:show="showQuickConfigModal"
preset="card"
title="快速配置"
class="w-[400px]"
>
<NForm :model="quickConfigForm" label-placement="left" label-width="80">
<NFormItem label="统一分值">
<NSelect v-model:value="quickConfigForm.score" :options="scoreOptions" />
</NFormItem>
<NFormItem label="统一时间">
<NInputNumber v-model:value="quickConfigForm.time" :show-button="false">
<template #suffix>
</template>
</NInputNumber>
</NFormItem>
</NForm>
<template #footer>
<div class="flex justify-end gap-3">
<NButton @click="showQuickConfigModal = false">
取消
</NButton>
<NButton type="primary" @click="applyQuickConfig">
应用
</NButton>
</div>
</template>
</NModal>
</NCard>
</template>