feat(admin): 新增活动管理功能并优化字典组件

- 新增活动管理相关功能,包括活动创建、编辑、删除和状态管理
- 新增 PublishStatus 枚举定义活动发布状态
- 优化字典组件,支持字符串和数字类型的字典值
- 重构业务数据存储逻辑,简化字典数据获取流程
- 新增活动详情页面,支持分组管理和队伍配置
- 集成富文本编辑器用于活动规则编辑
- 优化图片上传组件,支持小图上传模式
- 修复模板管理中的区域选择和分页问题
- 更新 TypeScript 类型定义,完善 API 接口
- 调整主题配置,新增活动状态相关颜色
This commit is contained in:
2026-02-04 18:01:52 +08:00
parent e1a19c48e0
commit ce5ee2a289
39 changed files with 2621 additions and 693 deletions

View File

@ -1,15 +1,25 @@
<script setup lang="ts">
// import type { CompetitionItem } from './modules/data'
import { NButton, NGi, NGrid, NSpace } from 'naive-ui'
import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { fetchDeleteActivity, fetchGetActivityList } from '@/service/api/competition'
import { useAppStore } from '@/store/modules/app'
import CompetitionAddModal from '../competition-add/index.vue'
import CompetitionCard from './modules/competition-card.vue'
import { type CompetitionItem, competitionList } from './modules/data'
export interface CompetitionItem {
Id: number
Name: string
StartTime: string
EndTime: string
TeamGroupNumber: number
PublishStatus: number
}
const appStore = useAppStore()
const gap = computed(() => (appStore.isMobile ? 12 : 26))
const competitionListRef = ref(competitionList)
const competitionListRef = ref<CompetitionItem[]>([])
const showAddModal = ref(false)
@ -19,39 +29,36 @@ function handleAdd() {
function handleCopy(item: CompetitionItem) {
// TODO: 实现复制逻辑
window.$message?.success(`已复制活动:${item.title}`)
window.$message?.success(`已复制活动:${item.Name}`)
}
function handleDelete(item: CompetitionItem) {
const d = window.$dialog?.warning({
title: '确认删除',
content: `确定删除活动:${item.title}吗?`,
content: `确定删除活动:${item.Name}吗?`,
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 { error } = await fetchDeleteActivity(item.Id)
if (error) {
window.$message?.error(error.message || '删除失败')
return false
}
// 成功逻辑
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 关闭弹窗
window.$message?.success(`已删除活动:${item.Name}`)
// 刷新列表
fetchActivityList()
return true
}
catch (error: any) {
window.$message?.error(error.message)
return false // 返回 false 阻止关闭弹窗
return false
}
finally {
if (d) {
@ -61,6 +68,21 @@ function handleDelete(item: CompetitionItem) {
},
})
}
/** 获取活动列表 */
async function fetchActivityList() {
try {
const { response, error } = await fetchGetActivityList()
if (!error) {
competitionListRef.value = response?.data?.data || []
}
}
catch (e) {
console.error(e)
}
}
onMounted(() => fetchActivityList())
</script>
<template>
@ -87,12 +109,12 @@ function handleDelete(item: CompetitionItem) {
<!-- 卡片列表区域 -->
<!-- 响应式布局手机1列平板2列中屏3列大屏4列 -->
<NGrid :x-gap="gap" :y-gap="gap" responsive="screen" item-responsive>
<NGi v-for="item in competitionListRef" :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>
<CompetitionAddModal v-model:show="showAddModal" />
<CompetitionAddModal v-model:show="showAddModal" @success="fetchActivityList" />
</NSpace>
</template>

View File

@ -1,7 +1,9 @@
<script setup lang="ts">
import type { CompetitionItem } from './data'
import type { CompetitionItem } from '../index.vue'
import { NButton, NCard, NTag, NTooltip } from 'naive-ui'
import { computed } from 'vue'
import { PublishStatus } from '@/enum/business'
import { useDict } from '@/hooks/business/useDict'
import { useRouterPush } from '@/hooks/common/router'
const props = defineProps<Props>()
@ -11,6 +13,8 @@ const emit = defineEmits<{
(e: 'delete', item: CompetitionItem): void
}>()
const { options: publishStatusOptions } = useDict('publish_status')
const { routerPushByKey } = useRouterPush()
interface Props {
@ -19,20 +23,28 @@ interface Props {
// 状态配置:颜色和文本
const statusConfig = computed(() => {
if (props.item.status === 'ongoing') {
return { type: 'success', text: '进行中', bgClass: 'bg-green-100 text-green-600' }
const status = props.item.PublishStatus
const label = publishStatusOptions.value?.find((item: any) => item.value === status)?.label || ''
switch (status) {
case PublishStatus.Unpublished:
return { type: 'default', text: label, bgClass: 'bg-unpublished-100 text-unpublished-600' }
case PublishStatus.Published:
return { type: 'primary', text: label, bgClass: 'bg-published-100 text-published-600' }
case PublishStatus.Processing:
return { type: 'success', text: label, bgClass: 'bg-processing-100 text-processing-600' }
case PublishStatus.Finished:
return { type: 'error', text: label, bgClass: 'bg-finished-100 text-finished-600' }
default:
return { type: 'default', text: label, bgClass: 'bg-unpublished-100 text-unpublished-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' }
})
// 处理标题换行
const formattedTitle = computed(() => props.item.title.replace(/\n/g, '<br/>'))
// const formattedTitle = computed(() => props.item.title.replace(/\n/g, '<br/>'))
function toDetail(item: CompetitionItem) {
routerPushByKey('competition_competition-detail', { query: { id: item.id } })
routerPushByKey('competition_competition-detail', { query: { Id: item.Id } })
}
// 随机背景色池
@ -47,11 +59,12 @@ const bgColors = [
'bg-pink-50/80',
'bg-yellow-50/80',
'bg-cyan-50/80',
'bg-pink-50/80',
]
// 基于 ID 生成确定性的随机颜色
const decorationBgClass = computed(() => {
const idStr = String(props.item.id || '')
const idStr = String(props.item.Id || '')
let hash = 0
for (let i = 0; i < idStr.length; i++) {
hash = idStr.charCodeAt(i) + ((hash << 5) - hash)
@ -119,13 +132,13 @@ const decorationBgClass = computed(() => {
<!-- 标题 -->
<h3
class="mb-3 min-h-[3rem] text-lg text-gray-800 font-bold leading-tight dark:text-white"
v-html="formattedTitle"
v-html="item.Name"
/>
<!-- 日期 -->
<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 }} {{ item.endDate }}</span>
<span class="text-base font-bold">{{ item.StartTime }} {{ item.EndTime }}</span>
</div>
</div>
@ -133,7 +146,7 @@ const decorationBgClass = computed(() => {
<div class="flex items-center justify-between border-t border-gray-50 pt-4 dark:border-gray-800">
<span class="flex items-center text-xs text-gray-500 font-medium">
<icon-ic-baseline-group class="mr-2 text-xl" />
{{ item.teamCount }} 支参赛队伍
{{ item.TeamGroupNumber }} 支参赛队伍
</span>
<NButton text type="primary" size="small" class="font-bold hover:underline">
查看配置详情