Files
reader-star/apps/admin/src/views/competition/competition-detail/modules/BasicInfoCard.vue
rjl ce5ee2a289 feat(admin): 新增活动管理功能并优化字典组件
- 新增活动管理相关功能,包括活动创建、编辑、删除和状态管理
- 新增 PublishStatus 枚举定义活动发布状态
- 优化字典组件,支持字符串和数字类型的字典值
- 重构业务数据存储逻辑,简化字典数据获取流程
- 新增活动详情页面,支持分组管理和队伍配置
- 集成富文本编辑器用于活动规则编辑
- 优化图片上传组件,支持小图上传模式
- 修复模板管理中的区域选择和分页问题
- 更新 TypeScript 类型定义,完善 API 接口
- 调整主题配置,新增活动状态相关颜色
2026-02-04 18:01:52 +08:00

289 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { NCard, NDatePicker, NGrid, NGridItem, NInput, NInputNumber } from 'naive-ui'
import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import OssImageUpload from '@/components/common/oss-image-upload/index.vue'
import { PublishStatus } from '@/enum/business'
import '@wangeditor/editor/dist/css/style.css'
const props = defineProps<{
isEdit: boolean
baseInfo: Api.Competition.ActivityDetail
publishStatus: number
}>()
const emit = defineEmits(['enterEdit', 'save', 'cancel'])
const form = ref<Api.Competition.ActivityDetail>({
Id: 0,
RoomID: 0,
Name: '',
StartTime: '',
EndTime: '',
Teams: 0,
TeamGroupNumber: 0,
PublishStatus: 0,
BackgroundImg: '',
CreatedTime: '',
ActivityTitle: '',
ActivityContent: '',
ExtraTimeContent: '',
})
// Editor Logic
const editorRef = shallowRef()
const mode = 'default'
const toolbarConfig = {
excludeKeys: ['group-video', 'insertVideo', 'uploadVideo'],
}
const editorConfig = { placeholder: '请输入具体的比赛参与规则及计分标准...' }
const activeTab = ref('normal') // normal or extra
function handleCreated(editor: any) {
editorRef.value = editor
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null)
return
editor.destroy()
})
/** 同步父组件基础信息到本地表单 */
watch(
() => props.baseInfo,
(val) => {
if (val) {
form.value = { ...val }
}
},
{ immediate: true, deep: true },
)
/** 日期范围计算属性 */
const dateRange = computed({
get() {
if (form.value.StartTime && form.value.EndTime) {
return [form.value.StartTime, form.value.EndTime] as [string, string]
}
return null
},
set(val: [string, string] | null) {
if (val) {
form.value.StartTime = val[0]
form.value.EndTime = val[1]
}
else {
form.value.StartTime = ''
form.value.EndTime = ''
}
},
})
/** 预览模式下的日期文案 */
const previewDateText = computed(() => {
if (!form.value.StartTime || !form.value.EndTime) {
return { start: '-', end: '-' }
}
const fmt = (str: string) => str.slice(0, 10)
return { start: fmt(form.value.StartTime), end: fmt(form.value.EndTime) }
})
const activeContent = computed(() => {
return activeTab.value === 'normal' ? form.value.ActivityContent : form.value.ExtraTimeContent
})
/** 是否显示编辑按钮 */
const showEnterEditButton = computed(() => {
return !props.isEdit && [PublishStatus.Unpublished, PublishStatus.Published].includes(props.publishStatus)
})
/** 是否显示保存/取消按钮 */
const showEditActions = computed(() => props.isEdit)
defineExpose({ form })
</script>
<template>
<div class="flex flex-col gap-6">
<NCard :bordered="false" class="rounded-xl shadow-sm">
<div class="mb-6 flex items-center justify-between border-l-4 border-blue-600 pl-3">
<span class="text-lg text-gray-800 font-bold">基础信息</span>
<div class="flex gap-2">
<NButton v-if="showEnterEditButton" size="small" type="primary" secondary @click="emit('enterEdit')">
编辑
</NButton>
<template v-else-if="showEditActions">
<NButton size="small" secondary @click="emit('cancel')">
取消
</NButton>
<NButton size="small" type="primary" @click="emit('save')">
保存
</NButton>
</template>
</div>
</div>
<!-- 赛事封面 -->
<div class="mb-6">
<div class="mb-2 text-xs text-gray-400 font-medium">
赛事封面
</div>
<div v-if="isEdit">
<OssImageUpload v-model="form.BackgroundImg" :max-size="5120" />
<div class="mt-2 text-xs text-gray-400">
建议上传 16:9 4:3 比例的图片以获得最佳的全屏显示效果
</div>
</div>
<div v-else class="w-full overflow-hidden border border-gray-100 rounded-3xl bg-gray-50">
<div v-if="form.BackgroundImg" class="h-[200px] w-full flex items-center justify-center">
<!-- <img :src="form.BackgroundImg" class="h-full w-full object-cover"> -->
<NImage width="100%" height="100%" :src="form.BackgroundImg" object-fit="contain" />
</div>
<div v-else class="h-[200px] w-full flex items-center justify-center text-sm text-gray-400">
暂无封面
</div>
</div>
</div>
<NGrid :x-gap="24" :cols="4" class="py-2">
<!-- 赛事名称 -->
<NGridItem>
<div class="mb-2 text-xs text-gray-400 font-medium">
赛事名称
</div>
<NInput v-if="isEdit" v-model:value="form.Name" placeholder="输入名称" />
<div v-else class="text-base text-gray-800 font-bold">
{{ form.Name }}
</div>
</NGridItem>
<!-- 赛事副标题 -->
<NGridItem>
<div class="mb-2 text-xs text-gray-400 font-medium">
赛事副标题
</div>
<NInput v-if="isEdit" v-model:value="form.ActivityTitle" placeholder="输入副标题" />
<div v-else class="text-base text-gray-800 font-bold">
{{ form.ActivityTitle || '-' }}
</div>
</NGridItem>
<!-- 起止时间 -->
<NGridItem>
<div class="mb-2 text-xs text-gray-400 font-medium">
起止时间
</div>
<NDatePicker
v-if="isEdit" v-model:formatted-value="dateRange" value-format="yyyy-MM-dd'T'HH:mm:ss"
type="daterange" clearable
/>
<div v-else class="flex items-center gap-2 text-sm text-gray-700 font-medium">
<div class="i-carbon-calendar text-gray-400" />
<span>{{ previewDateText.start }}</span>
<span class="text-gray-300">/</span>
<span>{{ previewDateText.end }}</span>
</div>
</NGridItem>
<!-- 参赛总人数 -->
<NGridItem>
<div class="mb-2 text-xs text-gray-400 font-medium">
参赛规模
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-400">总计</span>
<NInputNumber v-if="isEdit" v-model:value="form.Teams" :disabled="true" :show-button="false" class="w-20">
<template #suffix>
</template>
</NInputNumber>
<div
v-else
class="inline-flex items-center rounded bg-blue-100 px-2.5 py-0.5 text-sm text-blue-700 font-bold"
>
{{ form.Teams }}
</div>
<span class="ml-2 text-xs text-gray-400">单组</span>
<div v-if="isEdit">
<NInputNumber
v-model:value="form.TeamGroupNumber" :disabled="true" size="small" placeholder="分组数"
:show-button="false" class="w-16"
/>
</div>
<div v-else class="text-gray-700 font-bold">
{{ Math.floor(form.Teams / (form.TeamGroupNumber || 1)) }}
</div>
</div>
</NGridItem>
</NGrid>
</NCard>
<NCard :bordered="false" class="rounded-xl shadow-sm">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3 border-l-4 border-orange-500 pl-3">
<span class="text-lg text-gray-800 font-bold">赛事规则配置</span>
</div>
<!-- 切换 Tab -->
<div class="flex rounded-lg bg-gray-100 p-1">
<button
class="rounded-md px-4 py-1.5 text-sm font-medium transition-all"
:class="activeTab === 'normal' ? 'bg-blue-600 text-white shadow-sm' : 'text-gray-500 hover:text-gray-700'"
@click="activeTab = 'normal'"
>
普通赛事环节
</button>
<button
class="rounded-md px-4 py-1.5 text-sm font-medium transition-all"
:class="activeTab === 'extra' ? 'bg-blue-600 text-white shadow-sm' : 'text-gray-500 hover:text-gray-700'"
@click="activeTab = 'extra'"
>
加时挑战环节
</button>
</div>
</div>
<div class="min-h-[200px]">
<template v-if="isEdit">
<div class="overflow-hidden border border-gray-100 rounded-lg">
<Toolbar
style="border-bottom: 1px solid #eee" :editor="editorRef" :default-config="toolbarConfig"
:mode="mode"
/>
<div class="relative h-[400px]">
<div v-show="activeTab === 'normal'" class="h-full">
<Editor
v-model="form.ActivityContent" style="height: 100%; overflow-y: hidden;"
:default-config="{ ...editorConfig, placeholder: '请输入普通赛事的参与规则及计分标准...' }" :mode="mode"
@on-created="handleCreated"
/>
</div>
<div v-show="activeTab === 'extra'" class="h-full">
<Editor
v-model="form.ExtraTimeContent" style="height: 100%; overflow-y: hidden;"
:default-config="{ ...editorConfig, placeholder: '请输入加时赛的参与规则及计分标准...' }" :mode="mode"
@on-created="handleCreated"
/>
</div>
</div>
</div>
</template>
<template v-else>
<div
class="prose max-w-none rounded-lg bg-gray-50/50 p-4"
v-html="activeContent || '<div class=\'text-gray-400 text-center py-8\'>暂无规则配置</div>'"
/>
</template>
</div>
</NCard>
</div>
</template>
<style scoped>
:deep(.w-e-text-container) {
background-color: transparent;
}
</style>