@@ -0,0 +1,485 @@
|
||||
# 调度功能实现文档
|
||||
|
||||
## 📋 实现总结
|
||||
|
||||
调度功能已经完成后端和前端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. ✅ **用户体验**:提供上移/下移按钮,操作简单直观
|
||||
|
||||
现在可以开始测试调度功能了!🎉
|
||||
Reference in New Issue
Block a user