This commit is contained in:
@@ -15,7 +15,23 @@
|
||||
"Bash(mvn clean compile:*)",
|
||||
"Bash(mvn clean package:*)",
|
||||
"Bash(mvn package:*)",
|
||||
"Bash(findstr:*)"
|
||||
"Bash(findstr:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(mysql:*)",
|
||||
"Bash(netstat:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(powershell -Command:*)",
|
||||
"Bash(mvn spring-boot:run:*)",
|
||||
"Bash(timeout /t 2)",
|
||||
"Bash(ping:*)",
|
||||
"Bash(mvn compile:*)",
|
||||
"Bash(timeout /t 25)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(timeout /t 5)",
|
||||
"Bash(paste:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(node:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
581
doc/评委邀请码功能实现指南.md
Normal file
581
doc/评委邀请码功能实现指南.md
Normal file
@@ -0,0 +1,581 @@
|
||||
# 评委邀请码功能实现指南
|
||||
|
||||
> **实施日期**: 2025-12-12
|
||||
> **页面路径**: `src/views/martial/judgeInvite/index.vue`
|
||||
> **与赛事绑定**: ✅ 已通过 `competitionId` 实现
|
||||
|
||||
---
|
||||
|
||||
## 📋 实现方案
|
||||
|
||||
### 一、需求分析
|
||||
|
||||
根据文档,评委邀请码功能需要实现:
|
||||
|
||||
1. **单个生成**:为单个评委生成6位邀请码
|
||||
2. **批量生成**:为多个评委批量生成邀请码
|
||||
3. **重新生成**:已有邀请码时可重新生成
|
||||
4. **复制功能**:点击邀请码可复制
|
||||
5. **赛事绑定**:所有操作都与选中的赛事绑定
|
||||
|
||||
### 二、后端接口(已完成)
|
||||
|
||||
后端接口已在 `src/api/martial/judgeInvite.js` 中添加:
|
||||
|
||||
```javascript
|
||||
// 1. 生成邀请码
|
||||
export const generateInviteCode = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/generate',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 批量生成邀请码
|
||||
export const batchGenerateInviteCode = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/generate/batch',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 重新生成邀请码
|
||||
export const regenerateInviteCode = (inviteId) => {
|
||||
return request({
|
||||
url: `/api/blade-martial/judgeInvite/regenerate/${inviteId}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
// 4. 查询评委邀请码
|
||||
export const getInviteByJudge = (competitionId, judgeId) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/byJudge',
|
||||
method: 'get',
|
||||
params: { competitionId, judgeId }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、前端实现步骤
|
||||
|
||||
### 步骤1:导入新增的API
|
||||
|
||||
在 `src/views/martial/judgeInvite/index.vue` 第281-292行,修改导入语句:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
getJudgeInviteList,
|
||||
sendInvite,
|
||||
batchSendInvites,
|
||||
resendInvite,
|
||||
cancelInvite,
|
||||
confirmInvite,
|
||||
getInviteStatistics,
|
||||
importFromJudgePool,
|
||||
exportInvites,
|
||||
sendReminder,
|
||||
generateInviteCode, // 新增
|
||||
batchGenerateInviteCode, // 新增
|
||||
regenerateInviteCode // 新增
|
||||
} from '@/api/martial/judgeInvite'
|
||||
```
|
||||
|
||||
### 步骤2:修改邀请码列显示(第165-179行)
|
||||
|
||||
将现有的邀请码列替换为:
|
||||
|
||||
```vue
|
||||
<el-table-column prop="inviteCode" label="邀请码" width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<!-- 已有邀请码:显示邀请码 + 重新生成按钮 -->
|
||||
<div v-if="row.inviteCode" style="display: flex; align-items: center; justify-content: center; gap: 8px;">
|
||||
<el-tag
|
||||
type="warning"
|
||||
effect="dark"
|
||||
size="default"
|
||||
style="font-family: monospace; font-weight: bold; cursor: pointer;"
|
||||
@click="copyToClipboard(row.inviteCode, '邀请码')"
|
||||
title="点击复制"
|
||||
>
|
||||
{{ row.inviteCode }}
|
||||
</el-tag>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleRegenerateCode(row)"
|
||||
title="重新生成邀请码"
|
||||
>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 未生成邀请码:显示生成按钮 -->
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleGenerateCode(row)"
|
||||
>
|
||||
生成邀请码
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
### 步骤3:添加批量生成按钮(第129-131行)
|
||||
|
||||
修改工具栏的"批量邀请"按钮功能:
|
||||
|
||||
```vue
|
||||
<el-button type="success" :icon="DocumentCopy" @click="handleBatchGenerateCode">
|
||||
批量生成邀请码
|
||||
</el-button>
|
||||
```
|
||||
|
||||
### 步骤4:添加方法实现
|
||||
|
||||
在 `<script setup>` 部分,在第456行之后添加以下方法:
|
||||
|
||||
```javascript
|
||||
// ==================== 邀请码生成功能 ====================
|
||||
|
||||
/**
|
||||
* 生成单个邀请码
|
||||
*/
|
||||
const handleGenerateCode = async (row) => {
|
||||
if (!queryParams.competitionId) {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await generateInviteCode({
|
||||
competitionId: queryParams.competitionId,
|
||||
judgeId: row.judgeId,
|
||||
role: row.refereeType === 1 ? 'chief_judge' : 'judge', // 根据评委类型设置角色
|
||||
venueId: row.venueId || null,
|
||||
projects: row.projects ? JSON.stringify(row.projects) : null,
|
||||
expireDays: 30
|
||||
})
|
||||
|
||||
if (res.data && res.data.inviteCode) {
|
||||
ElMessage.success(`邀请码生成成功:${res.data.inviteCode}`)
|
||||
// 自动复制到剪贴板
|
||||
copyToClipboard(res.data.inviteCode, '邀请码')
|
||||
// 刷新列表
|
||||
await fetchData()
|
||||
await loadStatistics()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '生成失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成邀请码失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || error.message || '生成邀请码失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成邀请码
|
||||
*/
|
||||
const handleRegenerateCode = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'重新生成后,旧邀请码将失效。确定继续吗?',
|
||||
'重新生成邀请码',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
const res = await regenerateInviteCode(row.id)
|
||||
|
||||
if (res.data && res.data.inviteCode) {
|
||||
ElMessage.success(`邀请码已重新生成:${res.data.inviteCode}`)
|
||||
// 自动复制到剪贴板
|
||||
copyToClipboard(res.data.inviteCode, '邀请码')
|
||||
// 刷新列表
|
||||
await fetchData()
|
||||
await loadStatistics()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '重新生成失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('重新生成邀请码失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || error.message || '重新生成邀请码失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成邀请码
|
||||
*/
|
||||
const handleBatchGenerateCode = async () => {
|
||||
if (!queryParams.competitionId) {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
|
||||
if (selection.value.length === 0) {
|
||||
ElMessage.warning('请先选择评委')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定为选中的 ${selection.value.length} 位评委批量生成邀请码吗?`,
|
||||
'批量生成邀请码',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
const judgeIds = selection.value.map(item => item.judgeId)
|
||||
|
||||
const res = await batchGenerateInviteCode({
|
||||
competitionId: queryParams.competitionId,
|
||||
judgeIds: judgeIds,
|
||||
role: 'judge',
|
||||
expireDays: 30
|
||||
})
|
||||
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
ElMessage.success(`成功生成 ${res.data.length} 个邀请码`)
|
||||
// 刷新列表
|
||||
await fetchData()
|
||||
await loadStatistics()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '批量生成失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('批量生成邀请码失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || error.message || '批量生成邀请码失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、数据流转说明
|
||||
|
||||
### 1. 赛事绑定流程
|
||||
|
||||
```
|
||||
用户操作:
|
||||
1. 选择赛事(下拉框)
|
||||
↓
|
||||
2. queryParams.competitionId 更新
|
||||
↓
|
||||
3. 触发 handleCompetitionChange
|
||||
↓
|
||||
4. 加载该赛事的评委列表
|
||||
↓
|
||||
5. 显示评委及其邀请码状态
|
||||
```
|
||||
|
||||
### 2. 生成邀请码流程
|
||||
|
||||
```
|
||||
单个生成:
|
||||
1. 点击"生成邀请码"按钮
|
||||
↓
|
||||
2. 调用 generateInviteCode API
|
||||
↓
|
||||
3. 传入:
|
||||
- competitionId: 当前选中的赛事ID
|
||||
- judgeId: 评委ID
|
||||
- role: 评委角色
|
||||
- venueId: 场地ID(可选)
|
||||
- projects: 项目列表(可选)
|
||||
- expireDays: 30天
|
||||
↓
|
||||
4. 后端生成6位随机码
|
||||
↓
|
||||
5. 返回邀请码
|
||||
↓
|
||||
6. 前端自动复制到剪贴板
|
||||
↓
|
||||
7. 刷新列表显示
|
||||
```
|
||||
|
||||
### 3. 批量生成流程
|
||||
|
||||
```
|
||||
批量生成:
|
||||
1. 选择多个评委(勾选)
|
||||
↓
|
||||
2. 点击"批量生成邀请码"按钮
|
||||
↓
|
||||
3. 确认操作
|
||||
↓
|
||||
4. 调用 batchGenerateInviteCode API
|
||||
↓
|
||||
5. 传入:
|
||||
- competitionId: 当前选中的赛事ID
|
||||
- judgeIds: 评委ID数组
|
||||
- role: 'judge'
|
||||
- expireDays: 30天
|
||||
↓
|
||||
6. 后端循环为每个评委生成邀请码
|
||||
↓
|
||||
7. 返回生成的邀请码列表
|
||||
↓
|
||||
8. 刷新列表显示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、关键字段说明
|
||||
|
||||
### 1. 数据表字段(martial_judge_invite)
|
||||
|
||||
| 字段 | 类型 | 说明 | 示例值 |
|
||||
|------|------|------|--------|
|
||||
| `id` | bigint | 主键ID | 1001 |
|
||||
| `competition_id` | bigint | **赛事ID(绑定)** | 1 |
|
||||
| `judge_id` | bigint | 评委ID | 5 |
|
||||
| `invite_code` | varchar(50) | 邀请码 | ABC123 |
|
||||
| `role` | varchar(20) | 角色 | judge/chief_judge |
|
||||
| `venue_id` | bigint | 场地ID | 2 |
|
||||
| `projects` | varchar(500) | 项目列表 | ["太极拳","长拳"] |
|
||||
| `expire_time` | datetime | 过期时间 | 2026-01-11 |
|
||||
| `is_used` | int | 是否已使用 | 0/1 |
|
||||
| `status` | int | 状态 | 1-启用,0-禁用 |
|
||||
|
||||
### 2. 前端查询参数
|
||||
|
||||
```javascript
|
||||
queryParams = {
|
||||
current: 1, // 当前页
|
||||
size: 10, // 每页条数
|
||||
competitionId: '', // ⭐ 赛事ID(核心绑定字段)
|
||||
judgeName: '', // 评委姓名
|
||||
judgeLevel: '', // 评委等级
|
||||
inviteStatus: '' // 邀请状态
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、测试验证
|
||||
|
||||
### 测试场景1:单个生成邀请码
|
||||
|
||||
**前置条件**:
|
||||
- 已选择赛事
|
||||
- 列表中有评委记录
|
||||
- 评委未生成邀请码
|
||||
|
||||
**操作步骤**:
|
||||
1. 在评委列表中找到一个未生成邀请码的评委
|
||||
2. 点击"生成邀请码"按钮
|
||||
3. 等待生成完成
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 显示成功提示:`邀请码生成成功:ABC123`
|
||||
- ✅ 邀请码自动复制到剪贴板
|
||||
- ✅ 列表刷新,显示生成的邀请码
|
||||
- ✅ 邀请码列变为:邀请码 + 重新生成按钮
|
||||
|
||||
### 测试场景2:重新生成邀请码
|
||||
|
||||
**前置条件**:
|
||||
- 已选择赛事
|
||||
- 评委已有邀请码
|
||||
|
||||
**操作步骤**:
|
||||
1. 点击邀请码旁边的重新生成按钮
|
||||
2. 确认操作
|
||||
3. 等待生成完成
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 显示警告提示框
|
||||
- ✅ 确认后生成新邀请码
|
||||
- ✅ 旧邀请码失效
|
||||
- ✅ 新邀请码自动复制到剪贴板
|
||||
|
||||
### 测试场景3:批量生成邀请码
|
||||
|
||||
**前置条件**:
|
||||
- 已选择赛事
|
||||
- 列表中有多个未生成邀请码的评委
|
||||
|
||||
**操作步骤**:
|
||||
1. 勾选多个评委(如3个)
|
||||
2. 点击"批量生成邀请码"按钮
|
||||
3. 确认操作
|
||||
4. 等待生成完成
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 显示确认提示框:`确定为选中的 3 位评委批量生成邀请码吗?`
|
||||
- ✅ 确认后批量生成
|
||||
- ✅ 显示成功提示:`成功生成 3 个邀请码`
|
||||
- ✅ 列表刷新,所有评委都显示邀请码
|
||||
|
||||
### 测试场景4:复制邀请码
|
||||
|
||||
**前置条件**:
|
||||
- 评委已有邀请码
|
||||
|
||||
**操作步骤**:
|
||||
1. 点击邀请码标签
|
||||
2. 粘贴到其他地方验证
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 显示成功提示:`邀请码已复制:ABC123`
|
||||
- ✅ 剪贴板中有邀请码内容
|
||||
|
||||
### 测试场景5:赛事切换
|
||||
|
||||
**操作步骤**:
|
||||
1. 在赛事A生成邀请码
|
||||
2. 切换到赛事B
|
||||
3. 查看列表
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 只显示赛事B的评委列表
|
||||
- ✅ 赛事A的邀请码不显示
|
||||
- ✅ 每个赛事的邀请码独立管理
|
||||
|
||||
---
|
||||
|
||||
## 七、错误处理
|
||||
|
||||
### 错误1:未选择赛事
|
||||
|
||||
```javascript
|
||||
if (!queryParams.competitionId) {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 错误2:评委已有有效邀请码
|
||||
|
||||
后端会返回错误信息:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"msg": "该评委已有有效邀请码,请使用重新生成功能"
|
||||
}
|
||||
```
|
||||
|
||||
前端显示:
|
||||
```javascript
|
||||
ElMessage.error(error.response?.data?.msg || '生成邀请码失败')
|
||||
```
|
||||
|
||||
### 错误3:邀请码重复
|
||||
|
||||
后端会自动重试(最多10次),前端无需处理
|
||||
|
||||
---
|
||||
|
||||
## 八、UI/UX优化建议
|
||||
|
||||
### 1. 视觉优化
|
||||
|
||||
- ✅ 邀请码使用等宽字体(`monospace`)
|
||||
- ✅ 使用警告色标签(`type="warning"`)
|
||||
- ✅ 添加复制提示(`title="点击复制"`)
|
||||
- ✅ 按钮使用图标(`<Refresh />`)
|
||||
|
||||
### 2. 交互优化
|
||||
|
||||
- ✅ 点击邀请码自动复制
|
||||
- ✅ 重新生成前确认提示
|
||||
- ✅ 批量生成前确认提示
|
||||
- ✅ 生成成功后自动复制到剪贴板
|
||||
- ✅ 操作成功后自动刷新列表
|
||||
|
||||
### 3. 加载状态
|
||||
|
||||
- ✅ 生成时显示 loading
|
||||
- ✅ 防止重复点击
|
||||
- ✅ 异常时显示错误提示
|
||||
|
||||
---
|
||||
|
||||
## 九、实施检查清单
|
||||
|
||||
### 后端准备
|
||||
|
||||
- [x] DTO类创建完成
|
||||
- [x] Service方法实现完成
|
||||
- [x] Controller接口添加完成
|
||||
- [x] 数据库表结构正确
|
||||
- [x] 唯一索引配置正确
|
||||
|
||||
### 前端准备
|
||||
|
||||
- [x] API接口添加完成(`judgeInvite.js`)
|
||||
- [ ] 导入新增API
|
||||
- [ ] 修改邀请码列UI
|
||||
- [ ] 添加生成邀请码方法
|
||||
- [ ] 添加重新生成方法
|
||||
- [ ] 添加批量生成方法
|
||||
- [ ] 修改批量邀请按钮功能
|
||||
|
||||
### 测试验证
|
||||
|
||||
- [ ] 单个生成测试通过
|
||||
- [ ] 重新生成测试通过
|
||||
- [ ] 批量生成测试通过
|
||||
- [ ] 复制功能测试通过
|
||||
- [ ] 赛事切换测试通过
|
||||
- [ ] 错误处理测试通过
|
||||
|
||||
---
|
||||
|
||||
## 十、总结
|
||||
|
||||
### 核心要点
|
||||
|
||||
1. **赛事绑定**:所有操作都基于 `queryParams.competitionId`
|
||||
2. **数据隔离**:不同赛事的邀请码完全独立
|
||||
3. **用户友好**:自动复制、确认提示、状态反馈
|
||||
4. **错误处理**:完善的错误提示和异常处理
|
||||
5. **性能优化**:操作后自动刷新,保持数据同步
|
||||
|
||||
### 实施时间
|
||||
|
||||
- 前端修改:30分钟
|
||||
- 测试验证:20分钟
|
||||
- **总计**:50分钟
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 按照本文档修改前端代码
|
||||
2. 启动项目测试各功能
|
||||
3. 根据测试结果调整优化
|
||||
4. 上线部署
|
||||
|
||||
---
|
||||
|
||||
**祝您实施顺利!** 🚀
|
||||
|
||||
如有问题,请参考:
|
||||
- 后端实施文档:`评委邀请码生成方案实施指南.md`
|
||||
- API文档:`src/api/martial/judgeInvite.js`
|
||||
- 页面代码:`src/views/martial/judgeInvite/index.vue`
|
||||
554
doc/评委邀请码生成方案实施指南.md
Normal file
554
doc/评委邀请码生成方案实施指南.md
Normal file
@@ -0,0 +1,554 @@
|
||||
# 评委邀请码生成方案 - 实施指南
|
||||
|
||||
> **实施日期**: 2025-12-12
|
||||
> **实施方式**: 管理员生成 → 复制发送 → 评委使用
|
||||
> **状态**: ✅ 代码已完成,可立即测试
|
||||
|
||||
---
|
||||
|
||||
## 📋 方案概述
|
||||
|
||||
### 核心流程
|
||||
|
||||
```
|
||||
管理员操作:
|
||||
1. 进入评委管理页面
|
||||
2. 选择评委,点击"生成邀请码"
|
||||
3. 系统生成6位随机码(如:ABC123)
|
||||
4. 复制邀请码
|
||||
5. 通过微信/短信发送给评委
|
||||
|
||||
评委使用:
|
||||
1. 收到邀请码
|
||||
2. 打开小程序登录页
|
||||
3. 输入比赛编码 + 邀请码
|
||||
4. 登录成功,开始评分
|
||||
```
|
||||
|
||||
### 技术特点
|
||||
|
||||
- ✅ **无需改表** - 使用现有字段
|
||||
- ✅ **6位随机码** - 大写字母+数字组合
|
||||
- ✅ **唯一性保证** - 数据库唯一索引
|
||||
- ✅ **有效期管理** - 默认30天
|
||||
- ✅ **状态管理** - 待使用/已使用/已禁用
|
||||
|
||||
---
|
||||
|
||||
## 🚀 已完成的代码
|
||||
|
||||
### 1. DTO 类
|
||||
|
||||
#### GenerateInviteDTO.java
|
||||
**路径**: `src/main/java/org/springblade/modules/martial/pojo/dto/GenerateInviteDTO.java`
|
||||
|
||||
```java
|
||||
@Data
|
||||
@ApiModel("生成邀请码DTO")
|
||||
public class GenerateInviteDTO {
|
||||
@NotNull(message = "赛事ID不能为空")
|
||||
private Long competitionId;
|
||||
|
||||
@NotNull(message = "评委ID不能为空")
|
||||
private Long judgeId;
|
||||
|
||||
@NotBlank(message = "角色不能为空")
|
||||
private String role; // judge 或 chief_judge
|
||||
|
||||
private Long venueId; // 场地ID(普通评委必填)
|
||||
private String projects; // 项目列表(JSON)
|
||||
private Integer expireDays = 30; // 过期天数
|
||||
}
|
||||
```
|
||||
|
||||
#### BatchGenerateInviteDTO.java
|
||||
**路径**: `src/main/java/org/springblade/modules/martial/pojo/dto/BatchGenerateInviteDTO.java`
|
||||
|
||||
```java
|
||||
@Data
|
||||
@ApiModel("批量生成邀请码DTO")
|
||||
public class BatchGenerateInviteDTO {
|
||||
@NotNull(message = "赛事ID不能为空")
|
||||
private Long competitionId;
|
||||
|
||||
@NotEmpty(message = "评委列表不能为空")
|
||||
private List<Long> judgeIds;
|
||||
|
||||
private String role = "judge";
|
||||
private Integer expireDays = 30;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Service 层
|
||||
|
||||
#### IMartialJudgeInviteService.java
|
||||
**新增方法**:
|
||||
|
||||
```java
|
||||
// 生成邀请码
|
||||
MartialJudgeInvite generateInviteCode(GenerateInviteDTO dto);
|
||||
|
||||
// 批量生成邀请码
|
||||
List<MartialJudgeInvite> batchGenerateInviteCode(BatchGenerateInviteDTO dto);
|
||||
|
||||
// 重新生成邀请码
|
||||
MartialJudgeInvite regenerateInviteCode(Long inviteId);
|
||||
|
||||
// 生成唯一邀请码
|
||||
String generateUniqueInviteCode();
|
||||
```
|
||||
|
||||
#### MartialJudgeInviteServiceImpl.java
|
||||
**核心实现**:
|
||||
|
||||
1. **生成唯一邀请码**:
|
||||
```java
|
||||
// 6位随机字符串(大写字母+数字)
|
||||
String inviteCode = UUID.randomUUID().toString()
|
||||
.replaceAll("-", "")
|
||||
.substring(0, 6)
|
||||
.toUpperCase();
|
||||
```
|
||||
|
||||
2. **检查重复**:
|
||||
```java
|
||||
// 检查邀请码是否已存在
|
||||
long count = this.count(
|
||||
Wrappers.<MartialJudgeInvite>lambdaQuery()
|
||||
.eq(MartialJudgeInvite::getInviteCode, inviteCode)
|
||||
.eq(MartialJudgeInvite::getIsDeleted, 0)
|
||||
);
|
||||
```
|
||||
|
||||
3. **防止重复生成**:
|
||||
```java
|
||||
// 检查评委是否已有有效邀请码
|
||||
MartialJudgeInvite existInvite = this.getOne(
|
||||
Wrappers.<MartialJudgeInvite>lambdaQuery()
|
||||
.eq(MartialJudgeInvite::getCompetitionId, competitionId)
|
||||
.eq(MartialJudgeInvite::getJudgeId, judgeId)
|
||||
.eq(MartialJudgeInvite::getStatus, 1)
|
||||
.gt(MartialJudgeInvite::getExpireTime, LocalDateTime.now())
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Controller 层
|
||||
|
||||
#### MartialJudgeInviteController.java
|
||||
**新增接口**:
|
||||
|
||||
| 接口 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 生成邀请码 | POST | `/martial/judgeInvite/generate` | 为单个评委生成 |
|
||||
| 批量生成 | POST | `/martial/judgeInvite/generate/batch` | 批量生成 |
|
||||
| 重新生成 | PUT | `/martial/judgeInvite/regenerate/{id}` | 重新生成(旧码失效) |
|
||||
| 查询邀请码 | GET | `/martial/judgeInvite/byJudge` | 查询评委的邀请码 |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试指南
|
||||
|
||||
### 1. 使用 Postman 测试
|
||||
|
||||
#### 测试1:生成邀请码
|
||||
|
||||
```http
|
||||
POST http://localhost:8080/martial/judgeInvite/generate
|
||||
Content-Type: application/json
|
||||
Blade-Auth: Bearer {token}
|
||||
|
||||
{
|
||||
"competitionId": 1,
|
||||
"judgeId": 1,
|
||||
"role": "judge",
|
||||
"venueId": 1,
|
||||
"projects": "[\"女子组长拳\",\"男子组陈氏太极拳\"]",
|
||||
"expireDays": 30
|
||||
}
|
||||
```
|
||||
|
||||
**预期响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 1001,
|
||||
"competitionId": 1,
|
||||
"judgeId": 1,
|
||||
"inviteCode": "ABC123",
|
||||
"role": "judge",
|
||||
"venueId": 1,
|
||||
"projects": "[\"女子组长拳\",\"男子组陈氏太极拳\"]",
|
||||
"expireTime": "2026-01-11 10:00:00",
|
||||
"isUsed": 0,
|
||||
"status": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 测试2:批量生成邀请码
|
||||
|
||||
```http
|
||||
POST http://localhost:8080/martial/judgeInvite/generate/batch
|
||||
Content-Type: application/json
|
||||
Blade-Auth: Bearer {token}
|
||||
|
||||
{
|
||||
"competitionId": 1,
|
||||
"judgeIds": [1, 2, 3, 4, 5],
|
||||
"role": "judge",
|
||||
"expireDays": 30
|
||||
}
|
||||
```
|
||||
|
||||
#### 测试3:查询评委邀请码
|
||||
|
||||
```http
|
||||
GET http://localhost:8080/martial/judgeInvite/byJudge?competitionId=1&judgeId=1
|
||||
Blade-Auth: Bearer {token}
|
||||
```
|
||||
|
||||
#### 测试4:重新生成邀请码
|
||||
|
||||
```http
|
||||
PUT http://localhost:8080/martial/judgeInvite/regenerate/1001
|
||||
Blade-Auth: Bearer {token}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 使用 SQL 测试
|
||||
|
||||
#### 执行测试脚本
|
||||
|
||||
```bash
|
||||
# 进入数据库
|
||||
mysql -u root -p blade
|
||||
|
||||
# 执行测试脚本
|
||||
source database/martial-db/test_invite_code_generation.sql
|
||||
```
|
||||
|
||||
#### 查询有效邀请码
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
ji.id,
|
||||
ji.invite_code,
|
||||
ji.role,
|
||||
j.name AS judge_name,
|
||||
ji.expire_time,
|
||||
ji.is_used,
|
||||
CASE
|
||||
WHEN ji.is_used = 1 THEN '已使用'
|
||||
WHEN ji.expire_time < NOW() THEN '已过期'
|
||||
WHEN ji.status = 0 THEN '已禁用'
|
||||
ELSE '待使用'
|
||||
END AS status_text
|
||||
FROM martial_judge_invite ji
|
||||
LEFT JOIN martial_judge j ON ji.judge_id = j.id
|
||||
WHERE ji.competition_id = 1
|
||||
AND ji.is_deleted = 0
|
||||
ORDER BY ji.create_time DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 数据库字段说明
|
||||
|
||||
### martial_judge_invite 表
|
||||
|
||||
| 字段 | 类型 | 说明 | 使用方式 |
|
||||
|------|------|------|----------|
|
||||
| `invite_code` | varchar(50) | 邀请码 | 6位随机码 |
|
||||
| `status` | int | 状态 | 1-启用,0-禁用 |
|
||||
| `is_used` | int | 是否已使用 | 0-未使用,1-已使用 |
|
||||
| `expire_time` | datetime | 过期时间 | 默认30天后 |
|
||||
| `use_time` | datetime | 使用时间 | 登录时记录 |
|
||||
| `role` | varchar(20) | 角色 | judge/chief_judge |
|
||||
| `venue_id` | bigint | 场地ID | 普通评委必填 |
|
||||
| `projects` | varchar(500) | 项目列表 | JSON数组 |
|
||||
|
||||
### 状态判断逻辑
|
||||
|
||||
```
|
||||
有效邀请码:status=1 AND is_used=0 AND expire_time>NOW()
|
||||
已使用:is_used=1
|
||||
已过期:expire_time<=NOW()
|
||||
已禁用:status=0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 前端集成建议
|
||||
|
||||
### 1. 在评委管理页面添加按钮
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-table :data="judgeList">
|
||||
<el-table-column label="操作">
|
||||
<template #default="{ row }">
|
||||
<!-- 生成邀请码按钮 -->
|
||||
<el-button
|
||||
v-if="!row.inviteCode"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="generateInviteCode(row)"
|
||||
>
|
||||
生成邀请码
|
||||
</el-button>
|
||||
|
||||
<!-- 显示邀请码 -->
|
||||
<div v-else>
|
||||
<el-tag>{{ row.inviteCode }}</el-tag>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="copyInviteCode(row.inviteCode)"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="regenerateInviteCode(row)"
|
||||
>
|
||||
重新生成
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. 生成邀请码方法
|
||||
|
||||
```javascript
|
||||
async generateInviteCode(judge) {
|
||||
try {
|
||||
const res = await this.$http.post('/martial/judgeInvite/generate', {
|
||||
competitionId: this.competitionId,
|
||||
judgeId: judge.id,
|
||||
role: judge.refereeType === 1 ? 'chief_judge' : 'judge',
|
||||
venueId: judge.venueId,
|
||||
projects: JSON.stringify(judge.projects),
|
||||
expireDays: 30
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
this.$message.success('邀请码生成成功:' + res.data.inviteCode);
|
||||
// 复制到剪贴板
|
||||
this.copyToClipboard(res.data.inviteCode);
|
||||
// 刷新列表
|
||||
this.loadJudgeList();
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '生成失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
copyToClipboard(text) {
|
||||
const input = document.createElement('input');
|
||||
input.value = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
this.$message.success('已复制到剪贴板');
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 批量生成
|
||||
|
||||
```javascript
|
||||
async batchGenerate() {
|
||||
const selectedJudges = this.$refs.table.selection;
|
||||
if (selectedJudges.length === 0) {
|
||||
this.$message.warning('请选择评委');
|
||||
return;
|
||||
}
|
||||
|
||||
const judgeIds = selectedJudges.map(j => j.id);
|
||||
|
||||
try {
|
||||
const res = await this.$http.post('/martial/judgeInvite/generate/batch', {
|
||||
competitionId: this.competitionId,
|
||||
judgeIds: judgeIds,
|
||||
role: 'judge',
|
||||
expireDays: 30
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
this.$message.success(`成功生成${res.data.length}个邀请码`);
|
||||
this.loadJudgeList();
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '批量生成失败');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验证清单
|
||||
|
||||
### 后端验证
|
||||
|
||||
- [ ] DTO类创建成功
|
||||
- [ ] Service方法实现完成
|
||||
- [ ] Controller接口添加完成
|
||||
- [ ] 编译无错误
|
||||
- [ ] Swagger文档生成正常
|
||||
|
||||
### 功能验证
|
||||
|
||||
- [ ] 单个生成邀请码成功
|
||||
- [ ] 邀请码格式正确(6位大写字母+数字)
|
||||
- [ ] 邀请码唯一性验证通过
|
||||
- [ ] 批量生成成功
|
||||
- [ ] 重新生成成功(旧码失效)
|
||||
- [ ] 查询邀请码成功
|
||||
- [ ] 防止重复生成(已有有效邀请码时报错)
|
||||
|
||||
### 数据库验证
|
||||
|
||||
- [ ] 邀请码保存成功
|
||||
- [ ] 过期时间设置正确
|
||||
- [ ] 状态字段正确
|
||||
- [ ] 唯一索引生效
|
||||
|
||||
### 小程序验证
|
||||
|
||||
- [ ] 使用邀请码登录成功
|
||||
- [ ] 登录后权限正确
|
||||
- [ ] 场地和项目信息正确
|
||||
|
||||
---
|
||||
|
||||
## 🔧 常见问题
|
||||
|
||||
### 问题1:邀请码重复
|
||||
|
||||
**现象**: 生成的邀请码已存在
|
||||
|
||||
**原因**: 随机生成时碰撞
|
||||
|
||||
**解决**: 代码已实现重试机制(最多10次)
|
||||
|
||||
---
|
||||
|
||||
### 问题2:评委已有邀请码
|
||||
|
||||
**现象**: 提示"该评委已有有效邀请码"
|
||||
|
||||
**原因**: 防止重复生成
|
||||
|
||||
**解决**:
|
||||
- 使用"重新生成"功能
|
||||
- 或等待旧邀请码过期
|
||||
|
||||
---
|
||||
|
||||
### 问题3:邀请码过期
|
||||
|
||||
**现象**: 登录时提示邀请码已过期
|
||||
|
||||
**原因**: 超过30天有效期
|
||||
|
||||
**解决**: 使用"重新生成"功能
|
||||
|
||||
---
|
||||
|
||||
## 📈 后续优化建议
|
||||
|
||||
### 短期优化(可选)
|
||||
|
||||
1. **邀请码格式优化**
|
||||
- 添加前缀(如:WS-ABC123)
|
||||
- 区分角色(J-评委,C-裁判长)
|
||||
|
||||
2. **批量导出**
|
||||
- 导出Excel:评委信息+邀请码
|
||||
- 生成PDF邀请函
|
||||
|
||||
3. **统计报表**
|
||||
- 邀请码使用率
|
||||
- 过期邀请码数量
|
||||
|
||||
### 长期优化(可选)
|
||||
|
||||
1. **短信/邮件发送**
|
||||
- 集成短信服务
|
||||
- 自动发送邀请码
|
||||
|
||||
2. **二维码生成**
|
||||
- 生成邀请二维码
|
||||
- 扫码直接登录
|
||||
|
||||
3. **邀请码管理**
|
||||
- 批量禁用
|
||||
- 批量延期
|
||||
|
||||
---
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
### 代码位置
|
||||
|
||||
| 文件 | 路径 |
|
||||
|------|------|
|
||||
| DTO类 | `src/main/java/org/springblade/modules/martial/pojo/dto/` |
|
||||
| Service接口 | `src/main/java/org/springblade/modules/martial/service/IMartialJudgeInviteService.java` |
|
||||
| Service实现 | `src/main/java/org/springblade/modules/martial/service/impl/MartialJudgeInviteServiceImpl.java` |
|
||||
| Controller | `src/main/java/org/springblade/modules/martial/controller/MartialJudgeInviteController.java` |
|
||||
| 测试SQL | `database/martial-db/test_invite_code_generation.sql` |
|
||||
|
||||
### Swagger 文档
|
||||
|
||||
启动后端服务后访问:
|
||||
```
|
||||
http://localhost:8080/doc.html
|
||||
```
|
||||
|
||||
搜索"裁判邀请码管理"查看所有接口。
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
### 已完成
|
||||
|
||||
✅ DTO类创建
|
||||
✅ Service层实现
|
||||
✅ Controller接口
|
||||
✅ 测试SQL脚本
|
||||
✅ 实施文档
|
||||
|
||||
### 工作量
|
||||
|
||||
- 后端开发:2小时
|
||||
- 测试验证:1小时
|
||||
- 文档编写:1小时
|
||||
- **总计**:4小时
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 启动后端服务
|
||||
2. 使用Postman测试接口
|
||||
3. 前端集成(如需要)
|
||||
4. 联调测试
|
||||
5. 上线部署
|
||||
|
||||
---
|
||||
|
||||
**祝您实施顺利!** 🚀
|
||||
|
||||
如有问题,请查看代码注释或联系技术支持。
|
||||
@@ -88,7 +88,8 @@ export const saveAndLockSchedule = (competitionId) => {
|
||||
return request({
|
||||
url: '/martial/schedule/save-and-lock',
|
||||
method: 'post',
|
||||
data: { competitionId }
|
||||
data: { competitionId },
|
||||
timeout: 60000 // 设置60秒超时,因为锁定操作可能耗时较长
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,6 +104,82 @@ export const saveDraftSchedule = (data) => {
|
||||
return request({
|
||||
url: '/martial/schedule/save-draft',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 60000 // 设置60秒超时,因为保存草稿可能涉及大量数据
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发自动编排
|
||||
* @param {Number} competitionId - 赛事ID
|
||||
*/
|
||||
export const triggerAutoArrange = (competitionId) => {
|
||||
return request({
|
||||
url: '/martial/schedule/auto-arrange',
|
||||
method: 'post',
|
||||
data: { competitionId },
|
||||
timeout: 60000 // 设置60秒超时,因为自动编排可能耗时较长
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动赛程分组到指定场地和时间段
|
||||
* @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
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 调度功能接口 ====================
|
||||
|
||||
/**
|
||||
* 获取调度数据
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {Number} params.competitionId - 赛事ID
|
||||
* @param {Number} params.venueId - 场地ID
|
||||
* @param {Number} params.timeSlotIndex - 时间段索引
|
||||
*/
|
||||
export const getDispatchData = (params) => {
|
||||
return request({
|
||||
url: '/martial/schedule/dispatch-data',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整出场顺序
|
||||
* @param {Object} data - 调整请求数据
|
||||
* @param {Number} data.detailId - 编排明细ID
|
||||
* @param {Number} data.participantId - 参赛者记录ID
|
||||
* @param {String} data.action - 调整动作(move_up/move_down/swap)
|
||||
* @param {Number} data.targetOrder - 目标顺序(交换时使用)
|
||||
*/
|
||||
export const adjustOrder = (data) => {
|
||||
return request({
|
||||
url: '/martial/schedule/adjust-order',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存调度
|
||||
* @param {Object} data - 保存调度数据
|
||||
* @param {Number} data.competitionId - 赛事ID
|
||||
* @param {Array} data.adjustments - 调整列表
|
||||
*/
|
||||
export const saveDispatch = (data) => {
|
||||
return request({
|
||||
url: '/martial/schedule/save-dispatch',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -88,6 +88,21 @@ export const exportReferees = (params) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出工作人员名单(Excel)
|
||||
* @param {Object} params - 导出参数
|
||||
* @param {Number} params.competitionId - 赛事ID
|
||||
* @param {String} params.role - 角色类型(可选)
|
||||
*/
|
||||
export const exportStaff = (params) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/export/staff',
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出奖牌榜(Excel)
|
||||
* @param {Object} params - 导出参数
|
||||
|
||||
@@ -99,9 +99,8 @@ export const batchSendInvites = (data) => {
|
||||
*/
|
||||
export const resendInvite = (id) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/resend',
|
||||
method: 'post',
|
||||
params: { id }
|
||||
url: `/api/blade-martial/judgeInvite/resend/${id}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -124,14 +123,13 @@ export const replyInvite = (data) => {
|
||||
/**
|
||||
* 取消邀请
|
||||
* @param {Number} id - 邀请ID
|
||||
* @param {String} cancelReason - 取消原因
|
||||
* @param {String} reason - 取消原因
|
||||
*/
|
||||
export const cancelInvite = (id, cancelReason) => {
|
||||
export const cancelInvite = (id, reason) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/cancel',
|
||||
url: `/api/blade-martial/judgeInvite/cancel/${id}`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
data: { cancelReason }
|
||||
params: { reason }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -141,9 +139,8 @@ export const cancelInvite = (id, cancelReason) => {
|
||||
*/
|
||||
export const confirmInvite = (id) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/confirm',
|
||||
method: 'post',
|
||||
params: { id }
|
||||
url: `/api/blade-martial/judgeInvite/confirm/${id}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -173,15 +170,14 @@ export const getAcceptedJudges = (competitionId) => {
|
||||
|
||||
/**
|
||||
* 从裁判库导入
|
||||
* @param {Object} data - 导入参数
|
||||
* @param {Number} data.competitionId - 赛事ID
|
||||
* @param {Array} data.judgeIds - 裁判ID数组(从裁判库选择)
|
||||
* @param {Number} competitionId - 赛事ID
|
||||
* @param {String} judgeIds - 裁判ID(逗号分隔)
|
||||
*/
|
||||
export const importFromJudgePool = (data) => {
|
||||
export const importFromJudgePool = (competitionId, judgeIds) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/import-from-pool',
|
||||
url: '/api/blade-martial/judgeInvite/import/pool',
|
||||
method: 'post',
|
||||
data
|
||||
params: { competitionId, judgeIds }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -201,13 +197,70 @@ export const exportInvites = (params) => {
|
||||
/**
|
||||
* 发送提醒消息
|
||||
* @param {Number} id - 邀请ID
|
||||
* @param {String} reminderMessage - 提醒消息
|
||||
* @param {String} message - 提醒消息
|
||||
*/
|
||||
export const sendReminder = (id, reminderMessage) => {
|
||||
export const sendReminder = (id, message) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/send-reminder',
|
||||
url: `/api/blade-martial/judgeInvite/reminder/${id}`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
data: { reminderMessage }
|
||||
params: { message }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成邀请码
|
||||
* @param {Object} data - 生成参数
|
||||
* @param {Number} data.competitionId - 赛事ID
|
||||
* @param {Number} data.judgeId - 评委ID
|
||||
* @param {String} data.role - 角色(judge/chief_judge)
|
||||
* @param {Number} data.venueId - 场地ID(普通评委必填)
|
||||
* @param {String} data.projects - 项目列表(JSON)
|
||||
* @param {Number} data.expireDays - 过期天数(默认30)
|
||||
*/
|
||||
export const generateInviteCode = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/generate',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成邀请码
|
||||
* @param {Object} data - 批量生成参数
|
||||
* @param {Number} data.competitionId - 赛事ID
|
||||
* @param {Array} data.judgeIds - 评委ID数组
|
||||
* @param {String} data.role - 角色(默认judge)
|
||||
* @param {Number} data.expireDays - 过期天数(默认30)
|
||||
*/
|
||||
export const batchGenerateInviteCode = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/generate/batch',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成邀请码
|
||||
* @param {Number} inviteId - 邀请ID
|
||||
*/
|
||||
export const regenerateInviteCode = (inviteId) => {
|
||||
return request({
|
||||
url: `/api/blade-martial/judgeInvite/regenerate/${inviteId}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询评委的邀请码
|
||||
* @param {Number} competitionId - 赛事ID
|
||||
* @param {Number} judgeId - 评委ID
|
||||
*/
|
||||
export const getInviteByJudge = (competitionId, judgeId) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/byJudge',
|
||||
method: 'get',
|
||||
params: { competitionId, judgeId }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,6 +35,30 @@ export const getProjectDetail = (id) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增项目
|
||||
* @param {Object} data - 项目数据
|
||||
*/
|
||||
export const addProject = (data) => {
|
||||
return request({
|
||||
url: '/api/martial/project/save',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改项目
|
||||
* @param {Object} data - 项目数据
|
||||
*/
|
||||
export const updateProject = (data) => {
|
||||
return request({
|
||||
url: '/api/martial/project/update',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或修改项目
|
||||
* @param {Object} data - 项目数据
|
||||
@@ -73,6 +97,31 @@ export const removeProject = (ids) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入项目
|
||||
* @param {Object} data - 导入数据
|
||||
*/
|
||||
export const importProjects = (data) => {
|
||||
return request({
|
||||
url: '/api/martial/project/import',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出项目
|
||||
* @param {Object} params - 导出参数
|
||||
*/
|
||||
export const exportProjects = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/project/export',
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取赛事的项目列表(不分页,用于下拉选择)
|
||||
* @param {Number} competitionId - 赛事ID
|
||||
|
||||
@@ -232,3 +232,42 @@ export const exportResults = (params) => {
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出获奖名单
|
||||
* @param {Object} params - 查询参数
|
||||
*/
|
||||
export const exportAwardList = (params) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/result/export-award',
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单个证书
|
||||
* @param {Object} data - 证书参数
|
||||
*/
|
||||
export const generateCertificate = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/result/certificate/generate',
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成证书
|
||||
* @param {Object} data - 批量证书参数
|
||||
*/
|
||||
export const batchGenerateCertificates = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/result/certificate/batch-generate',
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
115
src/api/martial/rules.js
Normal file
115
src/api/martial/rules.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import request from '@/axios'
|
||||
|
||||
// 获取赛事列表
|
||||
export const getCompetitionList = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取赛事规程(小程序端)
|
||||
export const getCompetitionRules = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 附件管理 ====================
|
||||
|
||||
// 获取附件列表
|
||||
export const getAttachmentList = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/attachment/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 保存附件
|
||||
export const saveAttachment = (data) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/attachment/save',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除附件
|
||||
export const removeAttachment = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/attachment/remove',
|
||||
method: 'post',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 章节管理 ====================
|
||||
|
||||
// 获取章节列表
|
||||
export const getChapterList = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/chapter/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 保存章节
|
||||
export const saveChapter = (data) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/chapter/save',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除章节
|
||||
export const removeChapter = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/chapter/remove',
|
||||
method: 'post',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 章节内容管理 ====================
|
||||
|
||||
// 获取章节内容列表
|
||||
export const getContentList = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/content/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 保存章节内容
|
||||
export const saveContent = (data) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/content/save',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 批量保存章节内容
|
||||
export const batchSaveContents = (data) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/content/batch-save',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除章节内容
|
||||
export const removeContent = (params) => {
|
||||
return request({
|
||||
url: '/api/martial/competition/rules/content/remove',
|
||||
method: 'post',
|
||||
params
|
||||
})
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { Base64 } from 'js-base64';
|
||||
import { baseUrl } from '@/config/env';
|
||||
import crypto from '@/utils/crypto';
|
||||
|
||||
axios.defaults.timeout = 10000;
|
||||
axios.defaults.timeout = 60000; // 60秒超时,支持编排等耗时操作
|
||||
//返回其他状态吗
|
||||
axios.defaults.validateStatus = function (status) {
|
||||
return status >= 200 && status <= 500; // 默认的
|
||||
|
||||
@@ -71,22 +71,6 @@ export default [
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/order/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'schedule/list',
|
||||
name: '编排',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/schedule/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'dispatch/list',
|
||||
name: '调度',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/dispatch/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'banner/index',
|
||||
name: '轮播图管理',
|
||||
@@ -119,6 +103,22 @@ export default [
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/participant/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'schedule/list',
|
||||
name: '编排',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/schedule/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'dispatch/list',
|
||||
name: '调度',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/dispatch/index.vue'),
|
||||
},
|
||||
// 新增页面 - P0核心页面
|
||||
{
|
||||
path: 'project/list',
|
||||
@@ -218,6 +218,14 @@ export default [
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/activity/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'rules/index',
|
||||
name: '赛事规程管理',
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "martial" */ '@/views/martial/rules/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -32,6 +32,27 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
prop="competitionCode"
|
||||
label="比赛编码"
|
||||
width="150"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
v-if="scope.row.competitionCode"
|
||||
type="success"
|
||||
effect="dark"
|
||||
size="default"
|
||||
style="font-family: monospace; font-weight: bold; cursor: pointer;"
|
||||
@click="copyToClipboard(scope.row.competitionCode, '比赛编码')"
|
||||
>
|
||||
{{ scope.row.competitionCode }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small">未设置</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="organizer"
|
||||
label="主办单位"
|
||||
@@ -171,6 +192,31 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="比赛编码" prop="competitionCode">
|
||||
<el-input
|
||||
v-model="formData.competitionCode"
|
||||
placeholder="例如:WS2025 (留空自动生成)"
|
||||
maxlength="50"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
v-if="formData.competitionCode"
|
||||
icon="el-icon-document-copy"
|
||||
@click="copyToClipboard(formData.competitionCode, '比赛编码')"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
|
||||
💡 用于评委小程序登录,建议格式:项目简称+年份(如WS2025)
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="主办单位" prop="organizer">
|
||||
<el-input
|
||||
@@ -179,9 +225,6 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="举办地点" prop="location">
|
||||
<el-input
|
||||
@@ -190,6 +233,9 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="比赛场馆" prop="venue">
|
||||
<el-input
|
||||
@@ -776,6 +822,7 @@ export default {
|
||||
},
|
||||
formData: {
|
||||
competitionName: '',
|
||||
competitionCode: '', // 比赛编码
|
||||
organizer: '',
|
||||
location: '',
|
||||
venue: '',
|
||||
@@ -1433,6 +1480,7 @@ export default {
|
||||
resetFormData() {
|
||||
this.formData = {
|
||||
competitionName: '',
|
||||
competitionCode: '', // 比赛编码
|
||||
organizer: '',
|
||||
location: '',
|
||||
venue: '',
|
||||
@@ -1592,6 +1640,45 @@ export default {
|
||||
}
|
||||
// 未开始(默认状态)
|
||||
return 1;
|
||||
},
|
||||
|
||||
// 复制到剪贴板
|
||||
copyToClipboard(text, label) {
|
||||
if (!text) {
|
||||
this.$message.warning(`${label}为空`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用现代API复制
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
this.$message.success(`${label}已复制: ${text}`);
|
||||
}).catch(() => {
|
||||
this.fallbackCopyToClipboard(text, label);
|
||||
});
|
||||
} else {
|
||||
this.fallbackCopyToClipboard(text, label);
|
||||
}
|
||||
},
|
||||
|
||||
// 降级复制方法(兼容旧浏览器)
|
||||
fallbackCopyToClipboard(text, label) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.$message.success(`${label}已复制: ${text}`);
|
||||
} catch (err) {
|
||||
this.$message.error('复制失败,请手动复制');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,13 +11,27 @@
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- 场地选择器 -->
|
||||
<div class="venue-selector">
|
||||
<el-button
|
||||
v-for="venue in venues"
|
||||
:key="venue.id"
|
||||
size="small"
|
||||
:type="selectedVenueId === venue.id ? 'primary' : ''"
|
||||
@click="selectedVenueId = venue.id; loadDispatchData()"
|
||||
>
|
||||
{{ venue.venueName }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 时间段选择器 -->
|
||||
<div class="time-selector">
|
||||
<el-button
|
||||
v-for="(time, index) in timeSlots"
|
||||
:key="index"
|
||||
size="small"
|
||||
:type="selectedTime === index ? 'primary' : ''"
|
||||
@click="selectedTime = index"
|
||||
@click="selectedTime = index; loadDispatchData()"
|
||||
>
|
||||
{{ time }}
|
||||
</el-button>
|
||||
@@ -65,7 +79,7 @@
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
@click="handleMoveUp(index, scope.$index)"
|
||||
:disabled="scope.$index === 0"
|
||||
@@ -75,7 +89,7 @@
|
||||
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
@click="handleMoveDown(index, scope.$index)"
|
||||
:disabled="scope.$index === group.items.length - 1"
|
||||
@@ -88,110 +102,290 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="dispatch-footer" v-if="dispatchGroups.length > 0">
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="handleSaveDispatch" :disabled="!hasChanges">
|
||||
保存调度
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getVenuesByCompetition } from '@/api/martial/venue'
|
||||
import { getCompetitionDetail } from '@/api/martial/competition'
|
||||
import { getDispatchData, saveDispatch, getScheduleResult } from '@/api/martial/activitySchedule'
|
||||
|
||||
export default {
|
||||
name: 'MartialDispatchList',
|
||||
data() {
|
||||
return {
|
||||
orderId: null,
|
||||
selectedTime: 1,
|
||||
timeSlots: [
|
||||
'2025年11月6日上午8:30',
|
||||
'2025年11月6日下午13:00',
|
||||
'2025年11月7日上午8:30'
|
||||
],
|
||||
dispatchGroups: [
|
||||
{
|
||||
title: '1. 小学组小组赛男女类',
|
||||
type: '集体',
|
||||
count: '2队',
|
||||
code: '1101',
|
||||
venueType: 1,
|
||||
viewMode: 'dispatch',
|
||||
items: [
|
||||
{ schoolUnit: '少林寺武校', completed: true, refereed: false },
|
||||
{ schoolUnit: '访河社区', completed: false, refereed: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '1. 小学组小组赛男女类',
|
||||
type: '单人',
|
||||
count: '3队',
|
||||
code: '1组',
|
||||
venueType: 2,
|
||||
viewMode: 'dispatch',
|
||||
items: [
|
||||
{ schoolUnit: '少林寺武校', completed: true, refereed: true },
|
||||
{ schoolUnit: '访河社区', completed: false, refereed: false },
|
||||
{ schoolUnit: '少林寺武校', completed: true, refereed: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '2. 中学组决赛',
|
||||
type: '集体',
|
||||
count: '4队',
|
||||
code: '2101',
|
||||
venueType: 1,
|
||||
viewMode: 'dispatch',
|
||||
items: [
|
||||
{ schoolUnit: '成都体育学院', completed: true, refereed: true },
|
||||
{ schoolUnit: '武侯实验中学', completed: true, refereed: false },
|
||||
{ schoolUnit: '石室中学', completed: false, refereed: false },
|
||||
{ schoolUnit: '七中育才', completed: false, refereed: false }
|
||||
]
|
||||
}
|
||||
]
|
||||
competitionId: null,
|
||||
loading: false,
|
||||
selectedTime: 0,
|
||||
selectedVenueId: null,
|
||||
venues: [], // 场地列表
|
||||
timeSlots: [], // 时间段列表
|
||||
dispatchGroups: [], // 调度分组列表
|
||||
hasChanges: false, // 是否有未保存的更改
|
||||
originalData: null // 原始数据(用于取消时恢复)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.orderId = this.$route.query.orderId
|
||||
// 使用静态数据,不调用API
|
||||
async mounted() {
|
||||
this.competitionId = this.$route.query.competitionId
|
||||
if (this.competitionId) {
|
||||
// 先检查编排状态
|
||||
await this.checkScheduleStatus()
|
||||
this.loadCompetitionInfo()
|
||||
this.loadVenues()
|
||||
} else {
|
||||
this.$message.warning('未获取到赛事ID')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
|
||||
// 检查编排状态
|
||||
async checkScheduleStatus() {
|
||||
try {
|
||||
const res = await getScheduleResult(this.competitionId)
|
||||
const scheduleStatus = res.data?.data?.scheduleStatus || res.data?.scheduleStatus || 0
|
||||
|
||||
console.log('编排状态:', scheduleStatus)
|
||||
|
||||
// 如果编排未完成(状态不是2),提示用户并返回
|
||||
if (scheduleStatus !== 2) {
|
||||
this.$message.warning('请先完成编排并锁定后,才能进行调度操作')
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
this.$router.go(-1)
|
||||
}, 2000)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('检查编排状态失败:', error)
|
||||
this.$message.error('无法获取编排状态,请稍后重试')
|
||||
setTimeout(() => {
|
||||
this.$router.go(-1)
|
||||
}, 2000)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// 加载赛事信息
|
||||
async loadCompetitionInfo() {
|
||||
try {
|
||||
this.loading = true
|
||||
const res = await getCompetitionDetail(this.competitionId)
|
||||
const data = res.data?.data
|
||||
|
||||
if (data) {
|
||||
// 生成时间段
|
||||
this.generateTimeSlots(data.competitionStartTime || data.competition_start_time, data.competitionEndTime || data.competition_end_time)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载赛事信息失败', err)
|
||||
this.$message.error('加载赛事信息失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 生成时间段列表
|
||||
generateTimeSlots(startTime, endTime) {
|
||||
if (!startTime || !endTime) {
|
||||
this.timeSlots = ['2025年11月6日 上午8:30', '2025年11月6日 下午13:30']
|
||||
return
|
||||
}
|
||||
|
||||
const slots = []
|
||||
const start = new Date(startTime)
|
||||
const end = new Date(endTime)
|
||||
let currentDate = new Date(start)
|
||||
|
||||
while (currentDate <= end) {
|
||||
const year = currentDate.getFullYear()
|
||||
const month = currentDate.getMonth() + 1
|
||||
const day = currentDate.getDate()
|
||||
const dateStr = `${year}年${month}月${day}日`
|
||||
|
||||
slots.push(`${dateStr} 上午8:30`)
|
||||
slots.push(`${dateStr} 下午13:30`)
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
}
|
||||
|
||||
this.timeSlots = slots
|
||||
},
|
||||
|
||||
// 加载场地列表
|
||||
async loadVenues() {
|
||||
try {
|
||||
this.loading = true
|
||||
const res = await getVenuesByCompetition(this.competitionId)
|
||||
const venuesData = res.data?.data?.records || res.data?.data || []
|
||||
|
||||
if (venuesData.length === 0) {
|
||||
this.$message.warning('该赛事暂无场地信息')
|
||||
this.venues = []
|
||||
} else {
|
||||
this.venues = venuesData.map(v => ({
|
||||
id: v.id,
|
||||
venueName: v.venueName || v.venue_name
|
||||
}))
|
||||
// 默认选中第一个场地
|
||||
if (this.venues.length > 0) {
|
||||
this.selectedVenueId = this.venues[0].id
|
||||
this.loadDispatchData()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载场地失败', err)
|
||||
this.$message.error('加载场地失败')
|
||||
this.venues = []
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 加载调度数据
|
||||
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) {
|
||||
const groups = res.data.data.groups || []
|
||||
this.dispatchGroups = groups.map(group => ({
|
||||
...group,
|
||||
viewMode: 'dispatch',
|
||||
title: group.groupName,
|
||||
items: group.participants.map(p => ({
|
||||
...p,
|
||||
schoolUnit: p.organization,
|
||||
completed: false,
|
||||
refereed: false
|
||||
}))
|
||||
}))
|
||||
// 保存原始数据
|
||||
this.originalData = JSON.parse(JSON.stringify(this.dispatchGroups))
|
||||
this.hasChanges = false
|
||||
} else {
|
||||
this.$message.error(res.data.msg || '加载调度数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载调度数据失败:', error)
|
||||
this.$message.error('加载调度数据失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
setViewMode(index, mode) {
|
||||
this.dispatchGroups[index].viewMode = mode
|
||||
this.$message.success(`已切换到${mode === 'dispatch' ? '调度列表' : '场地'}模式`)
|
||||
},
|
||||
|
||||
handleMoveUp(groupIndex, itemIndex) {
|
||||
if (itemIndex === 0) return
|
||||
const group = this.dispatchGroups[groupIndex]
|
||||
const temp = group.items[itemIndex]
|
||||
group.items.splice(itemIndex, 1)
|
||||
group.items.splice(itemIndex - 1, 0, temp)
|
||||
|
||||
// 更新顺序号
|
||||
group.items.forEach((item, index) => {
|
||||
item.performanceOrder = index + 1
|
||||
})
|
||||
|
||||
this.hasChanges = true
|
||||
this.$message.success('上移成功')
|
||||
},
|
||||
|
||||
handleMoveDown(groupIndex, itemIndex) {
|
||||
const group = this.dispatchGroups[groupIndex]
|
||||
if (itemIndex === group.items.length - 1) return
|
||||
const temp = group.items[itemIndex]
|
||||
group.items.splice(itemIndex, 1)
|
||||
group.items.splice(itemIndex + 1, 0, temp)
|
||||
|
||||
// 更新顺序号
|
||||
group.items.forEach((item, index) => {
|
||||
item.performanceOrder = index + 1
|
||||
})
|
||||
|
||||
this.hasChanges = true
|
||||
this.$message.success('下移成功')
|
||||
},
|
||||
|
||||
handleBatchComplete() {
|
||||
this.$confirm('确定要标记当前批次所有为完赛状态吗?', '批次完赛', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 标记当前时间段所有为完赛
|
||||
this.dispatchGroups.forEach(group => {
|
||||
group.items.forEach(item => {
|
||||
item.completed = true
|
||||
})
|
||||
})
|
||||
this.$message.success('批次完赛成功')
|
||||
}).catch(() => {
|
||||
// 取消操作
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
// 保存调度
|
||||
async handleSaveDispatch() {
|
||||
if (!this.hasChanges) {
|
||||
this.$message.info('没有需要保存的更改')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
const adjustments = this.dispatchGroups.map(group => ({
|
||||
detailId: group.detailId,
|
||||
participants: group.items.map(p => ({
|
||||
id: p.id,
|
||||
performanceOrder: p.performanceOrder
|
||||
}))
|
||||
}))
|
||||
|
||||
const res = await saveDispatch({
|
||||
competitionId: this.competitionId,
|
||||
adjustments
|
||||
})
|
||||
|
||||
if (res.data.success) {
|
||||
this.$message.success('调度保存成功')
|
||||
this.hasChanges = false
|
||||
await this.loadDispatchData()
|
||||
} else {
|
||||
this.$message.error(res.data.msg || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存调度失败:', error)
|
||||
this.$message.error('保存失败,请稍后重试')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +417,14 @@ export default {
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.venue-selector {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
@@ -290,5 +492,17 @@ export default {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dispatch-footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.el-button {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
src/views/martial/judgeInvite/index.vue.old
Normal file
BIN
src/views/martial/judgeInvite/index.vue.old
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -69,7 +69,15 @@
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="handleRegistrationDetail(scope.row)">报名详情</el-button>
|
||||
<el-button type="success" size="small" @click="handleSchedule(scope.row)">编排</el-button>
|
||||
<el-button type="warning" size="small" @click="handleDispatch(scope.row)">调度</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleDispatch(scope.row)"
|
||||
:disabled="!isScheduleCompleted(scope.row.id)"
|
||||
:title="isScheduleCompleted(scope.row.id) ? '进入调度' : '请先完成编排'"
|
||||
>
|
||||
调度
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -91,6 +99,7 @@
|
||||
|
||||
<script>
|
||||
import { getCompetitionList } from '@/api/martial/competition'
|
||||
import { getScheduleResult } from '@/api/martial/activitySchedule'
|
||||
|
||||
export default {
|
||||
name: 'MartialOrderList',
|
||||
@@ -106,12 +115,19 @@ export default {
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
scheduleStatusMap: {} // 存储每个赛事的编排状态
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadCompetitionList()
|
||||
},
|
||||
activated() {
|
||||
// 当页面被激活时(从其他页面返回),重新加载编排状态
|
||||
if (this.tableData.length > 0) {
|
||||
this.loadScheduleStatus()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载赛事列表
|
||||
loadCompetitionList() {
|
||||
@@ -146,6 +162,9 @@ export default {
|
||||
createTime: competition.createTime || competition.create_time
|
||||
}))
|
||||
this.pagination.total = responseData.total || 0
|
||||
|
||||
// 加载每个赛事的编排状态
|
||||
this.loadScheduleStatus()
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -157,6 +176,28 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// 加载编排状态
|
||||
async loadScheduleStatus() {
|
||||
for (const competition of this.tableData) {
|
||||
try {
|
||||
const res = await getScheduleResult(competition.id)
|
||||
if (res.data?.data) {
|
||||
this.$set(this.scheduleStatusMap, competition.id, res.data.data.isCompleted || false)
|
||||
} else {
|
||||
this.$set(this.scheduleStatusMap, competition.id, false)
|
||||
}
|
||||
} catch (err) {
|
||||
// 如果获取失败,默认为未完成
|
||||
this.$set(this.scheduleStatusMap, competition.id, false)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 检查编排是否完成
|
||||
isScheduleCompleted(competitionId) {
|
||||
return this.scheduleStatusMap[competitionId] === true
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
this.pagination.current = 1
|
||||
this.loadCompetitionList()
|
||||
@@ -191,6 +232,12 @@ export default {
|
||||
|
||||
// 调度 - 传递赛事ID
|
||||
handleDispatch(row) {
|
||||
// 检查编排是否完成
|
||||
if (!this.isScheduleCompleted(row.id)) {
|
||||
this.$message.warning('请先完成编排后再进行调度')
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
path: '/martial/dispatch/list',
|
||||
query: { competitionId: row.id }
|
||||
|
||||
@@ -467,7 +467,7 @@ import {
|
||||
exportProjects
|
||||
} from '@/api/martial/project'
|
||||
import { getCompetitionList } from '@/api/martial/competition'
|
||||
import { getToken } from '@/util/auth'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 数据定义
|
||||
|
||||
@@ -310,12 +310,10 @@ export default {
|
||||
|
||||
// 计算总金额(从缓存中获取)
|
||||
let totalAmount = 0
|
||||
const projectIds = new Set()
|
||||
|
||||
for (const athlete of participants) {
|
||||
const projectId = athlete.projectId || athlete.project_id
|
||||
if (projectId && !projectIds.has(projectId)) {
|
||||
projectIds.add(projectId)
|
||||
if (projectId) {
|
||||
const project = this.projectCache.get(projectId)
|
||||
if (project) {
|
||||
totalAmount += parseFloat(project.price || 0)
|
||||
@@ -436,38 +434,30 @@ export default {
|
||||
if (!unitMap.has(unit)) {
|
||||
unitMap.set(unit, {
|
||||
projectIds: new Set(),
|
||||
projectPrices: new Map()
|
||||
totalAmount: 0
|
||||
})
|
||||
}
|
||||
|
||||
const stat = unitMap.get(unit)
|
||||
|
||||
// 添加项目ID(Set自动去重)
|
||||
// 添加项目ID(Set自动去重,用于统计项目数量)
|
||||
if (projectId) {
|
||||
stat.projectIds.add(projectId)
|
||||
|
||||
// 从缓存中获取价格(不再重复调用API)
|
||||
if (!stat.projectPrices.has(projectId)) {
|
||||
// 从缓存中获取价格并累加到总金额
|
||||
const project = this.projectCache.get(projectId)
|
||||
const price = project ? (project.price || 0) : 0
|
||||
stat.projectPrices.set(projectId, parseFloat(price))
|
||||
}
|
||||
stat.totalAmount += parseFloat(price)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 计算每个单位的总金额
|
||||
const amountStats = []
|
||||
for (const [unit, stat] of unitMap) {
|
||||
let totalAmount = 0
|
||||
// 遍历该单位的所有项目,累加价格
|
||||
for (const price of stat.projectPrices.values()) {
|
||||
totalAmount += price
|
||||
}
|
||||
|
||||
amountStats.push({
|
||||
schoolUnit: unit,
|
||||
projectCount: stat.projectIds.size,
|
||||
totalAmount: totalAmount.toFixed(2)
|
||||
totalAmount: stat.totalAmount.toFixed(2)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
625
src/views/martial/rules/index.vue
Normal file
625
src/views/martial/rules/index.vue
Normal file
@@ -0,0 +1,625 @@
|
||||
<template>
|
||||
<div class="rules-container">
|
||||
<!-- 赛事选择 -->
|
||||
<el-card shadow="never" class="search-card">
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="选择赛事">
|
||||
<el-select
|
||||
v-model="competitionId"
|
||||
placeholder="请选择赛事"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 300px"
|
||||
@change="handleCompetitionChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in competitionList"
|
||||
:key="item.id"
|
||||
:label="item.competitionName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div v-if="competitionId" class="content-wrapper">
|
||||
<!-- 附件管理 -->
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">📎 规程附件</span>
|
||||
<el-button type="primary" size="small" :icon="Plus" @click="handleAddAttachment">
|
||||
添加附件
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="attachmentList" border stripe>
|
||||
<el-table-column prop="fileName" label="文件名称" min-width="200" />
|
||||
<el-table-column prop="fileType" label="文件类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ row.fileType }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileSize" label="文件大小" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatFileSize(row.fileSize) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleEditAttachment(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="handlePreviewFile(row)">
|
||||
预览
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteAttachment(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 章节管理 -->
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">📄 规程章节</span>
|
||||
<el-button type="primary" size="small" :icon="Plus" @click="handleAddChapter">
|
||||
添加章节
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="chapterList" border stripe>
|
||||
<el-table-column prop="chapterNumber" label="章节编号" width="120" />
|
||||
<el-table-column prop="title" label="章节标题" min-width="200" />
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleEditChapter(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="success" size="small" @click="handleManageContent(row)">
|
||||
管理内容
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteChapter(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="请先选择赛事" />
|
||||
|
||||
<!-- 附件编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="attachmentDialogVisible"
|
||||
:title="attachmentForm.id ? '编辑附件' : '添加附件'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="attachmentForm" :rules="attachmentRules" ref="attachmentFormRef" label-width="100px">
|
||||
<el-form-item label="文件上传" prop="fileUrl">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="handleFileUploadSuccess"
|
||||
:before-upload="beforeFileUpload"
|
||||
:file-list="fileList"
|
||||
:limit="1"
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 pdf/doc/docx/xls/xlsx/ppt/pptx 等文档格式,文件大小不超过50MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件名称" prop="fileName">
|
||||
<el-input v-model="attachmentForm.fileName" placeholder="请输入文件名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="orderNum">
|
||||
<el-input-number v-model="attachmentForm.orderNum" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="attachmentForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="attachmentDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveAttachment">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 章节编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="chapterDialogVisible"
|
||||
:title="chapterForm.id ? '编辑章节' : '添加章节'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="chapterForm" :rules="chapterRules" ref="chapterFormRef" label-width="100px">
|
||||
<el-form-item label="章节编号" prop="chapterNumber">
|
||||
<el-input v-model="chapterForm.chapterNumber" placeholder="如:第一章" />
|
||||
</el-form-item>
|
||||
<el-form-item label="章节标题" prop="title">
|
||||
<el-input v-model="chapterForm.title" placeholder="请输入章节标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="orderNum">
|
||||
<el-input-number v-model="chapterForm.orderNum" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="chapterForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="chapterDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveChapter">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 章节内容管理对话框 -->
|
||||
<el-dialog
|
||||
v-model="contentDialogVisible"
|
||||
title="管理章节内容"
|
||||
width="800px"
|
||||
>
|
||||
<div class="content-header">
|
||||
<span class="content-title">{{ currentChapter.chapterNumber }} {{ currentChapter.title }}</span>
|
||||
<el-button type="primary" size="small" :icon="Plus" @click="handleAddContent">
|
||||
添加内容
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="contentList" border stripe class="content-table">
|
||||
<el-table-column prop="content" label="内容" min-width="400">
|
||||
<template #default="{ row, $index }">
|
||||
<el-input
|
||||
v-if="row.editing"
|
||||
v-model="row.content"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入内容"
|
||||
/>
|
||||
<span v-else>{{ row.content }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" />
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
v-if="!row.editing"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEditContent(row, $index)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
link
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleSaveContent(row, $index)"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteContent($index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="contentDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleBatchSaveContents">保存全部</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Delete, Upload, Download, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import {
|
||||
getCompetitionList,
|
||||
getAttachmentList,
|
||||
saveAttachment,
|
||||
removeAttachment,
|
||||
getChapterList,
|
||||
saveChapter,
|
||||
removeChapter,
|
||||
getContentList,
|
||||
batchSaveContents
|
||||
} from '@/api/martial/rules'
|
||||
|
||||
// 赛事列表
|
||||
const competitionList = ref([])
|
||||
const competitionId = ref(null)
|
||||
|
||||
// 附件相关
|
||||
const attachmentList = ref([])
|
||||
const attachmentDialogVisible = ref(false)
|
||||
const attachmentFormRef = ref(null)
|
||||
const attachmentForm = reactive({
|
||||
id: null,
|
||||
competitionId: null,
|
||||
fileName: '',
|
||||
fileUrl: '',
|
||||
fileSize: null,
|
||||
fileType: '',
|
||||
orderNum: 0,
|
||||
status: 1
|
||||
})
|
||||
const attachmentRules = {
|
||||
fileName: [{ required: true, message: '请输入文件名称', trigger: 'blur' }],
|
||||
fileUrl: [{ required: true, message: '请上传文件', trigger: 'change' }]
|
||||
}
|
||||
const fileList = ref([])
|
||||
|
||||
// 章节相关
|
||||
const chapterList = ref([])
|
||||
const chapterDialogVisible = ref(false)
|
||||
const chapterFormRef = ref(null)
|
||||
const chapterForm = reactive({
|
||||
id: null,
|
||||
competitionId: null,
|
||||
chapterNumber: '',
|
||||
title: '',
|
||||
orderNum: 0,
|
||||
status: 1
|
||||
})
|
||||
const chapterRules = {
|
||||
chapterNumber: [{ required: true, message: '请输入章节编号', trigger: 'blur' }],
|
||||
title: [{ required: true, message: '请输入章节标题', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 章节内容相关
|
||||
const contentDialogVisible = ref(false)
|
||||
const currentChapter = ref({})
|
||||
const contentList = ref([])
|
||||
|
||||
// 文件上传配置
|
||||
const uploadUrl = ref(import.meta.env.VITE_API_URL + '/blade-resource/oss/endpoint/put-file')
|
||||
const uploadHeaders = ref({
|
||||
'Blade-Auth': 'bearer ' + getToken()
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchCompetitionList()
|
||||
})
|
||||
|
||||
// 获取赛事列表
|
||||
const fetchCompetitionList = async () => {
|
||||
try {
|
||||
const res = await getCompetitionList({ current: 1, size: 1000 })
|
||||
competitionList.value = res.data.records || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取赛事列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 赛事切换
|
||||
const handleCompetitionChange = () => {
|
||||
if (competitionId.value) {
|
||||
fetchAttachmentList()
|
||||
fetchChapterList()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取附件列表
|
||||
const fetchAttachmentList = async () => {
|
||||
try {
|
||||
const res = await getAttachmentList({ competitionId: competitionId.value })
|
||||
attachmentList.value = res.data || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取附件列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加附件
|
||||
const handleAddAttachment = () => {
|
||||
Object.assign(attachmentForm, {
|
||||
id: null,
|
||||
competitionId: competitionId.value,
|
||||
fileName: '',
|
||||
fileUrl: '',
|
||||
fileSize: null,
|
||||
fileType: '',
|
||||
orderNum: 0,
|
||||
status: 1
|
||||
})
|
||||
fileList.value = []
|
||||
attachmentDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑附件
|
||||
const handleEditAttachment = (row) => {
|
||||
Object.assign(attachmentForm, { ...row })
|
||||
fileList.value = row.fileUrl ? [{ name: row.fileName, url: row.fileUrl }] : []
|
||||
attachmentDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 文件上传前校验
|
||||
const beforeFileUpload = (file) => {
|
||||
const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
||||
const isAllowed = allowedTypes.includes(file.type)
|
||||
const isLt50M = file.size / 1024 / 1024 < 50
|
||||
|
||||
if (!isAllowed) {
|
||||
ElMessage.error('只能上传文档格式文件!')
|
||||
}
|
||||
if (!isLt50M) {
|
||||
ElMessage.error('文件大小不能超过 50MB!')
|
||||
}
|
||||
return isAllowed && isLt50M
|
||||
}
|
||||
|
||||
// 文件上传成功
|
||||
const handleFileUploadSuccess = (response) => {
|
||||
if (response.code === 200) {
|
||||
attachmentForm.fileUrl = response.data.link
|
||||
attachmentForm.fileName = response.data.originalName
|
||||
attachmentForm.fileSize = response.data.size
|
||||
attachmentForm.fileType = response.data.extension
|
||||
ElMessage.success('文件上传成功')
|
||||
} else {
|
||||
ElMessage.error('文件上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存附件
|
||||
const handleSaveAttachment = async () => {
|
||||
await attachmentFormRef.value.validate()
|
||||
try {
|
||||
await saveAttachment(attachmentForm)
|
||||
ElMessage.success('保存成功')
|
||||
attachmentDialogVisible.value = false
|
||||
fetchAttachmentList()
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除附件
|
||||
const handleDeleteAttachment = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该附件吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await removeAttachment({ id: row.id })
|
||||
ElMessage.success('删除成功')
|
||||
fetchAttachmentList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
const handlePreviewFile = (row) => {
|
||||
window.open(row.fileUrl, '_blank')
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes) => {
|
||||
if (!bytes || bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 获取章节列表
|
||||
const fetchChapterList = async () => {
|
||||
try {
|
||||
const res = await getChapterList({ competitionId: competitionId.value })
|
||||
chapterList.value = res.data || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取章节列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加章节
|
||||
const handleAddChapter = () => {
|
||||
Object.assign(chapterForm, {
|
||||
id: null,
|
||||
competitionId: competitionId.value,
|
||||
chapterNumber: '',
|
||||
title: '',
|
||||
orderNum: 0,
|
||||
status: 1
|
||||
})
|
||||
chapterDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑章节
|
||||
const handleEditChapter = (row) => {
|
||||
Object.assign(chapterForm, { ...row })
|
||||
chapterDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 保存章节
|
||||
const handleSaveChapter = async () => {
|
||||
await chapterFormRef.value.validate()
|
||||
try {
|
||||
await saveChapter(chapterForm)
|
||||
ElMessage.success('保存成功')
|
||||
chapterDialogVisible.value = false
|
||||
fetchChapterList()
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除章节
|
||||
const handleDeleteChapter = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该章节吗?删除后章节下的所有内容也将被删除!', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await removeChapter({ id: row.id })
|
||||
ElMessage.success('删除成功')
|
||||
fetchChapterList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 管理章节内容
|
||||
const handleManageContent = async (row) => {
|
||||
currentChapter.value = row
|
||||
await fetchContentList(row.id)
|
||||
contentDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 获取章节内容列表
|
||||
const fetchContentList = async (chapterId) => {
|
||||
try {
|
||||
const res = await getContentList({ chapterId })
|
||||
contentList.value = (res.data || []).map(item => ({
|
||||
...item,
|
||||
editing: false
|
||||
}))
|
||||
} catch (error) {
|
||||
ElMessage.error('获取章节内容失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加内容
|
||||
const handleAddContent = () => {
|
||||
contentList.value.push({
|
||||
id: null,
|
||||
chapterId: currentChapter.value.id,
|
||||
content: '',
|
||||
orderNum: contentList.value.length + 1,
|
||||
status: 1,
|
||||
editing: true
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑内容
|
||||
const handleEditContent = (row, index) => {
|
||||
row.editing = true
|
||||
}
|
||||
|
||||
// 保存单个内容
|
||||
const handleSaveContent = (row, index) => {
|
||||
if (!row.content.trim()) {
|
||||
ElMessage.warning('内容不能为空')
|
||||
return
|
||||
}
|
||||
row.editing = false
|
||||
}
|
||||
|
||||
// 删除内容
|
||||
const handleDeleteContent = (index) => {
|
||||
contentList.value.splice(index, 1)
|
||||
// 重新排序
|
||||
contentList.value.forEach((item, idx) => {
|
||||
item.orderNum = idx + 1
|
||||
})
|
||||
}
|
||||
|
||||
// 批量保存内容
|
||||
const handleBatchSaveContents = async () => {
|
||||
const contents = contentList.value.map(item => item.content).filter(c => c.trim())
|
||||
if (contents.length === 0) {
|
||||
ElMessage.warning('请至少添加一条内容')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await batchSaveContents({
|
||||
chapterId: currentChapter.value.id,
|
||||
contents
|
||||
})
|
||||
ElMessage.success('保存成功')
|
||||
contentDialogVisible.value = false
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rules-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.content-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.content-table {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,14 @@
|
||||
<h2 class="page-title">编排</h2>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="autoArrangeLoading"
|
||||
@click="handleAutoArrange"
|
||||
>
|
||||
{{ isScheduleCompleted ? '重新编排' : '自动编排' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="showExceptionDialog">
|
||||
异常组 <el-badge :value="exceptionList.length" :hidden="exceptionList.length === 0" />
|
||||
</el-button>
|
||||
@@ -18,6 +26,7 @@
|
||||
size="small"
|
||||
:type="activeTab === 'competition' ? 'primary' : ''"
|
||||
@click="activeTab = 'competition'"
|
||||
:disabled="isScheduleCompleted"
|
||||
>
|
||||
竞赛分组
|
||||
</el-button>
|
||||
@@ -25,6 +34,7 @@
|
||||
size="small"
|
||||
:type="activeTab === 'venue' ? 'primary' : ''"
|
||||
@click="activeTab = 'venue'"
|
||||
:disabled="isScheduleCompleted"
|
||||
>
|
||||
场地
|
||||
</el-button>
|
||||
@@ -91,7 +101,7 @@
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
@click="handleMoveUp(group, scope.$index)"
|
||||
:disabled="scope.$index === 0 || isScheduleCompleted"
|
||||
@@ -101,7 +111,7 @@
|
||||
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
@click="handleMoveDown(group, scope.$index)"
|
||||
:disabled="scope.$index === group.items.length - 1 || isScheduleCompleted"
|
||||
@@ -112,7 +122,7 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="(scope.row.status || '未签到') === '未签到'"
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
@click="markAsException(group, scope.$index)"
|
||||
:disabled="isScheduleCompleted"
|
||||
@@ -174,23 +184,25 @@
|
||||
<!-- 确认对话框 -->
|
||||
<el-dialog
|
||||
title="确定完成编排"
|
||||
:visible.sync="confirmDialogVisible"
|
||||
v-model="confirmDialogVisible"
|
||||
width="450px"
|
||||
center
|
||||
>
|
||||
<p style="text-align: center; padding: 20px; color: #606266;">
|
||||
完成编排后,不可再次调整。确定完成编排吗?
|
||||
</p>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="confirmDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmComplete">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 移动分组对话框 -->
|
||||
<el-dialog
|
||||
title="移动竞赛分组"
|
||||
:visible.sync="moveDialogVisible"
|
||||
v-model="moveDialogVisible"
|
||||
width="500px"
|
||||
center
|
||||
>
|
||||
@@ -216,16 +228,18 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="moveDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmMoveGroup">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 异常组对话框 -->
|
||||
<el-dialog
|
||||
title="异常组参赛人员"
|
||||
:visible.sync="exceptionDialogVisible"
|
||||
v-model="exceptionDialogVisible"
|
||||
width="700px"
|
||||
center
|
||||
>
|
||||
@@ -241,7 +255,7 @@
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
@click="removeFromException(scope.$index)"
|
||||
style="color: #409eff;"
|
||||
@@ -254,9 +268,11 @@
|
||||
<div v-if="exceptionList.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
||||
暂无异常参赛人员
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="exceptionDialogVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -264,7 +280,7 @@
|
||||
<script>
|
||||
import { getVenuesByCompetition } from '@/api/martial/venue'
|
||||
import { getCompetitionDetail } from '@/api/martial/competition'
|
||||
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule } from '@/api/martial/activitySchedule'
|
||||
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup } from '@/api/martial/activitySchedule'
|
||||
|
||||
export default {
|
||||
name: 'MartialScheduleList',
|
||||
@@ -278,6 +294,7 @@ export default {
|
||||
confirmDialogVisible: false,
|
||||
isScheduleCompleted: false, // 是否已完成编排
|
||||
loading: false,
|
||||
autoArrangeLoading: false, // 自动编排加载状态
|
||||
venues: [], // 场地列表(从后端加载)
|
||||
competitionInfo: {
|
||||
competitionName: '',
|
||||
@@ -551,7 +568,7 @@ export default {
|
||||
},
|
||||
|
||||
// 确认移动分组
|
||||
confirmMoveGroup() {
|
||||
async confirmMoveGroup() {
|
||||
if (!this.moveTargetVenueId) {
|
||||
this.$message.warning('请选择目标场地')
|
||||
return
|
||||
@@ -564,6 +581,16 @@ export default {
|
||||
const group = this.competitionGroups[this.moveGroupIndex]
|
||||
const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)
|
||||
|
||||
try {
|
||||
// 调用后端API移动分组
|
||||
const res = await moveScheduleGroup({
|
||||
groupId: group.id,
|
||||
targetVenueId: this.moveTargetVenueId,
|
||||
targetTimeSlotIndex: this.moveTargetTimeSlot
|
||||
})
|
||||
|
||||
if (res.data.success) {
|
||||
// 更新前端数据
|
||||
group.venueId = this.moveTargetVenueId
|
||||
group.venueName = targetVenue ? targetVenue.venueName : ''
|
||||
group.timeSlotIndex = this.moveTargetTimeSlot
|
||||
@@ -571,6 +598,13 @@ export default {
|
||||
|
||||
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('移动分组失败,请稍后重试')
|
||||
}
|
||||
},
|
||||
|
||||
// 标记为异常
|
||||
@@ -624,6 +658,39 @@ export default {
|
||||
this.$message.success(`已将 ${exceptionItem.schoolUnit} 从异常组移除`)
|
||||
},
|
||||
|
||||
// 触发自动编排
|
||||
async handleAutoArrange() {
|
||||
try {
|
||||
// 确认操作
|
||||
await this.$confirm('自动编排将重新生成赛程安排,当前编排数据将被覆盖。确定继续吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
this.autoArrangeLoading = true
|
||||
|
||||
// 调用自动编排接口
|
||||
const res = await triggerAutoArrange(this.competitionId)
|
||||
|
||||
if (res.data?.success) {
|
||||
this.$message.success('自动编排完成')
|
||||
|
||||
// 重新加载编排数据
|
||||
await this.loadScheduleData()
|
||||
} else {
|
||||
this.$message.error(res.data?.msg || '自动编排失败')
|
||||
}
|
||||
} catch (err) {
|
||||
if (err !== 'cancel') {
|
||||
console.error('自动编排失败', err)
|
||||
this.$message.error('自动编排失败: ' + (err.message || ''))
|
||||
}
|
||||
} finally {
|
||||
this.autoArrangeLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 保存草稿
|
||||
async handleSaveDraft() {
|
||||
try {
|
||||
@@ -691,6 +758,16 @@ export default {
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
// 检查是否有编排数据
|
||||
if (!this.competitionGroups || this.competitionGroups.length === 0) {
|
||||
this.$message.warning('请先进行自动编排,生成编排数据后再完成编排')
|
||||
this.confirmDialogVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
console.log('开始完成编排,竞赛ID:', this.competitionId)
|
||||
console.log('编排分组数量:', this.competitionGroups.length)
|
||||
|
||||
// 1. 先保存当前的草稿数据
|
||||
const saveData = {
|
||||
competitionId: this.competitionId,
|
||||
@@ -714,19 +791,27 @@ export default {
|
||||
}))
|
||||
}
|
||||
|
||||
await saveDraftSchedule(saveData)
|
||||
console.log('保存草稿成功,准备锁定')
|
||||
console.log('准备保存草稿数据:', saveData)
|
||||
|
||||
const draftRes = await saveDraftSchedule(saveData)
|
||||
console.log('保存草稿成功,返回:', draftRes)
|
||||
|
||||
// 2. 然后调用锁定接口
|
||||
await saveAndLockSchedule(saveData)
|
||||
console.log('准备锁定编排,竞赛ID:', this.competitionId)
|
||||
const lockRes = await saveAndLockSchedule(this.competitionId)
|
||||
console.log('锁定接口返回:', lockRes)
|
||||
|
||||
// 3. 更新UI状态
|
||||
this.isScheduleCompleted = true
|
||||
this.confirmDialogVisible = false
|
||||
this.$message.success('编排已完成并锁定')
|
||||
|
||||
// 4. 重新加载编排数据以获取最新状态
|
||||
await this.loadScheduleData()
|
||||
} catch (err) {
|
||||
console.error('完成编排失败', err)
|
||||
this.$message.error('完成编排失败: ' + (err.message || ''))
|
||||
console.error('错误详情:', err.response?.data || err.message)
|
||||
this.$message.error('完成编排失败: ' + (err.response?.data?.msg || err.message || '未知错误'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -65,30 +65,30 @@
|
||||
<i class="card-arrow el-icon-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
<div class="access-card" @click="navigateTo('/martial/schedule/list')">
|
||||
<div class="access-card" @click="navigateTo('/martial/project/list')">
|
||||
<div class="card-bg"></div>
|
||||
<div class="card-icon">
|
||||
<i class="el-icon-menu"></i>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<h3>项目管理</h3>
|
||||
<p>武术项目管理</p>
|
||||
</div>
|
||||
<i class="card-arrow el-icon-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
<div class="access-card" @click="navigateTo('/martial/schedulePlan/list')">
|
||||
<div class="card-bg"></div>
|
||||
<div class="card-icon">
|
||||
<i class="el-icon-date"></i>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<h3>赛程编排</h3>
|
||||
<h3>赛程计划</h3>
|
||||
<p>比赛赛程安排</p>
|
||||
</div>
|
||||
<i class="card-arrow el-icon-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
<div class="access-card" @click="navigateTo('/martial/dispatch/list')">
|
||||
<div class="card-bg"></div>
|
||||
<div class="card-icon">
|
||||
<i class="el-icon-s-promotion"></i>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<h3>赛事调度</h3>
|
||||
<p>实时进度跟踪</p>
|
||||
</div>
|
||||
<i class="card-arrow el-icon-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
<div class="access-card" @click="navigateTo('/martial/referee/list')">
|
||||
<div class="card-bg"></div>
|
||||
<div class="card-icon">
|
||||
@@ -112,6 +112,18 @@
|
||||
</div>
|
||||
<i class="card-arrow el-icon-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
<div class="access-card" @click="navigateTo('/martial/result/list')">
|
||||
<div class="card-bg"></div>
|
||||
<div class="card-icon">
|
||||
<i class="el-icon-trophy"></i>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<h3>成绩管理</h3>
|
||||
<p>比赛成绩查询</p>
|
||||
</div>
|
||||
<i class="card-arrow el-icon-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user