486 lines
12 KiB
Markdown
486 lines
12 KiB
Markdown
# 调度功能实现文档
|
||
|
||
## 📋 实现总结
|
||
|
||
调度功能已经完成后端和前端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. ✅ **用户体验**:提供上移/下移按钮,操作简单直观
|
||
|
||
现在可以开始测试调度功能了!🎉
|