feat(competition): 新增比赛管理功能并优化UI交互
- 添加比赛创建向导,支持分步配置基础信息、分组管理、硬件绑定和题目配置 - 实现比赛卡片组件,支持复制和删除操作 - 优化比赛详情页的表单交互和样式 - 新增全局组件类型定义和图标 - 添加比赛海报上传功能 - 完善比赛状态管理,新增"已结束"状态
This commit is contained in:
@ -1,19 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NGi, NGrid, NSpace } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import CompetitionAddModal from '../competition-add/index.vue'
|
||||
import CompetitionCard from './modules/competition-card.vue'
|
||||
import { competitionList } from './modules/data'
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
import { type CompetitionItem, competitionList } from './modules/data'
|
||||
|
||||
// 引入 store 用于判断移动端布局 (与你提供的风格保持一致)
|
||||
const appStore = useAppStore()
|
||||
const gap = computed(() => (appStore.isMobile ? 12 : 16))
|
||||
const gap = computed(() => (appStore.isMobile ? 12 : 26))
|
||||
|
||||
const showAddModal = ref(false)
|
||||
|
||||
function handleAdd() {
|
||||
routerPushByKey('competition_competition-add')
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
function handleCopy(item: CompetitionItem) {
|
||||
// TODO: 实现复制逻辑
|
||||
window.$message?.success(`已复制活动:${item.title}`)
|
||||
}
|
||||
|
||||
function handleDelete(item: CompetitionItem) {
|
||||
// TODO: 实现删除逻辑
|
||||
window.$message?.success(`已删除活动:${item.title}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -42,9 +52,11 @@ function handleAdd() {
|
||||
<!-- 响应式布局:手机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">
|
||||
<CompetitionCard :item="item" />
|
||||
<CompetitionCard :item="item" @copy="handleCopy" @delete="handleDelete" />
|
||||
</NGi>
|
||||
</NGrid>
|
||||
|
||||
<CompetitionAddModal v-model:show="showAddModal" />
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { CompetitionItem } from './data'
|
||||
import { NButton, NCard, NTag } from 'naive-ui'
|
||||
import { NButton, NCard, NTag, NTooltip } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
import { useRouterPush } from '@/hooks/common/router'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'copy', item: CompetitionItem): void
|
||||
(e: 'delete', item: CompetitionItem): void
|
||||
}>()
|
||||
|
||||
const { routerPushByKey } = useRouterPush()
|
||||
|
||||
interface Props {
|
||||
@ -17,6 +22,9 @@ const statusConfig = computed(() => {
|
||||
if (props.item.status === 'ongoing') {
|
||||
return { type: 'success', text: '进行中', bgClass: 'bg-green-100 text-green-600' }
|
||||
}
|
||||
if (props.item.status === 'ended') {
|
||||
return { type: 'danger', text: '已结束', bgClass: 'bg-red-100 text-red-600' }
|
||||
}
|
||||
return { type: 'default', text: '未开始', bgClass: 'bg-gray-100 text-gray-500' }
|
||||
})
|
||||
|
||||
@ -26,20 +34,85 @@ const formattedTitle = computed(() => props.item.title.replace(/\n/g, '<br/>'))
|
||||
function toDetail(item: CompetitionItem) {
|
||||
routerPushByKey('competition_competition-detail', { query: { id: item.id } })
|
||||
}
|
||||
|
||||
// 随机背景色池
|
||||
const bgColors = [
|
||||
'bg-blue-50/80',
|
||||
'bg-green-50/80',
|
||||
'bg-purple-50/80',
|
||||
'bg-orange-50/80',
|
||||
'bg-red-50/80',
|
||||
'bg-teal-50/80',
|
||||
'bg-indigo-50/80',
|
||||
'bg-pink-50/80',
|
||||
'bg-yellow-50/80',
|
||||
'bg-cyan-50/80',
|
||||
]
|
||||
|
||||
// 基于 ID 生成确定性的随机颜色
|
||||
const decorationBgClass = computed(() => {
|
||||
const idStr = String(props.item.id || '')
|
||||
let hash = 0
|
||||
for (let i = 0; i < idStr.length; i++) {
|
||||
hash = idStr.charCodeAt(i) + ((hash << 5) - hash)
|
||||
}
|
||||
const index = Math.abs(hash) % bgColors.length
|
||||
return bgColors[index]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard
|
||||
:bordered="false"
|
||||
class="group relative h-full overflow-hidden rounded-2xl transition-all duration-300 hover:shadow-lg"
|
||||
content-style="padding: 24px; display: flex; flex-direction: column; height: 100%;" @click="toDetail(item)"
|
||||
class="group relative h-full overflow-hidden border border-transparent rounded-2xl transition-all duration-300 hover:scale-[1.02] hover:border-blue-500 hover:shadow-xl hover:-translate-y-2"
|
||||
content-style="padding: 34px; display: flex; flex-direction: column; height: 100%;" @click="toDetail(item)"
|
||||
>
|
||||
<!-- 特殊背景装饰 (仅高亮卡片显示) -->
|
||||
<!-- 特殊背景装饰 (所有卡片显示,随机颜色) -->
|
||||
<div
|
||||
v-if="item.isHighlight"
|
||||
class="pointer-events-none absolute right-0 top-0 z-0 h-32 w-32 rounded-bl-full bg-blue-50/80 -mr-8 -mt-8"
|
||||
class="pointer-events-none absolute right-0 top-0 z-0 h-32 w-32 rounded-bl-full transition-transform duration-800 ease-out -mr-6 -mt-8 group-hover:scale-150"
|
||||
:class="decorationBgClass"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮 (Hover 显示) -->
|
||||
<div
|
||||
class="absolute right-4 top-4 z-20 flex gap-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
circle
|
||||
secondary
|
||||
size="small"
|
||||
class="shadow-sm backdrop-blur-sm !bg-white/80 hover:!bg-white"
|
||||
@click.stop="emit('copy', item)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-ic-baseline-content-copy class="text-icon" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
复制活动
|
||||
</NTooltip>
|
||||
|
||||
<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)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-ic-baseline-delete class="text-icon" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
删除活动
|
||||
</NTooltip>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 h-full flex flex-col justify-between">
|
||||
<!-- 顶部内容 -->
|
||||
<div>
|
||||
@ -57,15 +130,16 @@ function toDetail(item: CompetitionItem) {
|
||||
/>
|
||||
|
||||
<!-- 日期 -->
|
||||
<div class="mb-6 flex items-center text-sm text-gray-400">
|
||||
<div class="i-carbon-calendar mr-2 text-base" />
|
||||
<span>{{ item.date }}</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部内容 -->
|
||||
<div class="flex items-center justify-between border-t border-gray-50 pt-4 dark:border-gray-800">
|
||||
<span class="text-xs text-gray-500 font-medium">
|
||||
<span class="flex items-center text-xs text-gray-500 font-medium">
|
||||
<icon-ic-baseline-group class="mr-2 text-xl" />
|
||||
{{ item.teamCount }} 支参赛队伍
|
||||
</span>
|
||||
<NButton text type="primary" size="small" class="font-bold hover:underline">
|
||||
|
||||
@ -5,7 +5,7 @@ export interface CompetitionItem {
|
||||
title: string
|
||||
date: string
|
||||
teamCount: number
|
||||
status: 'ongoing' | 'pending' // ongoing: 进行中, pending: 未开始
|
||||
status: 'ongoing' | 'pending' | 'ended' // ongoing: 进行中, pending: 未开始, ended: 已结束
|
||||
isHighlight?: boolean // 用于标记第一个卡片的特殊背景
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ export const competitionList: CompetitionItem[] = [
|
||||
title: '趣味百科大作战',
|
||||
date: '2026.01.12',
|
||||
teamCount: 32,
|
||||
status: 'pending',
|
||||
status: 'ended',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
|
||||
Reference in New Issue
Block a user