feat(admin): 新增活动管理功能并优化字典组件
- 新增活动管理相关功能,包括活动创建、编辑、删除和状态管理 - 新增 PublishStatus 枚举定义活动发布状态 - 优化字典组件,支持字符串和数字类型的字典值 - 重构业务数据存储逻辑,简化字典数据获取流程 - 新增活动详情页面,支持分组管理和队伍配置 - 集成富文本编辑器用于活动规则编辑 - 优化图片上传组件,支持小图上传模式 - 修复模板管理中的区域选择和分页问题 - 更新 TypeScript 类型定义,完善 API 接口 - 调整主题配置,新增活动状态相关颜色
This commit is contained in:
@ -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>
|
||||
|
||||
|
||||
@ -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">
|
||||
查看配置详情
|
||||
|
||||
Reference in New Issue
Block a user