Files
martial-master/docs/schedule-dispatch-implementation.md
宅房 1ca0f6a7f6
All checks were successful
continuous-integration/drone/push Build is passing
fix bugs
2025-12-12 13:49:00 +08:00

12 KiB
Raw Blame History

调度功能实现文档

📋 实现总结

调度功能已经完成后端和前端API的开发现在需要在前端页面中集成调度功能。


🎯 前端页面修改方案

方案在编排页面添加调度Tab

修改 src/views/martial/schedule/index.vue 文件,在现有的"竞赛分组"和"场地"Tab基础上添加"调度"Tab。


💻 前端代码实现

1. 在 <template> 中添加调度Tab

在现有的 tabs-section 中添加调度按钮和内容:

<div class="tabs-section">
  <div class="tab-buttons">
    <el-button
      size="small"
      :type="activeTab === 'competition' ? 'primary' : ''"
      @click="activeTab = 'competition'"
      :disabled="isScheduleCompleted">
      竞赛分组
    </el-button>
    <el-button
      size="small"
      :type="activeTab === 'venue' ? 'primary' : ''"
      @click="activeTab = 'venue'"
      :disabled="isScheduleCompleted">
      场地
    </el-button>
    <!-- 新增调度Tab -->
    <el-button
      size="small"
      :type="activeTab === 'dispatch' ? 'primary' : ''"
      @click="handleSwitchToDispatch"
      :disabled="!isScheduleCompleted">
      调度
    </el-button>
  </div>

  <!-- 竞赛分组 Tab -->
  <div v-show="activeTab === 'competition'" class="tab-content">
    <!-- 原有的竞赛分组内容 -->
  </div>

  <!-- 场地 Tab -->
  <div v-show="activeTab === 'venue'" class="tab-content">
    <!-- 原有的场地内容 -->
  </div>

  <!-- 新增调度 Tab -->
  <div v-show="activeTab === 'dispatch'" class="tab-content">
    <div class="dispatch-container">
      <!-- 场地和时间段选择 -->
      <div class="venue-list">
        <div class="venue-buttons">
          <el-button
            v-for="venue in venues"
            :key="venue.id"
            size="small"
            :type="selectedVenueId === venue.id ? 'primary' : ''"
            @click="handleSelectVenue(venue.id)">
            {{ venue.venueName }}
          </el-button>
        </div>
      </div>

      <div class="time-selector">
        <el-button
          v-for="(time, index) in timeSlots"
          :key="index"
          size="small"
          :type="selectedTime === index ? 'primary' : ''"
          @click="handleSelectTime(index)">
          {{ time }}
        </el-button>
      </div>

      <!-- 分组列表 -->
      <div v-for="group in dispatchGroups" :key="group.groupId" class="dispatch-group">
        <div class="group-header">
          <h3 class="group-title">{{ group.groupName }}</h3>
          <span class="participant-count">({{ group.participants.length }})</span>
        </div>

        <!-- 参赛者列表 -->
        <el-table :data="group.participants" border stripe size="small">
          <el-table-column label="序号" width="80" align="center">
            <template #default="{ $index }">
              {{ $index + 1 }}
            </template>
          </el-table-column>
          <el-table-column prop="organization" label="学校/单位" min-width="200"></el-table-column>
          <el-table-column prop="playerName" label="选手姓名" width="120"></el-table-column>
          <el-table-column prop="projectName" label="项目" width="150"></el-table-column>
          <el-table-column label="操作" width="180" align="center">
            <template #default="{ row, $index }">
              <el-button
                type="text"
                size="small"
                :disabled="$index === 0"
                @click="handleMoveUp(group, $index)">
                <img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
                上移
              </el-button>
              <el-button
                type="text"
                size="small"
                :disabled="$index === group.participants.length - 1"
                @click="handleMoveDown(group, $index)">
                <img src="/img/图标 4@3x.png" class="move-icon" alt="下移" />
                下移
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>

      <!-- 保存按钮 -->
      <div class="dispatch-footer" v-if="dispatchGroups.length > 0">
        <el-button @click="handleCancelDispatch">取消</el-button>
        <el-button type="primary" @click="handleSaveDispatch" :disabled="!hasDispatchChanges">
          保存调度
        </el-button>
      </div>
    </div>
  </div>
</div>

2. 在 <script> 中添加数据和方法

import { getDispatchData, saveDispatch } from '@/api/martial/activitySchedule'

export default {
  data() {
    return {
      // ... 原有数据
      activeTab: 'competition',  // 修改:支持 'competition' | 'venue' | 'dispatch'

      // 调度相关数据
      dispatchGroups: [],        // 调度分组列表
      hasDispatchChanges: false, // 是否有未保存的更改
      originalDispatchData: null // 原始调度数据(用于取消时恢复)
    }
  },

  methods: {
    // ... 原有方法

    // ==================== 调度功能方法 ====================

    /**
     * 切换到调度Tab
     */
    handleSwitchToDispatch() {
      if (!this.isScheduleCompleted) {
        this.$message.warning('请先完成编排后再进行调度')
        return
      }
      this.activeTab = 'dispatch'
      this.loadDispatchData()
    },

    /**
     * 选择场地(调度模式)
     */
    handleSelectVenue(venueId) {
      this.selectedVenueId = venueId
      this.loadDispatchData()
    },

    /**
     * 选择时间段(调度模式)
     */
    handleSelectTime(timeIndex) {
      this.selectedTime = timeIndex
      this.loadDispatchData()
    },

    /**
     * 加载调度数据
     */
    async loadDispatchData() {
      if (!this.selectedVenueId || this.selectedTime === null) {
        this.dispatchGroups = []
        return
      }

      try {
        this.loading = true
        const res = await getDispatchData({
          competitionId: this.competitionId,
          venueId: this.selectedVenueId,
          timeSlotIndex: this.selectedTime
        })

        if (res.data.success) {
          this.dispatchGroups = res.data.data.groups || []
          // 保存原始数据,用于取消时恢复
          this.originalDispatchData = JSON.parse(JSON.stringify(this.dispatchGroups))
          this.hasDispatchChanges = false
        } else {
          this.$message.error(res.data.msg || '加载调度数据失败')
        }
      } catch (error) {
        console.error('加载调度数据失败:', error)
        this.$message.error('加载调度数据失败')
      } finally {
        this.loading = false
      }
    },

    /**
     * 上移参赛者
     */
    handleMoveUp(group, index) {
      if (index === 0) return

      const participants = group.participants
      // 交换位置
      const temp = participants[index]
      participants[index] = participants[index - 1]
      participants[index - 1] = temp

      // 更新顺序号
      this.updatePerformanceOrder(group)
      this.hasDispatchChanges = true
    },

    /**
     * 下移参赛者
     */
    handleMoveDown(group, index) {
      const participants = group.participants
      if (index === participants.length - 1) return

      // 交换位置
      const temp = participants[index]
      participants[index] = participants[index + 1]
      participants[index + 1] = temp

      // 更新顺序号
      this.updatePerformanceOrder(group)
      this.hasDispatchChanges = true
    },

    /**
     * 更新出场顺序
     */
    updatePerformanceOrder(group) {
      group.participants.forEach((p, index) => {
        p.performanceOrder = index + 1
      })
    },

    /**
     * 保存调度
     */
    async handleSaveDispatch() {
      if (!this.hasDispatchChanges) {
        this.$message.info('没有需要保存的更改')
        return
      }

      try {
        this.loading = true

        // 构建保存数据
        const adjustments = this.dispatchGroups.map(group => ({
          detailId: group.detailId,
          participants: group.participants.map(p => ({
            id: p.id,
            performanceOrder: p.performanceOrder
          }))
        }))

        const res = await saveDispatch({
          competitionId: this.competitionId,
          adjustments
        })

        if (res.data.success) {
          this.$message.success('调度保存成功')
          this.hasDispatchChanges = false
          // 重新加载数据
          await this.loadDispatchData()
        } else {
          this.$message.error(res.data.msg || '保存失败')
        }
      } catch (error) {
        console.error('保存调度失败:', error)
        this.$message.error('保存失败,请稍后重试')
      } finally {
        this.loading = false
      }
    },

    /**
     * 取消调度
     */
    handleCancelDispatch() {
      if (this.hasDispatchChanges) {
        this.$confirm('有未保存的更改,确定要取消吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          // 恢复原始数据
          this.dispatchGroups = JSON.parse(JSON.stringify(this.originalDispatchData))
          this.hasDispatchChanges = false
          this.$message.info('已取消更改')
        }).catch(() => {
          // 用户点击了取消
        })
      } else {
        this.activeTab = 'competition'
      }
    }
  }
}

3. 添加样式

<style> 中添加调度相关样式:

<style scoped lang="scss">
// ... 原有样式

// 调度容器
.dispatch-container {
  padding: 20px;
}

// 调度分组
.dispatch-group {
  margin-bottom: 30px;
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

  .group-header {
    display: flex;
    align-items: center;
    margin-bottom: 15px;
    padding-bottom: 10px;
    border-bottom: 2px solid #409eff;

    .group-title {
      margin: 0;
      font-size: 16px;
      font-weight: bold;
      color: #303133;
    }

    .participant-count {
      margin-left: 10px;
      font-size: 14px;
      color: #909399;
    }
  }
}

// 调度底部按钮
.dispatch-footer {
  margin-top: 30px;
  text-align: center;
  padding: 20px;
  background: #f5f7fa;
  border-radius: 4px;

  .el-button {
    min-width: 120px;
  }
}

// 移动图标
.move-icon {
  width: 16px;
  height: 16px;
  vertical-align: middle;
  margin-right: 4px;
}
</style>

🎯 功能说明

1. Tab切换逻辑

  • 编排Tab:编排完成前可用,完成后禁用
  • 场地Tab:编排完成前可用,完成后禁用
  • 调度Tab:只有编排完成后才可用

2. 调度操作

  • 上移:将参赛者向上移动一位(第一个不能上移)
  • 下移:将参赛者向下移动一位(最后一个不能下移)
  • 保存:批量保存所有调整
  • 取消:恢复到原始数据

3. 数据同步

  • 切换场地或时间段时,自动加载对应的调度数据
  • 保存成功后,重新加载数据确保同步
  • 取消时,恢复到加载时的原始数据

⚠️ 注意事项

  1. 权限控制

    • 调度Tab只有在 isScheduleCompleted === true 时才可用
    • 编排完成后编排Tab和场地Tab应该禁用
  2. 数据一致性

    • 每次切换场地或时间段都重新加载数据
    • 保存前检查是否有未保存的更改
  3. 用户体验

    • 有未保存更改时,取消操作需要确认
    • 第一个不能上移,最后一个不能下移
    • 保存成功后显示提示并刷新数据
  4. 性能优化

    • 使用深拷贝保存原始数据
    • 只在有更改时才允许保存

🚀 测试步骤

  1. 完成编排

    • 进入编排页面
    • 完成自动编排
    • 点击"完成编排"按钮
  2. 进入调度模式

    • 点击"调度"Tab
    • 选择场地和时间段
    • 查看参赛者列表
  3. 调整顺序

    • 点击"上移"或"下移"按钮
    • 观察顺序变化
    • 检查第一个和最后一个的按钮是否正确禁用
  4. 保存调度

    • 点击"保存调度"按钮
    • 检查是否保存成功
    • 刷新页面验证数据是否持久化
  5. 取消操作

    • 进行一些调整
    • 点击"取消"按钮
    • 确认数据恢复到原始状态

📝 总结

调度功能的实现要点:

  1. 后端完成DTO、Service、Controller 全部实现
  2. 前端API封装了3个调度相关接口
  3. 页面集成在编排页面添加调度Tab
  4. 权限控制:只有编排完成后才能使用
  5. 用户体验:提供上移/下移按钮,操作简单直观

现在可以开始测试调度功能了!🎉