<template>
|
<div class="activity-form">
|
<el-card>
|
<template #header>
|
<div class="card-header">
|
<span>{{ isEdit ? '编辑比赛' : '新增比赛' }}</span>
|
<el-button @click="goBack">返回</el-button>
|
</div>
|
</template>
|
|
<el-form
|
ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-width="120px"
|
v-loading="loading"
|
@submit.prevent
|
>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="比赛名称" prop="name">
|
<el-input v-model="form.name" placeholder="请输入比赛名称" maxlength="30" />
|
</el-form-item>
|
</el-col>
|
|
<el-col :span="12">
|
<el-form-item label="评分模板" prop="ratingSchemeId">
|
<el-select v-model="form.ratingSchemeId" placeholder="请选择评分模板" style="width: 100%">
|
<el-option
|
v-for="scheme in ratingSchemes"
|
:key="scheme.id"
|
:label="scheme.name"
|
:value="scheme.id"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="报名截止时间" prop="signupDeadline">
|
<el-date-picker
|
v-model="form.signupDeadline"
|
type="datetime"
|
placeholder="选择报名截止时间"
|
style="width: 100%"
|
format="YYYY-MM-DD HH:mm"
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
/>
|
</el-form-item>
|
</el-col>
|
|
<el-col :span="12">
|
<el-form-item label="比赛开始时间" prop="matchTime">
|
<el-date-picker
|
v-model="form.matchTime"
|
type="datetime"
|
placeholder="选择比赛开始时间"
|
style="width: 100%"
|
format="YYYY-MM-DD HH:mm"
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-form-item label="比赛地址" prop="address">
|
<el-input v-model="form.address" placeholder="请输入比赛地址" />
|
</el-form-item>
|
|
<el-form-item label="比赛描述" prop="description">
|
<el-input
|
v-model="form.description"
|
type="textarea"
|
:rows="4"
|
placeholder="请输入比赛描述"
|
/>
|
</el-form-item>
|
|
<!-- 图片/视频上传 -->
|
<el-divider content-position="left">
|
图片/视频
|
<span class="media-description">支持jpg/png/mp4,最多3个文件</span>
|
</el-divider>
|
|
<el-form-item label="媒体文件">
|
<div class="media-upload-section">
|
<div class="media-container">
|
<!-- 已选择的媒体文件 -->
|
<div class="media-list">
|
<div v-for="(media, index) in form.mediaFiles" :key="index" class="media-item">
|
<div v-if="media.type === 'image'" class="media-preview">
|
<img :src="media.url" class="preview-image" />
|
<div class="media-name">{{ media.name }}</div>
|
</div>
|
<div v-else-if="media.type === 'video'" class="media-preview">
|
<video :src="media.url" controls class="preview-video"></video>
|
<div class="media-name">{{ media.name }}</div>
|
</div>
|
<el-button
|
size="small"
|
type="danger"
|
class="remove-btn"
|
@click="removeMedia(index)"
|
>
|
删除
|
</el-button>
|
</div>
|
</div>
|
|
<!-- 添加按钮 -->
|
<el-upload
|
v-if="form.mediaFiles.length < 3"
|
class="media-uploader media-uploader-left"
|
:show-file-list="false"
|
:before-upload="beforeMediaUpload"
|
action="#"
|
:auto-upload="false"
|
:on-change="handleMediaChange"
|
accept=".jpg,.jpeg,.png,.mp4"
|
>
|
<div class="upload-placeholder">
|
<el-icon class="upload-icon"><Plus /></el-icon>
|
<div class="upload-text">添加图片/视频</div>
|
</div>
|
</el-upload>
|
</div>
|
</div>
|
</el-form-item>
|
|
<!-- 比赛阶段配置 -->
|
<el-divider content-position="left">比赛阶段配置</el-divider>
|
|
<div class="stages-section">
|
<el-tabs v-model="activeTab" type="border-card">
|
<!-- 比赛阶段 -->
|
<el-tab-pane label="比赛阶段" name="stages">
|
<div class="stages-header">
|
<span>比赛阶段</span>
|
<div class="stages-controls">
|
<el-button size="small" type="primary" @click="addStage">添加阶段</el-button>
|
</div>
|
</div>
|
|
<div v-if="form.value && form.value.stages && form.value.stages.length > 0" class="stages-list">
|
<div v-for="(stage, index) in sortedFormStages" :key="index" class="stage-item">
|
<div class="stage-info">
|
<div class="stage-header">
|
<span class="stage-order">{{ stage.sortOrder || '-' }}</span>
|
<span class="stage-name">{{ stage.name || '未命名阶段' }}</span>
|
</div>
|
<div class="stage-details">
|
<span class="detail-item">
|
<el-icon><Clock /></el-icon>
|
{{ formatDateTime(stage.matchTime) }}
|
</span>
|
|
<el-tag :type="stage.state === 1 ? 'success' : 'info'" size="small">
|
{{ stage.state === 1 ? '进行中' : '未开始' }}
|
</el-tag>
|
</div>
|
</div>
|
<div class="stage-actions">
|
<el-button size="small" @click="editStage(stage, getOriginalStageIndex(stage))">编辑</el-button>
|
<el-button size="small" @click="closeStage(stage)" v-if="stage.state === 1">关闭</el-button>
|
<el-button size="small" type="danger" @click="removeStage(getOriginalStageIndex(stage))">删除</el-button>
|
</div>
|
</div>
|
</div>
|
|
<el-empty v-else description="暂无比赛阶段" />
|
</el-tab-pane>
|
|
<!-- 评委列表 -->
|
<el-tab-pane label="评委列表" name="judges">
|
<div class="judges-header">
|
<span>评委列表</span>
|
<el-button size="small" type="primary" @click="addJudge">新增评委</el-button>
|
</div>
|
|
<el-table :data="form.judges" style="width: 100%" border v-loading="judgeLoading">
|
<el-table-column label="名称" prop="name" />
|
<el-table-column label="比赛阶段" width="200">
|
<template #default="{ row }">
|
<el-tag
|
v-for="stage in getJudgeStages(row)"
|
:key="stage.id"
|
size="small"
|
style="margin-right: 5px;"
|
>
|
{{ stage.name }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="100" align="center">
|
<template #default="{ row, $index }">
|
<el-button size="small" type="danger" @click="removeJudge($index)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<el-empty v-if="!form.judges || form.judges.length === 0" description="暂无评委" />
|
</el-tab-pane>
|
|
|
</el-tabs>
|
</div>
|
|
<el-form-item>
|
<el-button type="primary" @click="handleSubmit" :loading="submitting" :disabled="submitting">
|
{{ isEdit ? '更新' : '创建' }}
|
</el-button>
|
<el-button @click="goBack">取消</el-button>
|
</el-form-item>
|
</el-form>
|
</el-card>
|
|
<!-- 阶段编辑弹窗 -->
|
<el-dialog
|
v-model="stageDialogVisible"
|
:title="currentStageIndex === -1 ? '新增阶段' : '编辑阶段'"
|
width="600px"
|
@close="resetStageForm"
|
>
|
<el-form
|
ref="stageFormRef"
|
:model="currentStage"
|
:rules="stageRules"
|
label-width="120px"
|
>
|
<el-form-item label="阶段名称" prop="name">
|
<el-input v-model="currentStage.name" placeholder="请输入阶段名称" maxlength="30" />
|
</el-form-item>
|
|
<el-form-item label="比赛阶段顺序" prop="sortOrder">
|
<el-select v-model="currentStage.sortOrder" placeholder="请选择阶段顺序" style="width: 100%">
|
<el-option label="1" :value="1" />
|
<el-option label="2" :value="2" />
|
<el-option label="3" :value="3" />
|
<el-option label="4" :value="4" />
|
<el-option label="5" :value="5" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="学员人数" prop="playerMax">
|
<el-input-number
|
v-model="currentStage.playerMax"
|
:min="1"
|
:max="1000"
|
placeholder="请输入学员人数"
|
style="width: 100%"
|
/>
|
</el-form-item>
|
|
<el-form-item label="评分模板">
|
<el-select v-model="currentStage.ratingSchemeId" placeholder="继承比赛模板" style="width: 100%">
|
<el-option label="继承比赛模板" :value="null" />
|
<el-option
|
v-for="scheme in ratingSchemes"
|
:key="scheme.id"
|
:label="scheme.name"
|
:value="scheme.id"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="阶段开始时间">
|
<el-date-picker
|
v-model="currentStage.matchTime"
|
type="datetime"
|
placeholder="选择阶段开始时间"
|
style="width: 100%"
|
format="YYYY-MM-DD HH:mm"
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
/>
|
</el-form-item>
|
|
<el-form-item label="阶段地址">
|
<el-input v-model="currentStage.address" placeholder="请输入阶段地址" />
|
</el-form-item>
|
|
<el-form-item label="阶段描述">
|
<el-input
|
v-model="currentStage.description"
|
type="textarea"
|
:rows="3"
|
placeholder="请输入阶段描述"
|
/>
|
</el-form-item>
|
</el-form>
|
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="stageDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="saveStage">确定</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
|
<!-- 评委选择弹窗 -->
|
<el-dialog
|
v-model="judgeDialogVisible"
|
title="添加评委"
|
width="700px"
|
@close="resetJudgeDialog"
|
>
|
<div class="judge-selector">
|
<!-- 搜索框 -->
|
<div style="margin-bottom: 16px;">
|
<el-input
|
v-model="judgeSearchText"
|
placeholder="搜索评委姓名"
|
clearable
|
@input="handleJudgeSearch"
|
>
|
<template #prefix>
|
<el-icon><Search /></el-icon>
|
</template>
|
</el-input>
|
</div>
|
|
<!-- 阶段选择 -->
|
<div style="margin-bottom: 16px;">
|
<el-form-item label="负责阶段:" label-width="100px">
|
<!-- 调试信息 -->
|
|
<el-select v-model="selectedStageOptions" multiple style="width: 100%;" placeholder="请选择负责的阶段">
|
<!-- 使用计算属性 -->
|
<el-option
|
v-for="option in stageOptions"
|
:key="option.value"
|
:label="option.label"
|
:value="option.value"
|
/>
|
</el-select>
|
</el-form-item>
|
</div>
|
|
<!-- 评委列表 -->
|
<div class="judge-list-container">
|
<div class="judge-list-header">
|
<span>评委列表 ({{ filteredJudges.length }} 人)</span>
|
<el-button
|
size="small"
|
@click="toggleSelectAll"
|
:type="isAllSelected ? 'primary' : 'default'"
|
>
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
</el-button>
|
</div>
|
|
<div class="judge-list" v-loading="judgeLoading">
|
<el-checkbox-group v-model="selectedJudges" @change="handleJudgeSelectionChange">
|
<div v-for="judge in displayJudges" :key="judge.id" class="judge-item">
|
<el-checkbox :value="judge.id" :label="judge.id">
|
<div class="judge-info">
|
<div class="judge-name">{{ judge.name }}</div>
|
<div class="judge-desc">{{ judge.description || '暂无描述' }}</div>
|
</div>
|
</el-checkbox>
|
</div>
|
</el-checkbox-group>
|
|
<el-empty v-if="!judgeLoading && displayJudges.length === 0" description="暂无评委数据" />
|
</div>
|
</div>
|
</div>
|
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="judgeDialogVisible = false">取消</el-button>
|
<el-button
|
type="primary"
|
@click="confirmAddJudges"
|
:disabled="selectedJudges.length === 0"
|
>
|
确定 ({{ selectedJudges.length }})
|
</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
|
<!-- 学员编辑弹窗 -->
|
<el-dialog
|
v-model="studentDialogVisible"
|
:title="currentStudentIndex === -1 ? '新增学员' : '编辑学员'"
|
width="500px"
|
>
|
<el-form :model="currentStudent" label-width="120px">
|
<el-form-item label="学员名称" required>
|
<el-input v-model="currentStudent.name" placeholder="请输入学员名称" />
|
</el-form-item>
|
|
<el-form-item label="最后参与阶段">
|
<el-select v-model="currentStudent.lastStageId" placeholder="请选择阶段" style="width: 100%">
|
<el-option label="无" :value="null" />
|
<el-option
|
v-for="stage in (form.value?.stages || [])"
|
:key="stage.id"
|
:label="stage.name"
|
:value="stage.id"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-form>
|
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="studentDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="saveStudent">确定</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, computed, nextTick } from 'vue'
|
import { useRouter, useRoute } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { Plus, VideoPlay, Clock, User, UserFilled, Search } from '@element-plus/icons-vue'
|
import { getActivity, saveActivity } from '@/api/activity'
|
import { getMediasByTarget, saveMedia, uploadFile } from '@/api/media'
|
import { getAllRatingSchemes } from '@/api/rating'
|
import { getAllJudges } from '@/api/judge'
|
import { MediaTargetType } from '@/constants/mediaTargetType'
|
|
const router = useRouter()
|
const route = useRoute()
|
|
// 响应式数据
|
const loading = ref(false)
|
const submitting = ref(false)
|
const formRef = ref()
|
const stageFormRef = ref()
|
const ratingSchemes = ref([])
|
|
// Tab相关
|
const activeTab = ref('stages')
|
|
// 阶段数量选择
|
const selectedStageCount = ref(1)
|
|
// 阶段编辑弹窗相关
|
const stageDialogVisible = ref(false)
|
const currentStageIndex = ref(-1)
|
const currentStage = ref({
|
id: null,
|
name: '',
|
description: '',
|
matchTime: '',
|
address: '',
|
ratingSchemeId: null,
|
state: 1,
|
actualPlayerCount: 0
|
})
|
|
// 评委和学员弹窗相关
|
const judgeDialogVisible = ref(false)
|
const studentDialogVisible = ref(false)
|
const currentStudentIndex = ref(-1)
|
const currentStudent = ref({
|
id: null,
|
name: '',
|
lastStageId: null
|
})
|
|
// 评委选择相关
|
const allJudges = ref([])
|
const judgeSearchText = ref('')
|
const selectedStageOptions = ref([])
|
const selectedJudges = ref([])
|
const judgeLoading = ref(false)
|
|
// 计算过滤后的评委列表
|
const filteredJudges = computed(() => {
|
if (!judgeSearchText.value.trim()) {
|
return allJudges.value
|
}
|
return allJudges.value.filter(judge =>
|
judge.name.toLowerCase().includes(judgeSearchText.value.toLowerCase())
|
)
|
})
|
|
// 显示的评委列表(默认显示前10个)
|
const displayJudges = computed(() => {
|
return filteredJudges.value.slice(0, 10)
|
})
|
|
// 是否全选
|
const isAllSelected = computed(() => {
|
return displayJudges.value.length > 0 &&
|
displayJudges.value.every(judge => selectedJudges.value.includes(judge.id))
|
})
|
|
// 表单数据
|
const form = ref({
|
id: null,
|
name: '',
|
description: '',
|
signupDeadline: '',
|
matchTime: '',
|
address: '',
|
ratingSchemeId: null,
|
playerMax: null,
|
state: 1,
|
stages: [],
|
judges: [],
|
students: [],
|
mediaFiles: []
|
})
|
|
// 计算属性
|
const isEdit = computed(() => !!route.params.id)
|
|
// 按sortOrder排序的阶段列表
|
const sortedFormStages = computed(() => {
|
if (!form.value.stages) return []
|
return [...form.value.stages].sort((a, b) => {
|
const orderA = a.sortOrder || 999
|
const orderB = b.sortOrder || 999
|
return orderA - orderB
|
})
|
})
|
|
// 用于下拉框的阶段选项
|
const stageOptions = computed(() => {
|
if (!form.value?.stages) {
|
return []
|
}
|
|
return form.value.stages
|
.filter(stage => stage && stage.id != null)
|
.map(stage => ({
|
label: stage.name,
|
value: stage.id.toString(),
|
stage: stage
|
}))
|
})
|
|
// 表单验证规则
|
const rules = {
|
name: [
|
{ required: true, message: '请输入比赛名称', trigger: 'blur' },
|
{ max: 30, message: '比赛名称不能超过30个字符', trigger: 'blur' }
|
],
|
signupDeadline: [
|
{ required: true, message: '请选择报名截止时间', trigger: 'change' }
|
],
|
ratingSchemeId: [
|
{ required: true, message: '请选择评分模板', trigger: 'change' }
|
]
|
}
|
|
const stageRules = {
|
name: [
|
{ required: true, message: '请输入阶段名称', trigger: 'blur' },
|
{ max: 30, message: '阶段名称不能超过30个字符', trigger: 'blur' }
|
],
|
playerMax: [
|
{ required: true, message: '请输入学员人数', trigger: 'blur' },
|
{ type: 'number', min: 1, max: 1000, message: '学员人数必须在1-1000之间', trigger: 'blur' }
|
]
|
}
|
|
// 加载评分模板
|
const loadRatingSchemes = async () => {
|
try {
|
const schemes = await getAllRatingSchemes()
|
ratingSchemes.value = schemes || []
|
} catch (error) {
|
console.error('加载评分模板失败:', error)
|
ElMessage.error('加载评分模板失败')
|
}
|
}
|
|
// 加载所有评委
|
const loadAllJudges = async () => {
|
try {
|
judgeLoading.value = true
|
const judges = await getAllJudges()
|
allJudges.value = judges || []
|
} catch (error) {
|
console.error('加载评委列表失败:', error)
|
ElMessage.error('加载评委列表失败: ' + error.message)
|
allJudges.value = []
|
} finally {
|
judgeLoading.value = false
|
}
|
}
|
|
// 加载比赛数据(编辑模式)
|
const loadActivity = async () => {
|
if (!isEdit.value) return
|
|
try {
|
loading.value = true
|
const activity = await getActivity(route.params.id)
|
|
if (activity) {
|
form.value = {
|
id: activity.id,
|
name: activity.name,
|
description: activity.description || '',
|
signupDeadline: activity.signupDeadline,
|
matchTime: activity.matchTime || '',
|
address: activity.address || '',
|
ratingSchemeId: activity.ratingSchemeId,
|
playerMax: activity.playerMax,
|
state: activity.state,
|
stages: activity.stages || [],
|
judges: activity.judges || [],
|
students: activity.students || [],
|
mediaFiles: []
|
}
|
// 加载并回填已上传媒体:targetType=2 假设为“活动”,如不同请调整
|
try {
|
const medias = await getMediasByTarget(MediaTargetType.ACTIVITY, parseInt(activity.id))
|
|
form.value.mediaFiles = (medias || []).map(m => {
|
const isImage = (m.mediaType === 1) || (m.fileExt && ['jpg','jpeg','png','gif','webp'].includes(m.fileExt.toLowerCase()))
|
const isVideo = (m.mediaType === 2) || (m.fileExt && ['mp4','mov','m4v','avi','mkv'].includes(m.fileExt.toLowerCase()))
|
const mediaItem = {
|
id: m.id, // 已保存的媒体文件有id
|
name: m.name || (m.path ? m.path.split('/').pop() : ''),
|
url: m.fullUrl || m.path || '',
|
type: isVideo ? 'video' : 'image',
|
uploaded: true, // 标记为已上传,不需要重新上传
|
file: null // 已保存的文件没有file对象
|
}
|
return mediaItem
|
})
|
} catch (e) {
|
console.error('加载活动媒体失败:', e)
|
}
|
|
// 设置阶段数量选择器的值
|
selectedStageCount.value = (form.value && form.value.stages) ? form.value.stages.length || 1 : 1
|
}
|
} catch (error) {
|
console.error('加载比赛数据失败:', error)
|
ElMessage.error('加载比赛数据失败')
|
} finally {
|
loading.value = false
|
}
|
}
|
|
// 阶段管理
|
// 阶段数量变化处理
|
const onStageCountChange = (count) => {
|
if (!count || !form.value || !form.value.stages) return
|
|
// 如果当前阶段数量少于选择的数量,自动添加阶段
|
while (form.value.stages.length < count) {
|
const stageIndex = form.value.stages.length + 1
|
form.value.stages.push({
|
id: null,
|
name: getDefaultStageName(stageIndex),
|
description: '',
|
matchTime: '',
|
address: form.value.address || '',
|
ratingSchemeId: form.value.ratingSchemeId,
|
sortOrder: stageIndex,
|
state: 1,
|
actualPlayerCount: 0
|
})
|
}
|
|
// 如果当前阶段数量多于选择的数量,删除多余的阶段
|
if (form.value.stages.length > count) {
|
form.value.stages = form.value.stages.slice(0, count)
|
}
|
|
ElMessage.success(`已设置为${count}个阶段`)
|
}
|
|
// 获取默认阶段名称 - 使用更灵活的命名方式,避免硬编码特定阶段名称
|
const getDefaultStageName = (index) => {
|
// 提供一些常用的阶段名称建议,但不强制使用
|
const suggestedNames = ['', '第一阶段', '第二阶段', '第三阶段', '第四阶段', '第五阶段']
|
return suggestedNames[index] || `第${index}阶段`
|
}
|
|
// 获取阶段在原始数组中的索引
|
const getOriginalStageIndex = (stage) => {
|
if (!form.value || !form.value.stages) return -1
|
return form.value.stages.findIndex(s => s === stage)
|
}
|
|
const addStage = () => {
|
currentStageIndex.value = -1
|
resetStageForm()
|
stageDialogVisible.value = true
|
}
|
|
const editStage = (stage, index) => {
|
currentStageIndex.value = index
|
currentStage.value = { ...stage }
|
stageDialogVisible.value = true
|
}
|
|
const removeStage = async (index) => {
|
if (!form.value || !form.value.stages) return
|
|
try {
|
await ElMessageBox.confirm('确定要删除这个阶段吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
form.value.stages.splice(index, 1)
|
|
// 重新排序sortOrder
|
form.value.stages.forEach((stage, idx) => {
|
stage.sortOrder = idx + 1
|
})
|
|
// 更新选择的阶段数量
|
selectedStageCount.value = form.value.stages.length
|
|
ElMessage.success('删除成功')
|
} catch {
|
// 用户取消删除
|
}
|
}
|
|
const closeStage = async (stage) => {
|
try {
|
await ElMessageBox.confirm('确定要关闭这个阶段吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
stage.state = 0
|
ElMessage.success('阶段已关闭')
|
} catch {
|
// 用户取消关闭
|
}
|
}
|
|
const saveStage = async () => {
|
if (!form.value || !form.value.stages) return
|
|
try {
|
await stageFormRef.value.validate()
|
|
if (currentStageIndex.value === -1) {
|
// 新增阶段 - 设置正确的sortOrder
|
const newStage = { ...currentStage.value }
|
newStage.sortOrder = form.value.stages.length + 1
|
form.value.stages.push(newStage)
|
} else {
|
// 编辑阶段
|
form.value.stages[currentStageIndex.value] = { ...currentStage.value }
|
}
|
|
stageDialogVisible.value = false
|
ElMessage.success(currentStageIndex.value === -1 ? '添加成功' : '更新成功')
|
} catch (error) {
|
console.error('保存阶段失败:', error)
|
}
|
}
|
|
const resetStageForm = () => {
|
currentStage.value = {
|
id: null,
|
name: '',
|
description: '',
|
matchTime: '',
|
address: '',
|
ratingSchemeId: null,
|
playerMax: null,
|
sortOrder: null, // 将在saveStage中设置正确的值
|
state: 1,
|
actualPlayerCount: 0
|
}
|
if (stageFormRef.value) {
|
stageFormRef.value.clearValidate()
|
}
|
}
|
|
// 格式化日期时间
|
const formatDateTime = (dateTime) => {
|
if (!dateTime) return '未设置'
|
return new Date(dateTime).toLocaleString('zh-CN', {
|
year: 'numeric',
|
month: '2-digit',
|
day: '2-digit',
|
hour: '2-digit',
|
minute: '2-digit'
|
})
|
}
|
|
// 评委管理
|
const addJudge = () => {
|
resetJudgeDialog()
|
judgeDialogVisible.value = true
|
}
|
|
const editJudge = (judge, index) => {
|
// 编辑功能暂时保留原有逻辑,或者可以移除
|
ElMessage.info('请通过删除后重新添加的方式修改评委')
|
}
|
|
const removeJudge = async (index) => {
|
if (!form.value || !form.value.judges) {
|
ElMessage.error('表单数据未初始化')
|
return
|
}
|
|
try {
|
await ElMessageBox.confirm('确定要删除这个评委吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
form.value.judges.splice(index, 1)
|
ElMessage.success('删除成功')
|
} catch {
|
// 用户取消删除
|
}
|
}
|
|
const getJudgeStages = (judge) => {
|
if (!judge.stageIds || !form.value || !form.value.stages) return []
|
|
const stages = []
|
|
// 检查比赛阶段
|
judge.stageIds.forEach(stageId => {
|
// 处理实际阶段ID
|
const stage = form.value.stages.find(s => s.id === stageId)
|
if (stage) {
|
stages.push({
|
id: stage.id,
|
name: stage.name
|
})
|
}
|
})
|
|
return stages
|
}
|
|
const resetJudgeDialog = () => {
|
judgeSearchText.value = ''
|
// 清空阶段选择
|
selectedStageOptions.value = []
|
selectedJudges.value = []
|
}
|
|
const handleJudgeSearch = (value) => {
|
// 搜索评委
|
}
|
|
|
|
const handleJudgeSelectionChange = (value) => {
|
// 选择评委
|
}
|
|
const toggleSelectAll = () => {
|
if (isAllSelected.value) {
|
// 取消全选
|
selectedJudges.value = selectedJudges.value.filter(id =>
|
!displayJudges.value.some(judge => judge.id === id)
|
)
|
} else {
|
// 全选
|
const newSelections = displayJudges.value.map(judge => judge.id)
|
selectedJudges.value = [...new Set([...selectedJudges.value, ...newSelections])]
|
}
|
}
|
|
const confirmAddJudges = () => {
|
if (selectedJudges.value.length === 0) {
|
ElMessage.warning('请选择至少一个评委')
|
return
|
}
|
|
if (!form.value || !form.value.judges) {
|
ElMessage.error('表单数据未初始化')
|
return
|
}
|
|
// 如果有阶段但没有选择阶段,则提示
|
if (form.value && form.value.stages && form.value.stages.length > 0 && selectedStageOptions.value.length === 0) {
|
ElMessage.warning('请选择至少一个负责阶段')
|
return
|
}
|
|
let addedCount = 0
|
|
selectedJudges.value.forEach(judgeId => {
|
const judge = allJudges.value.find(j => j.id === judgeId)
|
if (judge) {
|
// 检查是否已经存在
|
const existingJudge = form.value.judges.find(j => j.id === judgeId)
|
if (existingJudge) {
|
// 更新现有评委的阶段,合并新选择的阶段
|
if (selectedStageOptions.value.length > 0) {
|
selectedStageOptions.value.forEach(stageId => {
|
const stageIdInt = parseInt(stageId)
|
if (!existingJudge.stageIds.includes(stageIdInt)) {
|
existingJudge.stageIds.push(stageIdInt)
|
}
|
})
|
}
|
} else {
|
// 添加新评委,包含所有选择的阶段
|
const stageIds = selectedStageOptions.value.length > 0
|
? selectedStageOptions.value.map(id => parseInt(id))
|
: []
|
|
const newJudge = {
|
id: judge.id,
|
name: judge.name,
|
stageIds: stageIds
|
}
|
form.value.judges.push(newJudge)
|
addedCount++
|
}
|
}
|
})
|
|
// 清空选择
|
selectedJudges.value = []
|
selectedStageOptions.value = []
|
|
judgeDialogVisible.value = false
|
ElMessage.success(`成功添加 ${addedCount} 个评委`)
|
}
|
|
// 学员管理
|
const viewStudent = (student, index) => {
|
if (!form.value.id) {
|
ElMessage.warning('请先保存比赛后再查看学员')
|
return
|
}
|
ElMessage.info(`查看学员:${student.name}`)
|
// TODO: 实现学员详情查看功能
|
}
|
|
const rateStudent = (student, index) => {
|
if (!form.value.id) {
|
ElMessage.warning('请先保存比赛后再为学员评分')
|
return
|
}
|
ElMessage.info(`为学员 ${student.name} 评分`)
|
// TODO: 实现学员评分功能
|
}
|
|
const commentStudent = (student, index) => {
|
if (!form.value.id) {
|
ElMessage.warning('请先保存比赛后再为学员点评')
|
return
|
}
|
ElMessage.info(`为学员 ${student.name} 点评`)
|
// TODO: 实现学员点评功能
|
}
|
|
const toggleAdvancement = async (student, index) => {
|
if (!form.value.id) {
|
ElMessage.warning('请先保存比赛后再设置学员晋级状态')
|
return
|
}
|
try {
|
const action = student.isAdvanced ? '取消晋级' : '设为晋级'
|
await ElMessageBox.confirm(`确定要${action}学员 ${student.name} 吗?`, '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
|
student.isAdvanced = !student.isAdvanced
|
ElMessage.success(`${action}成功`)
|
} catch {
|
// 用户取消操作
|
}
|
}
|
|
const getLastStage = (student) => {
|
if (!student.lastStageId || !form.value || !form.value.stages) return '无'
|
const stage = form.value.stages.find(s => s.id === student.lastStageId)
|
return stage ? stage.name : '无'
|
}
|
|
|
|
const saveStudent = () => {
|
if (!currentStudent.value.name.trim()) {
|
ElMessage.error('请输入学员名称')
|
return
|
}
|
|
if (currentStudentIndex.value === -1) {
|
// 新增学员
|
form.value.students.push({ ...currentStudent.value })
|
} else {
|
// 编辑学员
|
form.value.students[currentStudentIndex.value] = { ...currentStudent.value }
|
}
|
|
studentDialogVisible.value = false
|
ElMessage.success(currentStudentIndex.value === -1 ? '添加成功' : '更新成功')
|
}
|
|
// 媒体文件上传处理
|
const handleMediaChange = async (file) => {
|
const isImage = file.raw.type.startsWith('image/')
|
const isVideo = file.raw.type === 'video/mp4'
|
|
if (!isImage && !isVideo) {
|
ElMessage.error('只支持图片(jpg/png)和视频(mp4)格式!')
|
return false
|
}
|
|
// 只是添加到列表,不立即上传
|
const url = URL.createObjectURL(file.raw)
|
const mediaFile = {
|
name: file.name,
|
url: url,
|
type: isImage ? 'image' : 'video',
|
file: file.raw,
|
uploaded: false // 标记为未上传
|
}
|
|
if (form.value && form.value.mediaFiles) {
|
form.value.mediaFiles.push(mediaFile)
|
ElMessage.success('文件已选择,点击更新按钮时将上传')
|
}
|
|
return false // 阻止el-upload的默认上传
|
}
|
|
const beforeMediaUpload = (file) => {
|
if (!form.value || !form.value.mediaFiles) return false
|
|
if (form.value.mediaFiles.length >= 3) {
|
ElMessage.error('最多只能上传3个文件!')
|
return false
|
}
|
|
const isImage = file.type.startsWith('image/')
|
const isVideo = file.type === 'video/mp4'
|
|
if (!isImage && !isVideo) {
|
ElMessage.error('只支持图片(jpg/png)和视频(mp4)格式!')
|
return false
|
}
|
|
if (isImage) {
|
const isLt10M = file.size / 1024 / 1024 < 10
|
if (!isLt10M) {
|
ElMessage.error('图片大小不能超过 10MB!')
|
return false
|
}
|
}
|
|
if (isVideo) {
|
const isLt200M = file.size / 1024 / 1024 < 200
|
if (!isLt200M) {
|
ElMessage.error('视频大小不能超过 200MB!')
|
return false
|
}
|
}
|
|
return true
|
}
|
|
const removeMedia = async (index) => {
|
const mediaFile = form.value.mediaFiles[index]
|
|
// 如果是已保存的媒体文件(有id),需要调用后端删除API
|
if (mediaFile.id) {
|
try {
|
const { deleteMedia } = await import('@/api/media.js')
|
const success = await deleteMedia(mediaFile.id)
|
if (success) {
|
ElMessage.success('媒体文件删除成功')
|
} else {
|
ElMessage.error('媒体文件删除失败')
|
return
|
}
|
} catch (error) {
|
console.error('删除媒体文件失败:', error)
|
ElMessage.error('删除媒体文件失败: ' + error.message)
|
return
|
}
|
}
|
|
// 从列表中移除
|
form.value.mediaFiles.splice(index, 1)
|
|
// 如果是新选择的文件(没有id),只需要从列表中移除即可
|
if (!mediaFile.id) {
|
ElMessage.success('文件已从列表中移除')
|
}
|
}
|
|
// 处理媒体文件上传
|
const handleMediaUpload = async (activityId) => {
|
if (!form.value || !form.value.mediaFiles) return
|
|
try {
|
for (const mediaFile of form.value.mediaFiles) {
|
// 跳过已经有 id 的媒体文件(已保存的)
|
if (mediaFile.id) {
|
continue
|
}
|
|
// 跳过没有 file 对象的媒体文件
|
if (!mediaFile.file) {
|
continue
|
}
|
|
// 跳过已经上传的文件
|
if (mediaFile.uploaded === true) {
|
continue
|
}
|
|
try {
|
// 1. 上传文件到服务器
|
const uploadResult = await uploadFile(mediaFile.file)
|
|
// 2. 保存媒体信息到数据库
|
const mediaInput = {
|
name: uploadResult.fileName || mediaFile.name,
|
path: uploadResult.path,
|
fileSize: uploadResult.fileSize || mediaFile.file.size,
|
fileExt: uploadResult.fileName ? uploadResult.fileName.split('.').pop() : mediaFile.name.split('.').pop() || 'jpg',
|
mediaType: mediaFile.type === 'video' ? 2 : 1, // 1=图片, 2=视频
|
targetType: MediaTargetType.ACTIVITY, // 活动
|
targetId: parseInt(activityId) // 转换为数字类型
|
}
|
const savedMedia = await saveMedia(mediaInput)
|
|
// 更新媒体文件信息
|
mediaFile.id = savedMedia.id
|
mediaFile.url = savedMedia.fullUrl || savedMedia.path
|
|
// 标记为已上传
|
mediaFile.uploaded = true
|
mediaFile.uploadResult = uploadResult
|
} catch (error) {
|
console.error(`媒体文件 ${mediaFile.name} 处理失败:`, error)
|
ElMessage.error(`文件 ${mediaFile.name} 上传失败: ${error.message}`)
|
// 不抛出错误,继续处理其他文件
|
}
|
}
|
} catch (error) {
|
console.error('媒体文件处理失败:', error)
|
// 不影响主流程,只记录错误
|
}
|
}
|
|
// 提交表单
|
const handleSubmit = async () => {
|
if (submitting.value) return
|
if (!form.value) return
|
|
try {
|
await formRef.value.validate()
|
|
submitting.value = true
|
|
// 准备保存数据,只包含后端支持的字段
|
const saveData = {
|
name: form.value.name,
|
description: form.value.description,
|
signupDeadline: form.value.signupDeadline,
|
matchTime: form.value.matchTime,
|
address: form.value.address,
|
ratingSchemeId: form.value.ratingSchemeId,
|
playerMax: form.value.playerMax,
|
state: form.value.state || 1,
|
stages: form.value.stages ? form.value.stages.map(stage => {
|
const stageData = {
|
name: stage.name,
|
description: stage.description,
|
matchTime: stage.matchTime,
|
address: stage.address,
|
playerMax: stage.playerMax,
|
sortOrder: stage.sortOrder,
|
state: stage.state || 1
|
}
|
// 只在有有效ID时才添加id字段
|
if (stage.id) {
|
stageData.id = stage.id
|
}
|
// 只在有有效ratingSchemeId时才添加该字段
|
if (stage.ratingSchemeId) {
|
stageData.ratingSchemeId = stage.ratingSchemeId
|
}
|
return stageData
|
}) : [],
|
judges: form.value.judges ? form.value.judges.filter(judge => judge.id && judge.name).map(judge => ({
|
judgeId: judge.id,
|
judgeName: judge.name,
|
stageIds: judge.stageIds || []
|
})) : []
|
}
|
|
// 如果是编辑模式,添加id字段
|
if (isEdit.value && form.value.id) {
|
saveData.id = form.value.id
|
}
|
|
// 如果有pid,添加pid字段
|
if (form.value.pid) {
|
saveData.pid = form.value.pid
|
}
|
|
const result = await saveActivity(saveData)
|
|
// 如果是新增,更新form的id
|
if (!isEdit.value && result && result.id) {
|
form.value.id = result.id
|
}
|
|
// 处理媒体文件上传和保存
|
if (form.value.mediaFiles && form.value.mediaFiles.length > 0) {
|
const activityId = result.id || form.value.id
|
if (activityId) {
|
await handleMediaUpload(activityId)
|
}
|
}
|
|
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
|
// 如果是新增,不自动返回,让用户可以继续添加评委和学员
|
if (isEdit.value) {
|
goBack()
|
}
|
} catch (error) {
|
if (error.message) {
|
console.error('保存比赛失败:', error)
|
ElMessage.error('保存失败: ' + error.message)
|
}
|
} finally {
|
submitting.value = false
|
}
|
}
|
|
// 返回
|
const goBack = () => {
|
router.push('/activity')
|
}
|
|
// 生命周期
|
onMounted(async () => {
|
await loadRatingSchemes()
|
await loadAllJudges()
|
await loadActivity()
|
|
// 如果是新建模式且没有阶段,自动创建一个阶段
|
if (!isEdit.value && form.value && form.value.stages && form.value.stages.length === 0) {
|
onStageCountChange(1)
|
}
|
})
|
</script>
|
|
<style scoped>
|
.activity-form {
|
padding: 20px;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.stages-section {
|
margin-top: 20px;
|
}
|
|
.stages-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
}
|
|
.stages-list {
|
margin-top: 15px;
|
}
|
|
.stage-card {
|
margin-bottom: 15px;
|
}
|
|
.stage-header {
|
display: flex;
|
justify-content: flex-start;
|
align-items: center;
|
}
|
|
/* 文件上传样式 */
|
.image-uploader .uploaded-image {
|
width: 178px;
|
height: 178px;
|
object-fit: cover;
|
border-radius: 6px;
|
}
|
|
.image-uploader .image-uploader-icon {
|
font-size: 28px;
|
color: #8c939d;
|
width: 178px;
|
height: 178px;
|
line-height: 178px;
|
text-align: center;
|
border: 1px dashed #d9d9d9;
|
border-radius: 6px;
|
cursor: pointer;
|
position: relative;
|
overflow: hidden;
|
transition: .3s;
|
}
|
|
.image-uploader .image-uploader-icon:hover {
|
border-color: #409eff;
|
}
|
|
.video-uploader .uploaded-video {
|
width: 100%;
|
max-width: 300px;
|
}
|
|
.video-uploader .video-name {
|
margin-top: 8px;
|
font-size: 12px;
|
color: #606266;
|
text-align: center;
|
}
|
|
.video-uploader .video-upload-placeholder {
|
width: 178px;
|
height: 178px;
|
border: 1px dashed #d9d9d9;
|
border-radius: 6px;
|
cursor: pointer;
|
position: relative;
|
overflow: hidden;
|
transition: .3s;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.video-uploader .video-upload-placeholder:hover {
|
border-color: #409eff;
|
}
|
|
.video-uploader .video-uploader-icon {
|
font-size: 28px;
|
color: #8c939d;
|
margin-bottom: 8px;
|
}
|
|
.video-uploader .upload-text {
|
font-size: 14px;
|
color: #606266;
|
margin-bottom: 4px;
|
}
|
|
.video-uploader .upload-tip {
|
font-size: 12px;
|
color: #909399;
|
text-align: center;
|
}
|
|
/* 阶段列表样式 */
|
.stage-item {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 16px;
|
border: 1px solid #ebeef5;
|
border-radius: 8px;
|
margin-bottom: 12px;
|
background-color: #fff;
|
transition: all 0.3s;
|
}
|
|
.stage-item:hover {
|
border-color: #409eff;
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
}
|
|
.stage-info {
|
flex: 1;
|
}
|
|
.stage-header {
|
display: flex;
|
align-items: center;
|
margin-bottom: 8px;
|
}
|
|
.stage-order {
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
width: 24px;
|
height: 24px;
|
background-color: #409eff;
|
color: white;
|
border-radius: 50%;
|
font-size: 12px;
|
font-weight: 600;
|
flex-shrink: 0;
|
}
|
|
.stage-name {
|
font-size: 16px;
|
font-weight: 500;
|
color: #303133;
|
margin: 0;
|
margin-left: 4px;
|
}
|
|
.stage-details {
|
display: flex;
|
align-items: center;
|
gap: 16px;
|
flex-wrap: wrap;
|
}
|
|
.detail-item {
|
display: flex;
|
align-items: center;
|
gap: 4px;
|
font-size: 14px;
|
color: #606266;
|
}
|
|
.detail-item .el-icon {
|
font-size: 16px;
|
color: #909399;
|
}
|
|
.stage-actions {
|
display: flex;
|
gap: 8px;
|
}
|
|
.stages-list {
|
margin-top: 16px;
|
}
|
|
/* 媒体文件上传样式 */
|
.media-upload-section {
|
width: 100%;
|
}
|
|
.media-container {
|
display: flex;
|
align-items: flex-start;
|
gap: 16px;
|
}
|
|
.media-list {
|
display: flex;
|
flex-direction: row;
|
gap: 16px;
|
overflow-x: auto;
|
padding-bottom: 8px;
|
flex: 1;
|
}
|
|
.media-item {
|
position: relative;
|
border: 1px solid #ebeef5;
|
border-radius: 8px;
|
padding: 8px;
|
background-color: #fff;
|
flex-shrink: 0;
|
}
|
|
.media-preview {
|
width: 150px;
|
text-align: center;
|
}
|
|
.preview-image {
|
width: 100%;
|
height: 100px;
|
object-fit: cover;
|
border-radius: 4px;
|
}
|
|
.preview-video {
|
width: 100%;
|
height: 100px;
|
border-radius: 4px;
|
}
|
|
.media-name {
|
margin-top: 8px;
|
font-size: 12px;
|
color: #606266;
|
word-break: break-all;
|
}
|
|
.remove-btn {
|
position: absolute;
|
top: -8px;
|
right: -8px;
|
border-radius: 50%;
|
width: 24px;
|
height: 24px;
|
padding: 0;
|
min-height: 24px;
|
}
|
|
.media-uploader .upload-placeholder {
|
width: 150px;
|
height: 120px;
|
border: 1px dashed #d9d9d9;
|
border-radius: 8px;
|
cursor: pointer;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
transition: all 0.3s;
|
}
|
|
.media-uploader .upload-placeholder:hover {
|
border-color: #409eff;
|
}
|
|
.media-uploader .upload-icon {
|
font-size: 24px;
|
color: #8c939d;
|
margin-bottom: 8px;
|
}
|
|
.media-uploader .upload-text {
|
font-size: 14px;
|
color: #606266;
|
margin-bottom: 4px;
|
}
|
|
.media-uploader .upload-tip {
|
font-size: 12px;
|
color: #909399;
|
text-align: center;
|
}
|
|
/* Tab相关样式 */
|
.judges-header,
|
.students-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 16px;
|
}
|
|
/* 评委选择弹窗样式 */
|
.judge-selector {
|
max-height: 600px;
|
}
|
|
.judge-list-container {
|
border: 1px solid #ebeef5;
|
border-radius: 8px;
|
overflow: hidden;
|
}
|
|
.judge-list-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 12px 16px;
|
background-color: #f5f7fa;
|
border-bottom: 1px solid #ebeef5;
|
font-weight: 500;
|
color: #303133;
|
}
|
|
.judge-list {
|
max-height: 350px;
|
overflow-y: auto;
|
padding: 8px;
|
background-color: #fff;
|
}
|
|
.judge-item {
|
padding: 12px 8px;
|
border-bottom: 1px solid #f5f7fa;
|
transition: background-color 0.3s;
|
}
|
|
.judge-item:last-child {
|
border-bottom: none;
|
}
|
|
.judge-item:hover {
|
background-color: #f5f7fa;
|
}
|
|
.judge-info {
|
margin-left: 8px;
|
flex: 1;
|
}
|
|
.judge-name {
|
font-weight: 500;
|
color: #303133;
|
margin-bottom: 4px;
|
font-size: 14px;
|
}
|
|
.judge-desc {
|
font-size: 12px;
|
color: #909399;
|
line-height: 1.4;
|
}
|
|
.el-checkbox {
|
width: 100%;
|
display: flex;
|
align-items: flex-start;
|
}
|
|
.el-checkbox__label {
|
flex: 1;
|
padding-left: 8px;
|
}
|
|
/* 媒体描述文本样式 */
|
.media-description {
|
font-size: 12px;
|
color: #909399;
|
font-weight: normal;
|
margin-left: 8px;
|
}
|
|
/* 媒体上传按钮 */
|
.media-uploader-left {
|
margin-left: 16px;
|
}
|
|
.media-container {
|
display: flex;
|
align-items: flex-start;
|
gap: 16px;
|
flex-wrap: wrap;
|
}
|
</style>
|