# 编排页面移动按钮功能分析

## 📋 功能概述

编排页面的"移动"按钮允许用户将一个竞赛分组（包含多个参赛人员）从当前的场地和时间段迁移到另一个场地和时间段。

## 🎯 核心功能

### 1. 用户操作流程

```
1. 用户在编排页面查看竞赛分组
   ↓
2. 点击某个分组的"移动"按钮
   ↓
3. 弹出对话框，选择目标场地和目标时间段
   ↓
4. 点击"确定"按钮
   ↓
5. 系统将整个分组迁移到新的场地和时间段
   ↓
6. 前端页面自动更新，分组显示在新位置
```

## 🏗️ 技术架构

### 前端实现

#### 1. 页面结构 ([index.vue:74-87](d:/workspace/31.比赛项目/project/martial-web/src/views/martial/schedule/index.vue#L74-L87))

```vue
<div v-for="(group, index) in filteredCompetitionGroups" :key="group.id" class="competition-group">
  <div class="group-header">
    <div class="group-info">
      <span class="group-title">{{ group.title }}</span>
      <span class="group-meta">{{ group.type }}</span>
      <span class="group-meta">{{ group.count }}</span>
      <span class="group-meta">{{ group.code }}</span>
    </div>
    <div class="group-actions">
      <el-button size="small" type="warning" @click="handleMoveGroup(group)">
        移动
      </el-button>
    </div>
  </div>
  <!-- 分组内的参赛人员表格 -->
</div>
```

**关键点**：
- 每个竞赛分组都有一个"移动"按钮
- 点击按钮触发 `handleMoveGroup(group)` 方法
- 传入整个分组对象作为参数

#### 2. 移动对话框 ([index.vue:198-231](d:/workspace/31.比赛项目/project/martial-web/src/views/martial/schedule/index.vue#L198-L231))

```vue
<el-dialog
  title="移动竞赛分组"
  :visible.sync="moveDialogVisible"
  width="500px"
  center
>
  <el-form label-width="100px">
    <!-- 目标场地选择 -->
    <el-form-item label="目标场地">
      <el-select v-model="moveTargetVenueId" placeholder="请选择场地" style="width: 100%;">
        <el-option
          v-for="venue in venues"
          :key="venue.id"
          :label="venue.venueName"
          :value="venue.id"
        ></el-option>
      </el-select>
    </el-form-item>

    <!-- 目标时间段选择 -->
    <el-form-item label="目标时间段">
      <el-select v-model="moveTargetTimeSlot" placeholder="请选择时间段" style="width: 100%;">
        <el-option
          v-for="(time, index) in timeSlots"
          :key="index"
          :label="time"
          :value="index"
        ></el-option>
      </el-select>
    </el-form-item>
  </el-form>

  <span slot="footer" class="dialog-footer">
    <el-button @click="moveDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="confirmMoveGroup">确定</el-button>
  </span>
</el-dialog>
```

**关键点**：
- 提供两个下拉选择框：目标场地、目标时间段
- 场地列表来自 `venues` 数组（从后端加载）
- 时间段列表来自 `timeSlots` 数组（根据赛事时间动态生成）

#### 3. 数据状态 ([index.vue:299-303](d:/workspace/31.比赛项目/project/martial-web/src/views/martial/schedule/index.vue#L299-L303))

```javascript
// 移动分组相关
moveDialogVisible: false,      // 对话框显示状态
moveTargetVenueId: null,       // 目标场地ID
moveTargetTimeSlot: null,      // 目标时间段索引
moveGroupIndex: null,          // 要移动的分组在数组中的索引
```

#### 4. 核心方法

##### handleMoveGroup - 打开移动对话框 ([index.vue:551-560](d:/workspace/31.比赛项目/project/martial-web/src/views/martial/schedule/index.vue#L551-L560))

```javascript
handleMoveGroup(group) {
  // 1. 检查是否已完成编排
  if (this.isScheduleCompleted) {
    this.$message.warning('编排已完成，无法移动')
    return
  }

  // 2. 记录要移动的分组索引
  this.moveGroupIndex = this.competitionGroups.findIndex(g => g.id === group.id)

  // 3. 预填充当前场地和时间段
  this.moveTargetVenueId = group.venueId || null
  this.moveTargetTimeSlot = group.timeSlotIndex || 0

  // 4. 显示对话框
  this.moveDialogVisible = true
}
```

**逻辑说明**：
1. 检查编排状态，已完成的编排不允许移动
2. 找到分组在数组中的索引位置
3. 将当前分组的场地和时间段作为默认值
4. 打开移动对话框

##### confirmMoveGroup - 确认移动 ([index.vue:563-600](d:/workspace/31.比赛项目/project/martial-web/src/views/martial/schedule/index.vue#L563-L600))

```javascript
async confirmMoveGroup() {
  // 1. 验证输入
  if (!this.moveTargetVenueId) {
    this.$message.warning('请选择目标场地')
    return
  }
  if (this.moveTargetTimeSlot === null) {
    this.$message.warning('请选择目标时间段')
    return
  }

  // 2. 获取分组和目标场地信息
  const group = this.competitionGroups[this.moveGroupIndex]
  const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)

  try {
    // 3. 调用后端API移动分组
    const res = await moveScheduleGroup({
      groupId: group.id,
      targetVenueId: this.moveTargetVenueId,
      targetTimeSlotIndex: this.moveTargetTimeSlot
    })

    if (res.data.success) {
      // 4. 更新前端数据
      group.venueId = this.moveTargetVenueId
      group.venueName = targetVenue ? targetVenue.venueName : ''
      group.timeSlotIndex = this.moveTargetTimeSlot
      group.timeSlot = this.timeSlots[this.moveTargetTimeSlot]

      // 5. 显示成功提示
      this.$message.success(`已移动到 ${group.venueName} - ${group.timeSlot}`)
      this.moveDialogVisible = false
    } else {
      this.$message.error(res.data.msg || '移动分组失败')
    }
  } catch (error) {
    console.error('移动分组失败:', error)
    this.$message.error('移动分组失败，请稍后重试')
  }
}
```

**逻辑说明**：
1. **验证输入**：确保选择了目标场地和时间段
2. **获取数据**：获取要移动的分组和目标场地信息
3. **调用API**：发送移动请求到后端
4. **更新前端**：成功后更新分组的场地和时间信息
5. **用户反馈**：显示成功或失败提示

---

### 后端实现

#### 1. API接口 ([activitySchedule.js:124-136](d:/workspace/31.比赛项目/project/martial-web/src/api/martial/activitySchedule.js#L124-L136))

```javascript
/**
 * 移动赛程分组到指定场地和时间段
 * @param {Object} data - 移动请求数据
 * @param {Number} data.groupId - 分组ID
 * @param {Number} data.targetVenueId - 目标场地ID
 * @param {Number} data.targetTimeSlotIndex - 目标时间段索引
 */
export const moveScheduleGroup = (data) => {
  return request({
    url: '/martial/schedule/move-group',
    method: 'post',
    data
  })
}
```

#### 2. Controller层 ([MartialScheduleArrangeController.java:106-119](d:/workspace/31.比赛项目/project/martial-master/src/main/java/org/springblade/modules/martial/controller/MartialScheduleArrangeController.java#L106-L119))

```java
/**
 * 移动赛程分组
 */
@PostMapping("/move-group")
@Operation(summary = "移动赛程分组", description = "将分组移动到指定场地和时间段")
public R moveGroup(@RequestBody MoveScheduleGroupDTO dto) {
    try {
        boolean success = scheduleService.moveScheduleGroup(dto);
        return success ? R.success("分组移动成功") : R.fail("分组移动失败");
    } catch (Exception e) {
        log.error("移动分组失败", e);
        return R.fail("移动分组失败: " + e.getMessage());
    }
}
```

#### 3. DTO对象 ([MoveScheduleGroupDTO.java](d:/workspace/31.比赛项目/project/martial-master/src/main/java/org/springblade/modules/martial/pojo/dto/MoveScheduleGroupDTO.java))

```java
@Data
@Schema(description = "移动赛程分组DTO")
public class MoveScheduleGroupDTO {

    /**
     * 分组ID
     */
    @Schema(description = "分组ID")
    private Long groupId;

    /**
     * 目标场地ID
     */
    @Schema(description = "目标场地ID")
    private Long targetVenueId;

    /**
     * 目标时间段索引
     */
    @Schema(description = "目标时间段索引(0=第1天上午,1=第1天下午,2=第2天上午...)")
    private Integer targetTimeSlotIndex;
}
```

**关键点**：
- `groupId`: 要移动的分组ID
- `targetVenueId`: 目标场地ID
- `targetTimeSlotIndex`: 目标时间段索引（0=第1天上午，1=第1天下午，2=第2天上午...）

#### 4. Service层实现 ([MartialScheduleServiceImpl.java:394-452](d:/workspace/31.比赛项目/project/martial-master/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java#L394-L452))

```java
@Override
public boolean moveScheduleGroup(MoveScheduleGroupDTO dto) {
    // 1. 查询分组信息
    MartialScheduleGroup group = scheduleGroupMapper.selectById(dto.getGroupId());
    if (group == null) {
        throw new RuntimeException("分组不存在");
    }

    // 2. 查询该分组的详情记录（包含所有参赛人员）
    List<MartialScheduleDetail> details = scheduleDetailMapper.selectList(
        new QueryWrapper<MartialScheduleDetail>()
            .eq("schedule_group_id", dto.getGroupId())
            .eq("is_deleted", 0)
    );

    if (details.isEmpty()) {
        throw new RuntimeException("分组详情不存在");
    }

    // 3. 查询目标场地信息
    MartialVenue targetVenue = venueService.getById(dto.getTargetVenueId());
    if (targetVenue == null) {
        throw new RuntimeException("目标场地不存在");
    }

    // 4. 根据时间段索引计算日期和时间
    // 假设: 0=第1天上午, 1=第1天下午, 2=第2天上午, 3=第2天下午...
    int dayOffset = dto.getTargetTimeSlotIndex() / 2; // 每天2个时段
    boolean isAfternoon = dto.getTargetTimeSlotIndex() % 2 == 1;
    String timeSlot = isAfternoon ? "13:30" : "08:30";

    // 获取赛事起始日期（从第一个detail中获取）
    LocalDate baseDate = details.get(0).getScheduleDate();
    if (baseDate == null) {
        throw new RuntimeException("无法确定赛事起始日期");
    }

    // 计算目标日期
    LocalDate minDate = details.stream()
        .map(MartialScheduleDetail::getScheduleDate)
        .filter(Objects::nonNull)
        .min(LocalDate::compareTo)
        .orElse(baseDate);

    LocalDate targetDate = minDate.plusDays(dayOffset);

    // 5. 更新所有detail记录
    for (MartialScheduleDetail detail : details) {
        detail.setVenueId(dto.getTargetVenueId());
        detail.setVenueName(targetVenue.getVenueName());
        detail.setScheduleDate(targetDate);
        detail.setTimeSlot(timeSlot);
        detail.setTimeSlotIndex(dto.getTargetTimeSlotIndex());
        scheduleDetailMapper.updateById(detail);
    }

    return true;
}
```

**核心逻辑**：

1. **查询分组信息**
   - 验证分组是否存在

2. **查询分组详情**
   - 获取该分组下的所有参赛人员记录（`MartialScheduleDetail`）
   - 这是关键：一个分组包含多个参赛人员

3. **查询目标场地**
   - 验证目标场地是否存在
   - 获取场地名称

4. **计算目标日期和时间**
   - 根据时间段索引计算天数偏移：`dayOffset = targetTimeSlotIndex / 2`
   - 判断上午/下午：`isAfternoon = targetTimeSlotIndex % 2 == 1`
   - 设置时间：上午 08:30，下午 13:30
   - 计算目标日期：`targetDate = minDate.plusDays(dayOffset)`

5. **批量更新所有详情记录**
   - 遍历分组下的所有参赛人员
   - 更新每个人的场地、日期、时间信息
   - 这样整个分组就迁移到了新的场地和时间段

---

## 📊 数据流转图

```
前端用户操作
    ↓
handleMoveGroup(group)
    ↓
显示移动对话框
    ↓
用户选择目标场地和时间段
    ↓
confirmMoveGroup()
    ↓
调用API: moveScheduleGroup({
    groupId,
    targetVenueId,
    targetTimeSlotIndex
})
    ↓
后端Controller: moveGroup()
    ↓
后端Service: moveScheduleGroup()
    ↓
1. 查询分组信息
2. 查询分组详情（所有参赛人员）
3. 查询目标场地信息
4. 计算目标日期和时间
5. 批量更新所有详情记录
    ↓
返回成功/失败
    ↓
前端更新分组数据
    ↓
页面自动刷新显示
```

---

## 🔑 关键数据结构

### 1. 竞赛分组（CompetitionGroup）

```javascript
{
  id: 1,                          // 分组ID
  title: "男子A组 长拳",           // 分组标题
  type: "个人项目",                // 项目类型
  count: "5人",                    // 参赛人数
  code: "MA-001",                  // 分组编号
  venueId: 1,                      // 当前场地ID
  venueName: "主场地",             // 当前场地名称
  timeSlotIndex: 0,                // 当前时间段索引
  timeSlot: "2025年11月6日 上午8:30", // 当前时间段
  items: [                         // 参赛人员列表
    {
      id: 101,
      schoolUnit: "北京体育大学",
      status: "已签到"
    },
    // ... 更多参赛人员
  ]
}
```

### 2. 场地（Venue）

```javascript
{
  id: 1,
  venueName: "主场地",
  venueLocation: "体育馆1层",
  capacity: 100
}
```

### 3. 时间段（TimeSlot）

```javascript
timeSlots: [
  "2025年11月6日 上午8:30",   // index: 0
  "2025年11月6日 下午13:30",  // index: 1
  "2025年11月7日 上午8:30",   // index: 2
  "2025年11月7日 下午13:30",  // index: 3
  // ...
]
```

**时间段索引规则**：
- `index = dayOffset * 2 + (isAfternoon ? 1 : 0)`
- 例如：第2天下午 = 1 * 2 + 1 = 3

---

## 🎨 UI交互流程

### 1. 初始状态
```
编排页面
├── 场地选择按钮（主场地、副场地1、副场地2）
├── 时间段选择按钮（上午8:30、下午13:30）
└── 竞赛分组列表
    ├── 分组1 [移动] 按钮
    ├── 分组2 [移动] 按钮
    └── 分组3 [移动] 按钮
```

### 2. 点击移动按钮
```
弹出对话框
├── 标题：移动竞赛分组
├── 目标场地下拉框
│   ├── 主场地
│   ├── 副场地1
│   └── 副场地2
├── 目标时间段下拉框
│   ├── 2025年11月6日 上午8:30
│   ├── 2025年11月6日 下午13:30
│   └── ...
└── 按钮
    ├── [取消]
    └── [确定]
```

### 3. 确认移动后
```
页面自动更新
├── 原场地/时间段：分组消失
└── 新场地/时间段：分组出现
```

---

## ⚠️ 注意事项

### 1. 权限控制
- ✅ 已完成编排的赛程不允许移动
- ✅ 检查：`if (this.isScheduleCompleted) { return }`

### 2. 数据一致性
- ✅ 移动时更新所有参赛人员的场地和时间信息
- ✅ 前端和后端数据同步更新

### 3. 用户体验
- ✅ 预填充当前场地和时间段
- ✅ 显示清晰的成功/失败提示
- ✅ 对话框关闭后自动刷新页面

### 4. 错误处理
- ✅ 分组不存在
- ✅ 场地不存在
- ✅ 时间段无效
- ✅ 网络请求失败

---

## 🚀 实现要点总结

### 前端关键点

1. **分组数据管理**
   - 使用 `competitionGroups` 数组存储所有分组
   - 使用 `filteredCompetitionGroups` 计算属性过滤显示

2. **对话框状态管理**
   - `moveDialogVisible`: 控制对话框显示
   - `moveTargetVenueId`: 目标场地ID
   - `moveTargetTimeSlot`: 目标时间段索引
   - `moveGroupIndex`: 要移动的分组索引

3. **数据更新策略**
   - 后端更新成功后，前端同步更新分组数据
   - 利用Vue的响应式特性自动刷新页面

### 后端关键点

1. **批量更新**
   - 一次移动操作更新整个分组的所有参赛人员
   - 使用循环遍历 `details` 列表批量更新

2. **时间计算**
   - 根据时间段索引计算天数偏移和上午/下午
   - 使用 `LocalDate.plusDays()` 计算目标日期

3. **数据验证**
   - 验证分组、场地、时间段的有效性
   - 抛出异常进行错误处理

---

## 📝 扩展建议

### 1. 功能增强

- **批量移动**：支持选择多个分组一次性移动
- **拖拽移动**：支持拖拽分组到目标位置
- **冲突检测**：检测目标场地和时间段是否已满
- **历史记录**：记录移动操作历史，支持撤销

### 2. 性能优化

- **防抖处理**：避免频繁点击导致重复请求
- **乐观更新**：先更新前端，后台异步同步
- **缓存机制**：缓存场地和时间段列表

### 3. 用户体验

- **移动预览**：显示移动后的效果预览
- **快捷操作**：右键菜单快速移动
- **智能推荐**：推荐合适的目标场地和时间段

---

## 🎯 总结

移动按钮功能的核心是**将整个竞赛分组（包含多个参赛人员）从一个场地和时间段迁移到另一个场地和时间段**。

**实现关键**：
1. 前端提供友好的对话框选择目标位置
2. 后端批量更新分组下所有参赛人员的场地和时间信息
3. 前后端数据同步，确保页面实时更新

**数据流转**：
```
用户点击移动 → 选择目标 → 调用API → 批量更新数据库 → 返回结果 → 更新前端 → 页面刷新
```

这个功能设计合理，实现清晰，用户体验良好！✨
