feat(admin): 新增题库模块并优化比赛管理功能

新增题库模块,包含题目管理页面和Excel导入导出功能
优化比赛管理功能,包括分组管理、硬件绑定和题目配置
重构比赛添加流程,增加重置功能和数据校验
添加Excel工具函数,支持读取和导出Excel文件
更新依赖,添加xlsx库支持
This commit is contained in:
2026-01-21 16:29:38 +08:00
parent 3270e24c25
commit 89773bd49b
33 changed files with 1988 additions and 496 deletions

View File

@ -6,10 +6,11 @@ import CompetitionAddModal from '../competition-add/index.vue'
import CompetitionCard from './modules/competition-card.vue'
import { type CompetitionItem, competitionList } from './modules/data'
// 引入 store 用于判断移动端布局 (与你提供的风格保持一致)
const appStore = useAppStore()
const gap = computed(() => (appStore.isMobile ? 12 : 26))
const competitionListRef = ref(competitionList)
const showAddModal = ref(false)
function handleAdd() {
@ -22,8 +23,43 @@ function handleCopy(item: CompetitionItem) {
}
function handleDelete(item: CompetitionItem) {
// TODO: 实现删除逻辑
window.$message?.success(`已删除活动:${item.title}`)
const d = window.$dialog?.warning({
title: '确认删除',
content: `确定删除活动:${item.title}吗?`,
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
if (d) {
d.loading = true
}
// 模拟接口请求延迟 2秒
await new Promise(resolve => setTimeout(resolve, 2000))
try {
// 模拟 50% 概率失败
if (Math.random() < 0.5) {
throw new Error('模拟服务器异常,删除失败')
}
// 成功逻辑
const index = competitionListRef.value.findIndex(i => i.id === item.id)
if (index > -1) {
competitionListRef.value.splice(index, 1)
window.$message?.success(`已删除活动:${item.title}`)
}
return true // 返回 true 关闭弹窗
}
catch (error: any) {
window.$message?.error(error.message)
return false // 返回 false 阻止关闭弹窗
}
finally {
if (d) {
d.loading = false
}
}
},
})
}
</script>
@ -40,9 +76,9 @@ function handleDelete(item: CompetitionItem) {
</p>
</div>
<NButton type="primary" size="medium" class="px-6 font-medium" @click="handleAdd">
<NButton type="primary" size="large" class="px-6 font-medium" @click="handleAdd">
<template #icon>
<div class="i-carbon-add text-lg" />
<icon-ic-outline-add-to-photos class="text-icon" />
</template>
新增比赛活动
</NButton>
@ -51,7 +87,7 @@ function handleDelete(item: CompetitionItem) {
<!-- 卡片列表区域 -->
<!-- 响应式布局手机1列平板2列中屏3列大屏4列 -->
<NGrid :x-gap="gap" :y-gap="gap" responsive="screen" item-responsive>
<NGi v-for="item in competitionList" :key="item.id" span="24 s:12 m:8 l:6">
<NGi v-for="item in competitionListRef" :key="item.id" span="24 s:12 m:8 l:6">
<CompetitionCard :item="item" @copy="handleCopy" @delete="handleDelete" />
</NGi>
</NGrid>

View File

@ -80,10 +80,7 @@ const decorationBgClass = computed(() => {
<NTooltip>
<template #trigger>
<NButton
circle
secondary
size="small"
class="shadow-sm backdrop-blur-sm !bg-white/80 hover:!bg-white"
circle secondary size="small" class="shadow-sm backdrop-blur-sm !bg-white/80 hover:!bg-white"
@click.stop="emit('copy', item)"
>
<template #icon>
@ -97,12 +94,8 @@ const decorationBgClass = computed(() => {
<NTooltip>
<template #trigger>
<NButton
circle
secondary
size="small"
type="error"
class="shadow-sm backdrop-blur-sm !bg-white/80 hover:!bg-red-50"
@click.stop="emit('delete', item)"
circle secondary size="small" type="error"
class="shadow-sm backdrop-blur-sm !bg-white/80 hover:!bg-red-50" @click.stop="emit('delete', item)"
>
<template #icon>
<icon-ic-baseline-delete class="text-icon" />
@ -132,7 +125,7 @@ const decorationBgClass = computed(() => {
<!-- 日期 -->
<div class="mb-6 flex items-center text-gray-400">
<icon-ic-baseline-calendar-month class="mr-2 text-xl" />
<span class="text-base font-bold">{{ item.date }}</span>
<span class="text-base font-bold">{{ item.date }} {{ item.endDate }}</span>
</div>
</div>

View File

@ -4,6 +4,7 @@ export interface CompetitionItem {
id: string
title: string
date: string
endDate: string
teamCount: number
status: 'ongoing' | 'pending' | 'ended' // ongoing: 进行中, pending: 未开始, ended: 已结束
isHighlight?: boolean // 用于标记第一个卡片的特殊背景
@ -14,6 +15,7 @@ export const competitionList: CompetitionItem[] = [
id: '1',
title: '阅读之星大赛\n辞海遨游环节', // 使用换行符或在组件中处理换行
date: '2026.01.12',
endDate: '2026.01.12',
teamCount: 40,
status: 'ongoing',
isHighlight: true,
@ -22,6 +24,7 @@ export const competitionList: CompetitionItem[] = [
id: '2',
title: '诗词大会总决赛',
date: '2026.01.12',
endDate: '2026.01.12',
teamCount: 32,
status: 'pending',
},
@ -29,6 +32,7 @@ export const competitionList: CompetitionItem[] = [
id: '3',
title: '趣味百科大作战',
date: '2026.01.12',
endDate: '2026.01.12',
teamCount: 32,
status: 'ended',
},
@ -36,6 +40,7 @@ export const competitionList: CompetitionItem[] = [
id: '4',
title: '经典文学研读周',
date: '2026.01.12',
endDate: '2026.01.12',
teamCount: 32,
status: 'pending',
},
@ -43,6 +48,7 @@ export const competitionList: CompetitionItem[] = [
id: '5',
title: '未来科学家阅读赛',
date: '2026.01.12',
endDate: '2026.01.12',
teamCount: 32,
status: 'pending',
},
@ -50,6 +56,7 @@ export const competitionList: CompetitionItem[] = [
id: '6',
title: '历史人文知识赛',
date: '2026.01.12',
endDate: '2026.01.12',
teamCount: 32,
status: 'pending',
},
@ -57,6 +64,7 @@ export const competitionList: CompetitionItem[] = [
id: '7',
title: '少儿绘本创意营',
date: '2026.01.12',
endDate: '2026.01.12',
teamCount: 32,
status: 'pending',
},
@ -64,6 +72,7 @@ export const competitionList: CompetitionItem[] = [
id: '8',
title: '唐诗三百首挑战',
date: '2026.01.12',
endDate: '2026.01.12',
teamCount: 32,
status: 'pending',
},