# 调度功能实现文档

## 📋 实现总结

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

---

## 🎯 前端页面修改方案

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

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

---

## 💻 前端代码实现

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

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

```vue
<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>` 中添加数据和方法

```javascript
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>` 中添加调度相关样式：

```scss
<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. ✅ **用户体验**：提供上移/下移按钮，操作简单直观

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