<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-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="比赛地址" prop="address">
|
<el-input v-model="form.address" placeholder="请输入比赛地址" />
|
</el-form-item>
|
</el-col>
|
|
<el-col :span="12">
|
<el-form-item label="人数" prop="playerMax">
|
<el-input-number v-model="form.playerMax" :min="1" :max="9999" style="width: 100%" />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<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">图片/视频</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"
|
: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 class="upload-tip">支持jpg/png/mp4,最多3个文件</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>
|
<el-button size="small" type="primary" @click="addStage">添加阶段</el-button>
|
</div>
|
|
<div v-if="form.stages && form.stages.length > 0" class="stages-list">
|
<div v-for="(stage, index) in form.stages" :key="index" class="stage-item">
|
<div class="stage-info">
|
<div class="stage-name">{{ stage.name || '未命名阶段' }}</div>
|
<div class="stage-details">
|
<span class="detail-item">
|
<el-icon><Clock /></el-icon>
|
{{ formatDateTime(stage.matchTime) }}
|
</span>
|
<span class="detail-item">
|
<el-icon><User /></el-icon>
|
{{ stage.playerMax || 0 }} 人
|
</span>
|
<span class="detail-item">
|
<el-icon><UserFilled /></el-icon>
|
实际: {{ stage.actualPlayerCount || 0 }} 人
|
</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, index)">编辑</el-button>
|
<el-button size="small" @click="closeStage(stage)" v-if="stage.state === 1">关闭</el-button>
|
<el-button size="small" type="danger" @click="removeStage(index)">删除</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>
|
<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="150" align="center">
|
<template #default="{ row, $index }">
|
<el-button size="small" @click="editJudge(row, $index)">编辑</el-button>
|
<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-tab-pane label="学员列表" name="students">
|
<div class="students-header">
|
<span>学员列表</span>
|
</div>
|
|
<el-table :data="form.students" style="width: 100%" border>
|
<el-table-column label="学员名称" prop="name" />
|
<el-table-column label="最后参与的比赛阶段" width="200">
|
<template #default="{ row }">
|
{{ getLastStage(row) }}
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="250" align="center">
|
<template #default="{ row, $index }">
|
<el-button size="small" @click="viewStudent(row, $index)">查看</el-button>
|
<el-button size="small" type="primary" @click="rateStudent(row, $index)">评分</el-button>
|
<el-button size="small" @click="commentStudent(row, $index)">点评</el-button>
|
<el-button
|
size="small"
|
:type="row.isAdvanced ? 'success' : 'warning'"
|
@click="toggleAdvancement(row, $index)"
|
>
|
{{ row.isAdvanced ? '已晋级' : '晋级' }}
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<el-empty v-if="!form.students || form.students.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="评分模板">
|
<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-number v-model="currentStage.playerMax" :min="1" :max="9999" style="width: 100%" />
|
</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="selectedStageOption" style="width: 100%;" @change="handleStageChange">
|
<el-option label="所有阶段" value="all" />
|
<el-option
|
v-for="stage in form.stages"
|
:key="stage.id"
|
:label="stage.name"
|
:value="stage.id ? stage.id.toString() : ''"
|
/>
|
</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.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 } 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'
|
|
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 stageDialogVisible = ref(false)
|
const currentStageIndex = ref(-1)
|
const currentStage = ref({
|
id: null,
|
name: '',
|
description: '',
|
matchTime: '',
|
address: '',
|
ratingSchemeId: null,
|
playerMax: 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 selectedStageOption = ref('all')
|
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: 100,
|
state: 1,
|
stages: [],
|
judges: [],
|
students: [],
|
mediaFiles: []
|
})
|
|
// 计算属性
|
const isEdit = computed(() => !!route.params.id)
|
|
// 表单验证规则
|
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' }
|
]
|
}
|
|
// 加载评分模板
|
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 || []
|
console.log('加载评委列表成功:', allJudges.value.length, '个评委')
|
} 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 || 100,
|
state: activity.state,
|
stages: activity.stages || [],
|
judges: activity.judges || [],
|
students: activity.students || [],
|
mediaFiles: []
|
}
|
// 加载并回填已上传媒体:targetType=2 假设为“活动”,如不同请调整
|
try {
|
const medias = await getMediasByTarget(2, parseInt(activity.id))
|
console.log('=== 加载活动媒体调试信息 ===')
|
console.log('活动ID:', activity.id)
|
console.log('获取到的媒体数据:', medias)
|
|
form.value.mediaFiles = (medias || []).map(m => {
|
console.log('处理媒体文件:', 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对象
|
}
|
console.log('转换后的媒体项:', mediaItem)
|
return mediaItem
|
})
|
console.log('最终的mediaFiles:', form.value.mediaFiles)
|
} catch (e) {
|
console.error('加载活动媒体失败:', e)
|
}
|
}
|
} catch (error) {
|
console.error('加载比赛数据失败:', error)
|
ElMessage.error('加载比赛数据失败')
|
} finally {
|
loading.value = false
|
}
|
}
|
|
// 阶段管理
|
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) => {
|
try {
|
await ElMessageBox.confirm('确定要删除这个阶段吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
form.value.stages.splice(index, 1)
|
ElMessage.success('删除成功')
|
} catch {
|
// 用户取消删除
|
}
|
}
|
|
const closeStage = async (stage) => {
|
try {
|
await ElMessageBox.confirm('确定要关闭这个阶段吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
stage.state = 0
|
ElMessage.success('阶段已关闭')
|
} catch {
|
// 用户取消关闭
|
}
|
}
|
|
const saveStage = async () => {
|
try {
|
await stageFormRef.value.validate()
|
|
if (currentStageIndex.value === -1) {
|
// 新增阶段
|
form.value.stages.push({ ...currentStage.value })
|
} 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,
|
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) => {
|
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.stages) return []
|
return form.value.stages.filter(stage => judge.stageIds.includes(stage.id))
|
}
|
|
const resetJudgeDialog = () => {
|
judgeSearchText.value = ''
|
selectedStageOption.value = 'all'
|
selectedJudges.value = []
|
}
|
|
const handleJudgeSearch = (value) => {
|
console.log('搜索评委:', value)
|
}
|
|
const handleStageChange = (value) => {
|
console.log('选择阶段:', value)
|
}
|
|
const handleJudgeSelectionChange = (value) => {
|
console.log('选择评委:', 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
|
}
|
|
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 (selectedStageOption.value === 'all') {
|
existingJudge.stageIds = form.value.stages.map(s => s.id).filter(id => id != null)
|
} else {
|
const stageId = parseInt(selectedStageOption.value)
|
if (!existingJudge.stageIds.includes(stageId)) {
|
existingJudge.stageIds.push(stageId)
|
}
|
}
|
} else {
|
// 添加新评委
|
const newJudge = {
|
id: judge.id,
|
name: judge.name,
|
stageIds: selectedStageOption.value === 'all'
|
? form.value.stages.map(s => s.id).filter(id => id != null)
|
: [parseInt(selectedStageOption.value)]
|
}
|
form.value.judges.push(newJudge)
|
addedCount++
|
}
|
}
|
})
|
|
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.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 // 标记为未上传
|
}
|
|
form.value.mediaFiles.push(mediaFile)
|
ElMessage.success('文件已选择,点击更新按钮时将上传')
|
|
return false // 阻止el-upload的默认上传
|
}
|
|
const beforeMediaUpload = (file) => {
|
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) => {
|
try {
|
for (const mediaFile of form.value.mediaFiles) {
|
// 跳过已经有 id 的媒体文件(已保存的)
|
if (mediaFile.id) {
|
continue
|
}
|
|
// 跳过没有 file 对象的媒体文件
|
if (!mediaFile.file) {
|
continue
|
}
|
|
// 跳过已经上传的文件
|
if (mediaFile.uploaded === true) {
|
continue
|
}
|
|
try {
|
console.log('开始上传文件:', mediaFile.name)
|
// 1. 上传文件到服务器
|
const uploadResult = await uploadFile(mediaFile.file)
|
console.log('文件上传成功:', uploadResult)
|
|
// 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: 2, // 2=活动
|
targetId: parseInt(activityId) // 转换为数字类型
|
}
|
|
console.log('准备保存媒体信息:', mediaInput)
|
console.log('活动ID:', activityId)
|
const savedMedia = await saveMedia(mediaInput)
|
console.log(`媒体文件 ${mediaFile.name} 上传并保存成功:`, savedMedia)
|
|
// 更新媒体文件信息
|
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
|
try {
|
await formRef.value.validate()
|
|
submitting.value = true
|
|
// 准备保存数据,只包含后端支持的字段
|
const saveData = {
|
id: form.value.id,
|
pid: form.value.pid || 0,
|
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 => ({
|
id: stage.id,
|
name: stage.name,
|
description: stage.description,
|
matchTime: stage.matchTime,
|
address: stage.address,
|
ratingSchemeId: stage.ratingSchemeId,
|
playerMax: stage.playerMax,
|
state: stage.state || 1
|
})) : [],
|
judges: form.value.judges ? form.value.judges.map(judge => ({
|
judgeId: judge.id,
|
judgeName: judge.name,
|
stageIds: judge.stageIds || []
|
})) : []
|
}
|
|
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()
|
})
|
</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: space-between;
|
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-name {
|
font-size: 16px;
|
font-weight: 500;
|
color: #303133;
|
margin-bottom: 8px;
|
}
|
|
.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;
|
}
|
</style>
|