web/src/views/ActivityForm.vue
@@ -79,7 +79,10 @@
        </el-form-item>
        <!-- 图片/视频上传 -->
        <el-divider content-position="left">图片/视频</el-divider>
        <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">
@@ -109,7 +112,7 @@
              <!-- 添加按钮 -->
              <el-upload
                v-if="form.mediaFiles.length < 3"
                class="media-uploader"
                class="media-uploader media-uploader-left"
                :show-file-list="false"
                :before-upload="beforeMediaUpload"
                action="#"
@@ -120,7 +123,6 @@
                <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>
@@ -141,7 +143,7 @@
                </div>
              </div>
              
              <div v-if="form.stages && form.stages.length > 0" class="stages-list">
              <div v-if="form && form.stages && form.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">
@@ -191,9 +193,8 @@
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column label="操作" width="180" align="center">
                <el-table-column label="操作" width="100" 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>
@@ -321,14 +322,16 @@
        
        <!-- 阶段选择 -->
        <div style="margin-bottom: 16px;">
          <el-form-item label="添加到阶段:" label-width="100px">
            <el-select v-model="selectedStageOption" style="width: 100%;" @change="handleStageChange">
              <!-- 只显示比赛阶段 -->
          <el-form-item label="负责阶段:" label-width="100px">
            <!-- 调试信息 -->
            <el-select v-model="selectedStageOptions" multiple style="width: 100%;" placeholder="请选择负责的阶段">
              <!-- 使用计算属性 -->
              <el-option 
                v-for="stage in form.stages"
                :key="stage.id"
                :label="stage.name"
                :value="stage.id ? stage.id.toString() : ''"
                v-for="option in stageOptions"
                :key="option.value"
                :label="option.label"
                :value="option.value"
              />
            </el-select>
          </el-form-item>
@@ -393,7 +396,7 @@
          <el-select v-model="currentStudent.lastStageId" placeholder="请选择阶段" style="width: 100%">
            <el-option label="无" :value="null" />
            <el-option 
              v-for="stage in form.stages"
              v-for="stage in (form?.stages || [])"
              :key="stage.id" 
              :label="stage.name" 
              :value="stage.id"
@@ -413,7 +416,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
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'
@@ -466,17 +469,17 @@
// 评委选择相关
const allJudges = ref([])
const judgeSearchText = ref('')
const selectedStageOption = ref('all')
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 && judge.id && judge.name)
  }
  return allJudges.value.filter(judge => 
    judge.name.toLowerCase().includes(judgeSearchText.value.toLowerCase())
    judge && judge.name && judge.name.toLowerCase().includes(judgeSearchText.value.toLowerCase())
  )
})
@@ -521,6 +524,21 @@
  })
})
// 用于下拉框的阶段选项
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: [
@@ -562,8 +580,7 @@
  try {
    judgeLoading.value = true
    const judges = await getAllJudges()
    allJudges.value = judges || []
    console.log('加载评委列表成功:', allJudges.value.length, '个评委')
    allJudges.value = (judges || []).filter(judge => judge && judge.id && judge.name)
  } catch (error) {
    console.error('加载评委列表失败:', error)
    ElMessage.error('加载评委列表失败: ' + error.message)
@@ -580,6 +597,7 @@
  try {
    loading.value = true
    const activity = await getActivity(route.params.id)
    if (activity) {
      form.value = {
        id: activity.id,
@@ -592,19 +610,15 @@
        playerMax: activity.playerMax,
        state: activity.state,
        stages: activity.stages || [],
        judges: activity.judges || [],
        judges: (activity.judges || []).filter(judge => judge && judge.id && judge.name),
        students: activity.students || [],
        mediaFiles: []
      }
      // 加载并回填已上传媒体:targetType=2 假设为“活动”,如不同请调整
      try {
        const medias = await getMediasByTarget(MediaTargetType.ACTIVITY, 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 = {
@@ -615,16 +629,14 @@
            uploaded: true, // 标记为已上传,不需要重新上传
            file: null // 已保存的文件没有file对象
          }
          console.log('转换后的媒体项:', mediaItem)
          return mediaItem
        })
        console.log('最终的mediaFiles:', form.value.mediaFiles)
      // 设置阶段数量选择器的值
      selectedStageCount.value = form.value.stages.length || 1
      } catch (e) {
        console.error('加载活动媒体失败:', e)
      }
      // 设置阶段数量选择器的值
      selectedStageCount.value = (form.value && form.value.stages) ? form.value.stages.length || 1 : 1
    }
  } catch (error) {
    console.error('加载比赛数据失败:', error)
@@ -637,7 +649,7 @@
// 阶段管理
// 阶段数量变化处理
const onStageCountChange = (count) => {
  if (!count) return
  if (!count || !form.value || !form.value.stages) return
  
  // 如果当前阶段数量少于选择的数量,自动添加阶段
  while (form.value.stages.length < count) {
@@ -666,12 +678,13 @@
// 获取默认阶段名称 - 使用更灵活的命名方式,避免硬编码特定阶段名称
const getDefaultStageName = (index) => {
  // 提供一些常用的阶段名称建议,但不强制使用
  const suggestedNames = ['', '第一阶段', '第二阶段', '第三阶段', '第四阶段', '第五阶段']
  return suggestedNames[index] || `第${index}阶段`
  const suggestedNames = ['第一阶段', '第二阶段', '第三阶段', '第四阶段', '第五阶段']
  return suggestedNames[index - 1] || `第${index}阶段`
}
// 获取阶段在原始数组中的索引
const getOriginalStageIndex = (stage) => {
  if (!form.value || !form.value.stages) return -1
  return form.value.stages.findIndex(s => s === stage)
}
@@ -688,6 +701,8 @@
}
const removeStage = async (index) => {
  if (!form.value || !form.value.stages) return
  try {
    await ElMessageBox.confirm('确定要删除这个阶段吗?', '提示', {
      confirmButtonText: '确定',
@@ -725,6 +740,8 @@
}
const saveStage = async () => {
  if (!form.value || !form.value.stages) return
  try {
    await stageFormRef.value.validate()
    
@@ -777,6 +794,12 @@
// 评委管理
const addJudge = () => {
  // 检查比赛是否已保存
  if (!isEdit.value && !form.value.id) {
    ElMessage.warning('请先保存比赛信息后再添加评委')
    return
  }
  resetJudgeDialog()
  judgeDialogVisible.value = true
}
@@ -787,6 +810,11 @@
}
const removeJudge = async (index) => {
  if (!form.value || !form.value.judges) {
    ElMessage.error('表单数据未初始化')
    return
  }
  try {
    await ElMessageBox.confirm('确定要删除这个评委吗?', '提示', {
      confirmButtonText: '确定',
@@ -801,43 +829,40 @@
}
const getJudgeStages = (judge) => {
  if (!judge.stageIds) return []
  if (!judge.stageIds || !form.value || !form.value.stages) return []
  
  const stages = []
  
  // 只检查比赛阶段
  if (form.value.stages) {
    const matchedStages = form.value.stages.filter(stage => judge.stageIds.includes(stage.id))
    matchedStages.forEach(stage => {
  // 检查比赛阶段
  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 = ''
  // 默认选择第一个阶段
  selectedStageOption.value = form.value.stages && form.value.stages.length > 0
    ? form.value.stages[0].id?.toString() || ''
    : ''
  // 清空阶段选择
  selectedStageOptions.value = []
  selectedJudges.value = []
}
const handleJudgeSearch = (value) => {
  console.log('搜索评委:', value)
  // 搜索评委
}
const handleStageChange = (value) => {
  console.log('选择阶段:', value)
}
const handleJudgeSelectionChange = (value) => {
  console.log('选择评委:', value)
  // 选择评委
}
const toggleSelectAll = () => {
@@ -859,6 +884,17 @@
    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 => {
@@ -867,14 +903,20 @@
      // 检查是否已经存在
      const existingJudge = form.value.judges.find(j => j.id === judgeId)
      if (existingJudge) {
        // 更新现有评委的阶段
        const stageId = parseInt(selectedStageOption.value)
        if (!existingJudge.stageIds.includes(stageId)) {
          existingJudge.stageIds.push(stageId)
        // 更新现有评委的阶段,合并新选择的阶段
        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 = [parseInt(selectedStageOption.value)]
        // 添加新评委,包含所有选择的阶段
        const stageIds = selectedStageOptions.value.length > 0
          ? selectedStageOptions.value.map(id => parseInt(id))
          : []
        
        const newJudge = {
          id: judge.id,
@@ -886,6 +928,10 @@
      }
    }
  })
  // 清空选择
  selectedJudges.value = []
  selectedStageOptions.value = []
  
  judgeDialogVisible.value = false
  ElMessage.success(`成功添加 ${addedCount} 个评委`)
@@ -940,7 +986,7 @@
}
const getLastStage = (student) => {
  if (!student.lastStageId || !form.value.stages) return '无'
  if (!student.lastStageId || !form.value || !form.value.stages) return '无'
  const stage = form.value.stages.find(s => s.id === student.lastStageId)
  return stage ? stage.name : '无'
}
@@ -985,13 +1031,17 @@
    uploaded: false // 标记为未上传
  }
  
  form.value.mediaFiles.push(mediaFile)
  ElMessage.success('文件已选择,点击更新按钮时将上传')
  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
@@ -1056,6 +1106,10 @@
// 处理媒体文件上传
const handleMediaUpload = async (activityId) => {
  if (!form.value || !form.value.mediaFiles) return
  const failedFiles = []
  try {
    for (const mediaFile of form.value.mediaFiles) {
      // 跳过已经有 id 的媒体文件(已保存的)
@@ -1074,10 +1128,8 @@
      }
      
      try {
        console.log('开始上传文件:', mediaFile.name)
        // 1. 上传文件到服务器
        const uploadResult = await uploadFile(mediaFile.file)
        console.log('文件上传成功:', uploadResult)
        
        // 2. 保存媒体信息到数据库
        const mediaInput = {
@@ -1089,11 +1141,7 @@
          targetType: MediaTargetType.ACTIVITY, // 活动
          targetId: parseInt(activityId) // 转换为数字类型
        }
        console.log('准备保存媒体信息:', mediaInput)
        console.log('活动ID:', activityId)
        const savedMedia = await saveMedia(mediaInput)
        console.log(`媒体文件 ${mediaFile.name} 上传并保存成功:`, savedMedia)
        
        // 更新媒体文件信息
        mediaFile.id = savedMedia.id
@@ -1104,28 +1152,41 @@
        mediaFile.uploadResult = uploadResult
      } catch (error) {
        console.error(`媒体文件 ${mediaFile.name} 处理失败:`, error)
        ElMessage.error(`文件 ${mediaFile.name} 上传失败: ${error.message}`)
        // 不抛出错误,继续处理其他文件
        failedFiles.push({
          name: mediaFile.name,
          error: error.message
        })
      }
    }
    // 如果有文件上传失败,抛出错误
    if (failedFiles.length > 0) {
      const failedNames = failedFiles.map(f => f.name).join(', ')
      throw new Error(`以下文件上传失败: ${failedNames}`)
    }
  } catch (error) {
    console.error('媒体文件处理失败:', error)
    // 不影响主流程,只记录错误
    throw error // 抛出错误让上层处理
  }
}
// 提交表单
const handleSubmit = async () => {
  if (submitting.value) return
  if (!form.value) return
  try {
    await formRef.value.validate()
    
    submitting.value = true
    
    console.log('=== 开始保存比赛 ===')
    console.log('form.value:', form.value)
    console.log('form.value.stages:', form.value.stages)
    console.log('form.value.judges:', form.value.judges)
    // 准备保存数据,只包含后端支持的字段
    const saveData = {
      id: form.value.id,
      pid: form.value.pid || 0,
      name: form.value.name,
      description: form.value.description,
      signupDeadline: form.value.signupDeadline,
@@ -1134,25 +1195,70 @@
      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,
        sortOrder: stage.sortOrder,
        state: stage.state || 1
      })) : [],
      judges: form.value.judges ? form.value.judges.map(judge => ({
        judgeId: judge.id,
        judgeName: judge.name,
        stageIds: judge.stageIds || []
      })) : []
      stages: form.value.stages ? (() => {
        console.log('=== 处理stages ===')
        console.log('原始stages:', form.value.stages)
        const filteredStages = form.value.stages.filter(stage => stage)
        console.log('过滤后stages:', filteredStages)
        return filteredStages.map((stage, index) => {
          console.log(`处理stage ${index}:`, 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
          }
          console.log(`处理后stage ${index}:`, stageData)
          return stageData
        })
      })() : [],
      judges: form.value.judges ? (() => {
        console.log('=== 处理judges ===')
        console.log('原始judges:', form.value.judges)
        const filteredJudges = form.value.judges.filter(judge => judge && judge.id && judge.name)
        console.log('过滤后judges:', filteredJudges)
        return filteredJudges.map((judge, index) => {
          console.log(`处理judge ${index}:`, judge)
          const judgeData = {
            judgeId: judge.id,
            judgeName: judge.name,
            stageIds: judge.stageIds || []
          }
          console.log(`处理后judge ${index}:`, judgeData)
          return judgeData
        })
      })() : []
    }
    // 如果是编辑模式,添加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)
    console.log('=== 保存结果 ===')
    console.log('result:', result)
    console.log('result type:', typeof result)
    console.log('result.id:', result?.id)
    console.log('result.id type:', typeof result?.id)
    console.log('JSON.stringify(result):', JSON.stringify(result, null, 2))
    
    // 如果是新增,更新form的id
    if (!isEdit.value && result && result.id) {
@@ -1160,23 +1266,48 @@
    }
    
    // 处理媒体文件上传和保存
    let mediaUploadSuccess = true
    if (form.value.mediaFiles && form.value.mediaFiles.length > 0) {
      const activityId = result.id || form.value.id
      // 修复activityId获取逻辑:优先使用result.id(新增时),其次使用form.value.id(编辑时)
      const activityId = result?.id || form.value.id
      console.log('=== 媒体文件上传 ===')
      console.log('result:', result)
      console.log('form.value.id:', form.value.id)
      console.log('最终activityId:', activityId)
      if (activityId) {
        await handleMediaUpload(activityId)
        try {
          await handleMediaUpload(activityId)
          console.log('媒体文件上传成功')
        } catch (mediaError) {
          console.error('媒体文件上传失败:', mediaError)
          mediaUploadSuccess = false
          ElMessage.warning('比赛保存成功,但部分媒体文件上传失败,请稍后重新编辑添加')
        }
      } else {
        console.error('无法获取activityId,跳过媒体文件上传')
        mediaUploadSuccess = false
        ElMessage.warning('比赛保存成功,但无法获取比赛ID,媒体文件上传失败')
      }
    }
    
    ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
    // 如果是新增,不自动返回,让用户可以继续添加评委和学员
    if (isEdit.value) {
      goBack()
    if (mediaUploadSuccess) {
      ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
    }
    // 保存成功后返回列表页面
    goBack()
  } catch (error) {
    console.error('=== 保存比赛失败 ===')
    console.error('错误对象:', error)
    console.error('错误消息:', error.message)
    console.error('错误堆栈:', error.stack)
    console.error('当前form.value:', form.value)
    if (error.message) {
      console.error('保存比赛失败:', error)
      ElMessage.error('保存失败: ' + error.message)
    } else {
      ElMessage.error('保存失败: 未知错误')
    }
  } finally {
    submitting.value = false
@@ -1195,7 +1326,15 @@
  await loadActivity()
  
  // 如果是新建模式且没有阶段,自动创建一个阶段
  if (!isEdit.value && form.value.stages.length === 0) {
  console.log('检查默认阶段创建条件:', {
    isEdit: isEdit.value,
    hasForm: !!form.value,
    hasStages: !!(form.value && form.value.stages),
    stagesLength: form.value && form.value.stages ? form.value.stages.length : 'undefined'
  })
  if (!isEdit.value && form.value && form.value.stages && form.value.stages.length === 0) {
    console.log('创建默认阶段')
    onStageCountChange(1)
  }
})
@@ -1572,4 +1711,24 @@
  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>