@@ -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": []
|
||||
|
||||
@@ -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`
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -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; // 默认的
|
||||
|
||||
+24
-16
@@ -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.
+15
-1174
File diff suppressed because it is too large
Load Diff
Binary file not shown.
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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
+26
-14
@@ -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