feat(competition): 新增比赛管理功能并优化UI交互
- 添加比赛创建向导,支持分步配置基础信息、分组管理、硬件绑定和题目配置 - 实现比赛卡片组件,支持复制和删除操作 - 优化比赛详情页的表单交互和样式 - 新增全局组件类型定义和图标 - 添加比赛海报上传功能 - 完善比赛状态管理,新增"已结束"状态
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from '@sa/utils'
|
||||
import { NCard, NDatePicker, NForm, NFormItemGi, NGrid, NInput, NInputNumber } from 'naive-ui'
|
||||
import { NButton, NDatePicker, NForm, NInput, NInputNumber, NUpload, type UploadCustomRequestOptions } from 'naive-ui'
|
||||
import { ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -10,6 +10,7 @@ const props = defineProps<{
|
||||
endTime: number | null
|
||||
groupCount: number
|
||||
teamCount: number
|
||||
poster: string
|
||||
}
|
||||
}>()
|
||||
|
||||
@ -35,60 +36,274 @@ watch(formData, (val) => {
|
||||
handleUpdate(val)
|
||||
}, { deep: true })
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
formData,
|
||||
})
|
||||
function updateCount(type: 'group' | 'team', delta: number) {
|
||||
if (type === 'group') {
|
||||
const newVal = (formData.value.groupCount || 0) + delta
|
||||
if (newVal >= 1 && newVal <= 999) {
|
||||
formData.value.groupCount = newVal
|
||||
}
|
||||
}
|
||||
else {
|
||||
const newVal = (formData.value.teamCount || 0) + delta
|
||||
if (newVal >= 1 && newVal <= 999) {
|
||||
formData.value.teamCount = newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟上传处理
|
||||
function handleUpload({ file, onFinish }: UploadCustomRequestOptions) {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file.file as File)
|
||||
reader.onload = () => {
|
||||
// 模拟上传成功,直接使用 base64 作为图片地址
|
||||
formData.value.poster = reader.result as string
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
|
||||
// 校验方法
|
||||
function validate() {
|
||||
if (!formData.value.name) {
|
||||
window.$message?.error('请输入比赛项目名称')
|
||||
return false
|
||||
}
|
||||
if (!formData.value.startTime) {
|
||||
window.$message?.error('请选择开始时间')
|
||||
return false
|
||||
}
|
||||
if (!formData.value.endTime) {
|
||||
window.$message?.error('请选择结束时间')
|
||||
return false
|
||||
}
|
||||
if (formData.value.startTime >= formData.value.endTime) {
|
||||
window.$message?.error('结束时间必须晚于开始时间')
|
||||
return false
|
||||
}
|
||||
if (!formData.value.groupCount || formData.value.groupCount < 1) {
|
||||
window.$message?.error('参赛小组数量必须为正整数')
|
||||
return false
|
||||
}
|
||||
if (!formData.value.teamCount || formData.value.teamCount < 1) {
|
||||
window.$message?.error('每组队伍数量必须为正整数')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" class="rounded-xl shadow-sm">
|
||||
<div class="mb-4 flex items-center gap-2 border-l-4 border-blue-600 pl-3">
|
||||
<span class="text-base font-bold">基础活动信息</span>
|
||||
<div class="h-full flex gap-12">
|
||||
<!-- 左侧表单区域 -->
|
||||
<div class="flex-1">
|
||||
<div class="mb-12">
|
||||
<h2 class="mb-4 text-4xl text-gray-800 font-bold">
|
||||
第一步: 基础定义
|
||||
</h2>
|
||||
<p class="text-lg text-gray-400 font-bold">
|
||||
请确认比赛名称与参与规模。这将作为系统自动生成分组与终端连接数的依据。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<NForm ref="formRef" label-placement="top" :show-feedback="false">
|
||||
<div class="mb-10">
|
||||
<div class="mb-3 text-sm text-[#8DA5C3] font-bold">
|
||||
比赛项目名称
|
||||
</div>
|
||||
<NInput
|
||||
v-model:value="formData.name" placeholder="例如:2026年度阅读之星-辞海遨游总决赛"
|
||||
class="h-16 rounded-2xl border-none text-xl line-height-16 shadow-[0_2px_10px_rgba(0,0,0,0.02)] !bg-[#FCFCFC]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 mb-10 gap-8">
|
||||
<div>
|
||||
<div class="mb-3 text-sm text-[#8DA5C3] font-bold">
|
||||
开始时间
|
||||
</div>
|
||||
<NDatePicker
|
||||
v-model:value="formData.startTime" type="datetime" clearable class="w-full"
|
||||
placeholder="选择开始时间" :theme-overrides="{
|
||||
peers: {
|
||||
Input: {
|
||||
borderRadius: '1rem',
|
||||
color: '#FCFCFC',
|
||||
heightLarge: '4rem',
|
||||
fontSizeLarge: '1.25rem',
|
||||
textColor: '#1f2937',
|
||||
borderHover: 'none',
|
||||
borderFocus: 'none',
|
||||
},
|
||||
},
|
||||
}" size="large"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-3 text-sm text-[#8DA5C3] font-bold">
|
||||
结束时间
|
||||
</div>
|
||||
<NDatePicker
|
||||
v-model:value="formData.endTime" type="datetime" clearable class="w-full" placeholder="选择结束时间"
|
||||
:theme-overrides="{
|
||||
peers: {
|
||||
Input: {
|
||||
borderRadius: '1rem',
|
||||
color: '#FCFCFC',
|
||||
heightLarge: '4rem',
|
||||
fontSizeLarge: '1.25rem',
|
||||
textColor: '#1f2937',
|
||||
borderHover: 'none',
|
||||
borderFocus: 'none',
|
||||
},
|
||||
},
|
||||
}" size="large"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-8">
|
||||
<!-- 参赛小组数量卡片 -->
|
||||
<div class="rounded-3xl bg-[#F5F8FF] p-8">
|
||||
<div class="mb-6 text-sm text-[#3B82F6] font-bold tracking-wide uppercase">
|
||||
参赛小组总数 (GROUPS)
|
||||
</div>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<button
|
||||
class="h-14 w-14 flex items-center justify-center rounded-full bg-white text-3xl text-[#3B82F6] shadow-md transition-transform active:scale-95 hover:bg-gray-50"
|
||||
@click="updateCount('group', -1)"
|
||||
>
|
||||
<div class="mb-1 font-bold">
|
||||
-
|
||||
</div>
|
||||
</button>
|
||||
<div class="w-32">
|
||||
<NInputNumber
|
||||
v-model:value="formData.groupCount" :min="1" :max="999" :show-button="false"
|
||||
class="text-center" :theme-overrides="{
|
||||
peers: {
|
||||
Input: {
|
||||
textColor: '#1f2937',
|
||||
fontSizeMedium: '3rem',
|
||||
heightMedium: '4rem',
|
||||
color: 'transparent',
|
||||
caretColor: '#3B82F6',
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="h-14 w-14 flex items-center justify-center rounded-full bg-white text-3xl text-[#3B82F6] shadow-md transition-transform active:scale-95 hover:bg-gray-50"
|
||||
@click="updateCount('group', 1)"
|
||||
>
|
||||
<div class="mb-1 font-bold">
|
||||
+
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-center text-sm text-[#3B82F6]/60 font-medium italic">
|
||||
系统将预设 {{ formData.groupCount }} 个队伍
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 每组队伍数量卡片 -->
|
||||
<div class="rounded-3xl bg-[#F5F8FF] p-8">
|
||||
<div class="mb-6 text-sm text-[#3B82F6] font-bold tracking-wide uppercase">
|
||||
单组包含队伍 (TEAMS)
|
||||
</div>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<button
|
||||
class="h-14 w-14 flex items-center justify-center rounded-full bg-white text-3xl text-[#3B82F6] shadow-md transition-transform active:scale-95 hover:bg-gray-50"
|
||||
@click="updateCount('team', -1)"
|
||||
>
|
||||
<div class="mb-1 font-bold">
|
||||
-
|
||||
</div>
|
||||
</button>
|
||||
<div class="w-32">
|
||||
<NInputNumber
|
||||
v-model:value="formData.teamCount" :min="1" :max="999" :show-button="false"
|
||||
class="text-center" :theme-overrides="{
|
||||
peers: {
|
||||
Input: {
|
||||
textColor: '#1f2937',
|
||||
fontSizeMedium: '3rem',
|
||||
heightMedium: '4rem',
|
||||
color: 'transparent',
|
||||
caretColor: '#3B82F6',
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="h-14 w-14 flex items-center justify-center rounded-full bg-white text-3xl text-[#3B82F6] shadow-md transition-transform active:scale-95 hover:bg-gray-50"
|
||||
@click="updateCount('team', 1)"
|
||||
>
|
||||
<div class="mb-1 font-bold">
|
||||
+
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-center text-sm text-[#3B82F6]/60 font-medium italic">
|
||||
每组将有 {{ formData.teamCount }} 支队伍同时进行比赛
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NForm>
|
||||
</div>
|
||||
|
||||
<NForm ref="formRef" label-placement="top" :show-feedback="false">
|
||||
<NGrid :x-gap="24" :y-gap="24" cols="1 s:2 m:4 l:5" responsive="screen">
|
||||
<NFormItemGi span="1" label="比赛名称">
|
||||
<NInput v-model:value="formData.name" placeholder="请输入比赛名称" />
|
||||
</NFormItemGi>
|
||||
|
||||
<NFormItemGi label="开始时间">
|
||||
<NDatePicker
|
||||
v-model:value="formData.startTime" type="datetime" clearable class="w-full"
|
||||
placeholder="年 / 月 / 日 --:--"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
|
||||
<NFormItemGi label="结束时间">
|
||||
<NDatePicker
|
||||
v-model:value="formData.endTime" type="datetime" clearable class="w-full"
|
||||
placeholder="年 / 月 / 日 --:--"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
|
||||
<NFormItemGi label="参赛队伍总数">
|
||||
<NInputNumber
|
||||
v-model:value="formData.groupCount"
|
||||
class="w-full"
|
||||
:min="1"
|
||||
:max="999"
|
||||
:precision="0"
|
||||
/>
|
||||
<span class="text-xs text-gray-400">(对)</span>
|
||||
</NFormItemGi>
|
||||
|
||||
<NFormItemGi label="单组队伍">
|
||||
<NInputNumber
|
||||
v-model:value="formData.teamCount"
|
||||
class="w-full"
|
||||
:min="1"
|
||||
:max="formData.groupCount"
|
||||
:precision="0"
|
||||
/>
|
||||
<span class="text-xs text-gray-400">(对)</span>
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
</NCard>
|
||||
<!-- 右侧海报上传区域 -->
|
||||
<div class="w-99 flex flex-col pt-32">
|
||||
<div class="mb-3 w-full text-sm text-[#8DA5C3] font-bold">
|
||||
活动海报
|
||||
<NAlert type="info" class="mt-2 text-xs text-[#616263] font-normal">
|
||||
<span class="text-xs font-normal">
|
||||
建议上传 16:9 或 4:3 比例的图片,以获得最佳的全屏显示效果。
|
||||
</span>
|
||||
</NAlert>
|
||||
</div>
|
||||
<NUpload accept="image/*" :show-file-list="false" :custom-request="handleUpload" class="block w-full">
|
||||
<div
|
||||
class="relative h-[400px] w-full flex flex-col cursor-pointer items-center justify-center overflow-hidden rounded-3xl bg-[#F5F8FF] transition-all hover:bg-gray-100"
|
||||
:class="{ 'border-2 border-dashed border-gray-300': !formData.poster }"
|
||||
>
|
||||
<div
|
||||
v-if="formData.poster"
|
||||
class="group absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity hover:opacity-100"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<!-- NUpload 的 trigger 会自动处理点击事件,这里不需要额外的上传逻辑,只需要一个按钮作为视觉触发 -->
|
||||
<NButton ghost color="#fff" size="small">
|
||||
更换
|
||||
</NButton>
|
||||
<NButton ghost color="#ff4d4f" size="small" @click.stop="formData.poster = ''">
|
||||
删除
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<img v-if="formData.poster" :src="formData.poster" class="h-full w-full object-cover" alt="Poster">
|
||||
<div v-else class="flex flex-col items-center text-gray-400">
|
||||
<div class="i-carbon-add-filled mb-4 text-4xl text-[#3B82F6]" />
|
||||
<span class="text-sm font-bold">点击上传海报</span>
|
||||
<span class="mt-2 text-xs opacity-60">支持 JPG/PNG 格式</span>
|
||||
</div>
|
||||
</div>
|
||||
</NUpload>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-input-number .n-input__input-el) {
|
||||
text-align: center;
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:deep(.n-upload-trigger) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user