Compare commits

..
27 Commits
Author SHA1 Message Date
hongjianli c12fb79444 Merge branch 'main' of git.waypeak.work:martial/martial-web 2025-12-26 11:06:52 +08:00
hongjianli 1744adcf92 fix bugs 2025-12-26 11:06:38 +08:00
DevOpsandClaude Opus 4.5 7f8c5c630b 修复导出功能:正确处理blob响应
- axios拦截器跳过blob类型响应的code检查
- 从res.data获取blob数据而非res

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 13:58:21 +08:00
DevOpsandClaude Opus 4.5 694b955cef 实现赛程表导出功能
- 添加 exportSchedule API 调用后端导出接口
- 实现 handleExport 方法,支持下载 Excel 文件
- 文件名格式:赛程表_赛事名称.xlsx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 13:45:09 +08:00
DevOpsandClaude Opus 4.5 ea4650b912 场地Tab添加场地过滤功能
- 添加场地按钮列表,支持切换不同场地
- 新增 selectedVenueIdForVenueTab 状态变量
- 修改 venueData 计算属性支持按场地过滤
- 初始化时默认选中第一个场地

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 13:34:35 +08:00
DevOpsandClaude Opus 4.5 e035647b51 feat: 竞赛分组页面项目支持折叠,默认收起
- 点击项目头部可展开/收起队伍列表
- 默认所有项目收起,方便管理多个项目
- 添加展开图标指示当前状态

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 12:55:55 +08:00
DevOpsandClaude Opus 4.5 352727b4fb feat: 竞赛分组页面添加队伍展开功能,显示选手签到状态和异常标记
- 点击队伍行可展开显示选手详情
- 显示选手签到状态:未签到/已签到/异常
- 支持标记异常和取消异常操作
- 优化评分页面代码

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 12:49:33 +08:00
DevOpsandClaude Opus 4.5 5e75688e13 feat: 赛程编排表格按队伍分组显示选手
continuous-integration/drone/push Build is pending
- 将同一单位的选手合并为一个队伍行
- 多选手队伍可展开查看具体选手
- 队伍状态根据所有选手状态计算(已签到/未签到/部分签到/部分异常)
- 上移/下移操作移动整个队伍
- 异常标记:单人队伍在主行标记,多人队伍需展开后标记单个选手
- 修复语法错误(转义字符和字符串引号)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 17:17:34 +08:00
DevOpsandClaude 226d92f725 fix: 修复P2和P3问题
continuous-integration/drone/push Build was killed
1. P2: 修复订单管理this.$set不兼容Vue3问题,改为直接赋值
2. P3: 修复选手年龄显示-1问题,改为显示"--"

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-22 13:25:38 +08:00
DevOpsandClaude 3d314fe84f chore: 更新package-lock.json
continuous-integration/drone/push Build is passing
🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 14:44:37 +08:00
hongjianli 179f7ea85d Merge branch 'main' of git.waypeak.work:martial/martial-web
continuous-integration/drone/push Build is passing
2025-12-17 09:31:23 +08:00
hongjianli d8730cc2c2 fix bugs 2025-12-17 09:31:14 +08:00
Developer 96f3b56eff fix: 修复 API 代理路径,去掉多余的 /api 前缀
continuous-integration/drone/push Build is passing
🤖 Generated with Claude Code
2025-12-13 23:11:31 +08:00
hongjianli 1a99a45729 fix bugs
continuous-integration/drone/push Build is passing
2025-12-13 09:34:06 +08:00
hongjianli 669f29878b fix bugs
continuous-integration/drone/push Build is passing
2025-12-12 17:54:40 +08:00
hongjianli 5b806e29b7 fix bugs
continuous-integration/drone/push Build is failing
2025-12-11 16:56:19 +08:00
hongjianli ab69968bda 修改前端页面
continuous-integration/drone/push Build is failing
2025-12-03 13:51:22 +08:00
hongjianli aa6facf13a fix bugs
continuous-integration/drone/push Build is passing
2025-11-30 12:15:21 +08:00
n72595987@gmail.comandClaude e77e73074a docs: 更新前端项目文档
continuous-integration/drone/push Build is passing
- 修复字符编码问题(之前显示乱码)
- 添加完整的项目说明和技术栈介绍
- 记录 Docker 部署方法和 CI/CD 流程
- 添加常见问题解答(登录接口错误、容器启动失败等)
- 完善生产架构图和网络拓扑说明
- 补充开发规范和配置说明

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:24:05 +08:00
n72595987@gmail.comandClaude a65daba7a9 fix: 修复 Drone 部署步骤使用正确的 Dockerfile
continuous-integration/drone/push Build is passing
- 使用 Dockerfile.deploy 而非默认 Dockerfile
- 直接使用 docker run 而不是 docker-compose(避免依赖问题)
- 强制删除旧容器后创建新容器(确保使用新镜像)
- 修复镜像构建后容器没有更新的问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:05:01 +08:00
n72595987@gmail.comandClaude 134208d510 fix: 修复前端 nginx 配置和 API 基础路径
continuous-integration/drone/push Build is passing
- nginx.conf: 将后端代理地址从 martial-backend 改为 172.21.0.1:8123(宿主机网关)
- .env.production: 移除 VITE_APP_API 的 /api 前缀
- 修复登录时 "No endpoint POST /api/blade-auth/oauth/token" 错误
- 修复容器启动失败 "host not found in upstream martial-backend" 错误

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:01:30 +08:00
n72595987@gmail.comandClaude cbf161048c fix: 修复前端 API 路径配置
continuous-integration/drone/push Build is passing
- 移除 VITE_APP_API 的 /api 前缀
- BladeX 框架的端点已包含完整路径(/blade-auth, /blade-system 等)
- 修复登录时 "No endpoint POST /api/blade-auth/oauth/token" 错误

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 23:55:30 +08:00
n72595987@gmail.comandClaude 890218199b 修复前端 CI/CD:改为在部署服务器上构建镜像
continuous-integration/drone/push Build is passing
问题:
- plugins/docker 尝试推送镜像到 Docker Hub
- 没有配置认证导致 'denied: requested access to resource is denied'

解决方案:
- 移除 plugins/docker 步骤
- 添加 drone-scp 步骤传输构建产物(dist、Dockerfile、nginx.conf)
- 在部署服务器上执行 docker build
- 直接使用本地镜像启动容器

优势:
- 不需要配置 Docker Hub 认证
- 不需要推送和拉取镜像,更快
- 镜像只存在于部署服务器本地
- 修正健康检查 URL 为 http://154.30.6.21/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 18:04:56 +08:00
n72595987@gmail.comandClaude 36116e0973 修复前端构建:正确配置 BladeX 私有 npm registry
continuous-integration/drone/push Build is failing
问题:
- Dockerfile 中 npm install 时未复制 .npmrc 文件
- 使用 --registry 参数覆盖了 .npmrc 中的私有 registry 配置
- 导致无法下载 @saber scope 的私有包,报 E401 认证错误

解决方案:
- Dockerfile: 在 npm install 前复制 .npmrc 文件
- Dockerfile: 移除 --registry 参数,使用 .npmrc 中的配置
- .drone.yml: 同样移除 --registry 参数

现在 npm 会正确使用:
- @saber 包 -> https://center.javablade.com (私有 registry)
- 其他包 -> npm 默认源

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 17:57:25 +08:00
n72595987@gmail.comandClaude 1d8d60ccaa 修复 Drone CI 配置:移除 host volumes 以解决 untrusted 仓库问题
continuous-integration/drone/push Build is failing
- 移除 npm-cache volume 挂载
- 同一构建内的步骤仍可共享 node_modules
- 使用国内镜像加速依赖下载

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 17:45:39 +08:00
n72595987@gmail.comandClaude 40b1e48f11 添加 Drone CI/CD 前端自动化部署配置
continuous-integration/drone/push Build encountered an error
1. 新增 .drone.yml 配置文件
   - 自动安装 npm 依赖
   - 自动构建生产版本
   - 构建 Docker 镜像
   - 自动部署到生产服务器
   - 健康检查验证部署成功

2. 新增 Dockerfile
   - 多阶段构建:Node 编译 + Nginx 运行
   - 优化镜像体积,使用 alpine 基础镜像
   - 配置静态资源缓存
   - 添加健康检查

3. 新增 nginx.conf
   - 配置前端路由支持(Vue Router history 模式)
   - 代理 API 请求到后端服务
   - 优化 Gzip 压缩和静态资源缓存
   - 支持 BladeX 系统模块路径代理

现在推送代码后,前端会自动部署到生产环境!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 17:04:57 +08:00
n72595987@gmail.comandClaude dd9927014e 修复Vite代理配置,解决前后端通信问题
1. 修复后端API代理配置
   - 更新代理目标端口从82改为8123(匹配实际后端端口)
   - 区分BladeX系统模块和业务模块的URL路径处理

2. 解决验证码接口404问题
   - 新增 ^/api/blade- 规则处理系统模块(blade-auth、blade-system等)
   - 系统模块需去掉 /api 前缀转发到后端 /blade-auth/* 路径
   - 业务模块保留 /api 前缀转发到后端 /api/martial/* 路径

3. 验证测试结果
   -  /api/blade-auth/oauth/captcha 验证码接口正常
   -  /api/blade-system/tenant/info 租户信息接口正常
   -  /api/martial/competition/list 比赛列表接口正常

现在前端登录页面可以正常获取验证码,所有API接口都能正确访问。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 16:31:05 +08:00
23 changed files with 752 additions and 1987 deletions
-37
View File
@@ -1,37 +0,0 @@
# 待处理问题
## Issue 1: 调度页面"状态"列数据应从后端获取
### 问题描述
调度页面中的"状态"列(已完赛/未完赛)目前是前端硬编码为 `false`,并非从后端 API 获取的真实状态。
### 当前行为
- 前端在加载调度数据时,将 `completed` 字段默认设置为 `false`
- 所有参赛者的状态都显示为"未完赛"
### 期望行为
- 从后端 API 获取每个参赛者的真实完赛状态
- 正确显示"已完赛"或"未完赛"
### 相关代码
**文件:** `src/views/martial/dispatch/index.vue`
```javascript
// 第320行 - 当前硬编码
completed: false,
// 第69-70行 - 状态显示逻辑
<el-tag v-if="scope.row.completed" type="success" size="small">已完赛</el-tag>
<el-tag v-else type="warning" size="small">未完赛</el-tag>
```
### 待办
- [ ] 后端 API 返回参赛者的完赛状态字段
- [ ] 前端从 API 响应中读取真实状态
---
+248 -50
View File
@@ -1,72 +1,270 @@
# 武术赛事管理系统 - 管理后台 # 武术赛事通 - 前端项目
基于 Vue 3 + Vite + Element Plus 构建的武术比赛管理后台。 基于 Vue 3 + Vite + Element Plus 构建
## 在线访问 ## 🌐 在线访问
| 服务 | 地址 | - **生产环境**: https://martial.johnsion.club
|------|------| - **后端 API**: https://martial-api.johnsion.club
| 管理后台 | https://martial-admin.aitisai.com | - **API 文档**: https://martial-doc.johnsion.club
| 后端 API | https://martial-api.aitisai.com |
## 技术栈 ## 📦 技术栈
- **框架**: Vue 3.4 (Composition API) - **框架**: Vue 3.4 (Composition API)
- **构建工具**: Vite 5 - **构建工具**: Vite 5
- **UI 组件**: Element Plus + Avue - **UI 组件**: Element Plus
- **状态管理**: Vuex 4 - **表单/表格**: Avue
- **HTTP 库**: Axios
- **路由**: Vue Router 4 - **路由**: Vue Router 4
- **状态管理**: Vuex 4
- **样式**: Sass/SCSS
## 快速开始 ## 📁 项目结构
```bash
# 安装依赖
npm install
# 开发环境
npm run dev
# 构建生产版本
npm run build
```
访问 http://localhost:8083
## 项目结构
``` ```
martial-web/ martial-web/
├── src/ ├── src/
│ ├── views/martial/ # 武术业务页面 │ ├── main.js # 应用入口
│ ├── competition/ # 赛事管理 │ ├── App.vue # 根组件
│ ├── project/ # 项目管理 │ ├── router/ # 路由配置
│ ├── participant/ # 参赛选手 │ ├── store/ # Vuex 状态管理
│ ├── judgeInvite/ # 裁判邀请 │ ├── views/ # 页面组件
│ ├── score/ # 评分管理 │ ├── components/ # 通用组件
│ └── ... ├── api/ # API 接口
── api/ # API 接口 ── utils/ # 工具函数
│ ├── router/ # 路由配置 ├── public/ # 静态资源
│ └── store/ # 状态管理 ├── .env.development # 开发环境配置
├── .env.development # 开发环境配置 ├── .env.production # 生产环境配置
├── .env.production # 生产环境配置 ├── vite.config.js # Vite 配置
── vite.config.js # Vite 配置 ── nginx.conf # Nginx 配置(生产环境容器使用)
├── Dockerfile # 完整构建 Dockerfile
├── Dockerfile.deploy # 部署 Dockerfile
└── .drone.yml # Drone CI/CD 配置
``` ```
## Docker 部署 ## 🚀 本地开发
### 环境要求
- Node.js >= 18
- npm >= 9
### 安装依赖
```bash ```bash
docker build -t martial-web . npm install
docker run -d -p 8083:80 martial-web
``` ```
## 相关仓库 ### 开发运行
| 仓库 | 说明 | ```bash
|------|------| npm run dev
| [martial-master](https://git.waypeak.work/martial/martial-master) | 后端 API | ```
| [martial-mini](https://git.waypeak.work/martial/martial-mini) | 用户端小程序 |
| [martial-admin-mini](https://git.waypeak.work/martial/martial-admin-mini) | 裁判端小程序 |
--- 访问 http://localhost:5173
**最后更新**: 2024-12-29 ### 生产构建
```bash
npm run build
```
构建产物在 `dist/` 目录
### 预览生产构建
```bash
npm run serve
```
## 🐳 Docker 部署
### 方法一:完整构建(开发/测试)
```bash
docker build -t martial/frontend:latest -f Dockerfile .
docker run -d -p 80:80 martial/frontend:latest
```
### 方法二:部署已构建产物(生产推荐)
```bash
# 先本地构建
npm run build
# 使用 Dockerfile.deploy 轻量化部署
docker build -t martial/frontend:latest -f Dockerfile.deploy .
# 运行容器
docker run -d \
--name martial-frontend \
--restart always \
-p 5173:80 \
--network martial_martial-network \
-e TZ=Asia/Shanghai \
martial/frontend:latest
```
## 🔄 CI/CD 自动部署
项目使用 Drone CI/CD 进行自动化部署。
### 部署流程
当推送到 `main` 分支时,自动触发:
1. **安装依赖** - `npm install`
2. **构建项目** - `npm run build`
3. **传输文件** - SCP 上传 dist、Dockerfile.deploy、nginx.conf 到服务器
4. **构建镜像** - 在服务器上执行 `docker build -f Dockerfile.deploy`
5. **部署容器** - 停止旧容器,启动新容器
6. **健康检查** - 验证服务可访问
### 查看构建状态
访问 https://martial-ci.johnsion.club
## ⚙️ 配置说明
### 环境变量
**开发环境** (`.env.development`)
```env
VITE_APP_API=/api # API 前缀(开发时走 Vite 代理)
```
**生产环境** (`.env.production`)
```env
VITE_APP_API= # 留空(因为 BladeX 端点路径已完整)
```
### Nginx 配置
生产环境 nginx 配置(`nginx.conf`):
```nginx
# 前端路由(Vue Router history 模式)
location / {
try_files $uri $uri/ /index.html;
}
# API 代理到后端(通过宿主机网关地址)
location /blade-auth/ {
proxy_pass http://172.21.0.1:8123/blade-auth/;
}
location /blade-system/ {
proxy_pass http://172.21.0.1:8123/blade-system/;
}
location /api/ {
proxy_pass http://172.21.0.1:8123/api/;
}
```
### Vite 配置
开发环境代理配置(`vite.config.js`):
```js
server: {
proxy: {
'/api': {
target: 'http://localhost:8123',
changeOrigin: true
}
}
}
```
## 🔧 常见问题
### 问题1: "No endpoint POST /api/blade-auth/oauth/token"
**原因**: 错误配置 `VITE_APP_API``/api` 前缀,导致请求 `/api/blade-auth/oauth/token`
**解决**: 检查 `.env.production``VITE_APP_API=`(留空)
### 问题2: 容器启动报 "host not found in upstream martial-backend"
**原因**: nginx.conf 中使用 Docker 服务名 `martial-backend`,但容器无法解析
**解决**: 使用宿主机网关地址 `172.21.0.1:8123` 代替
### 问题3: 端口 80 占用
**原因**: 宿主机 Caddy 已占用 80 端口
**解决**: 前端容器使用其他端口(如 5173),由 Caddy 反向代理
## 📝 开发规范
### 代码风格
- 使用 ESLint + Prettier
- 遵循 Vue 3 组合式 API 最佳实践
- 组件命名使用PascalCase
- 文件命名使用kebab-case
### Git 提交规范
```
feat: 新功能
fix: 修复 Bug
docs: 文档更新
style: 代码格式调整
refactor: 重构
perf: 性能优化
test: 测试相关
chore: 构建/工具配置
```
## 🏗️ 生产架构
### 部署拓扑
```
互联网
Cloudflare CDN
Caddy (80/443端口,自动 HTTPS)
martial.johnsion.club → localhost:5173 (前端 Nginx 容器)
├── 静态文件 → 直接返回 Vue 应用
└── /blade-auth, /blade-system, /api → 代理到后端 172.21.0.1:8123
```
### 网络架构
```
Docker Network: martial_martial-network (bridge)
├── martial-frontend (172.21.0.x) - 端口映射 5173:80
├── martial-mysql (172.21.0.x) - 端口映射 33066:3306
└── martial-redis (172.21.0.x) - 端口映射 63379:6379
宿主机:
├── Caddy (80/443) - 反向代理服务
├── Java 后端 (8123) - martial-master 应用
└── Drone CI/CD (8080) - 自动化部署
```
## 🔐 安全考虑
- 生产环境启用 HTTPS 证书(Caddy 自动签发)
- API 接口通过 Nginx 代理,隔离后端
- 敏感配置通过 Drone Secrets 管理
- 容器间网络隔离,仅暴露必要端口
## 📚 相关链接
- [后端仓库](https://git.waypeak.work/martial/martial-master)
- [BladeX 框架](https://bladex.cn)
- [Vue 3 文档](https://cn.vuejs.org/)
- [Element Plus](https://element-plus.org/)
- [Vite 文档](https://vitejs.dev/)
## 👥 贡献者
- **开发者**: JohnSion
- **AI 助手**: Claude Code
-19
View File
@@ -188,15 +188,6 @@ export const saveDispatch = (data) => {
* 导出赛程表 * 导出赛程表
* @param {Number} competitionId - 赛事ID * @param {Number} competitionId - 赛事ID
*/ */
// 预览赛程表数据
export const getScheduleExportPreview = (competitionId, template, venueId) => {
return request({
url: "/api/martial/export/schedule/preview",
method: "get",
params: { competitionId, template, venueId }
})
}
export const exportSchedule = (competitionId) => { export const exportSchedule = (competitionId) => {
return request({ return request({
url: '/martial/export/schedule', url: '/martial/export/schedule',
@@ -205,13 +196,3 @@ export const exportSchedule = (competitionId) => {
responseType: 'blob' responseType: 'blob'
}) })
} }
export const exportScheduleTemplate2 = (competitionId, venueId, venueName, timeSlot) => {
return request({
url: '/martial/export/schedule2',
method: 'get',
params: { competitionId, venueId, venueName, timeSlot },
responseType: 'blob'
})
}
-13
View File
@@ -141,16 +141,3 @@ export const getOrderAmountStats = (orderId) => {
params: { orderId } params: { orderId }
}) })
} }
/**
* 获取单位统计
* @param {Number} competitionId - 赛事ID
*/
export const getOrganizationStats = (competitionId) => {
return request({
url: '/api/martial/registrationOrder/organization-stats',
method: 'get',
params: { competitionId }
})
}
+2 -2
View File
@@ -9,7 +9,7 @@ import request from '@/axios';
* @param {Object} params - 查询参数 * @param {Object} params - 查询参数
* @param {String} params.name - 裁判姓名 * @param {String} params.name - 裁判姓名
* @param {String} params.phone - 手机号 * @param {String} params.phone - 手机号
* @param {Number} params.refereeType - 裁判类型(1-裁判,2-裁判 * @param {Number} params.refereeType - 裁判类型(1-裁判2-普通裁判)
*/ */
export const getRefereeList = (current, size, params) => { export const getRefereeList = (current, size, params) => {
return request({ return request({
@@ -43,7 +43,7 @@ export const getRefereeDetail = (id) => {
* @param {Number} data.gender - 性别(1-男,2-女) * @param {Number} data.gender - 性别(1-男,2-女)
* @param {String} data.phone - 手机号 * @param {String} data.phone - 手机号
* @param {String} data.idCard - 身份证号 * @param {String} data.idCard - 身份证号
* @param {Number} data.refereeType - 裁判类型(1-裁判,2-裁判 * @param {Number} data.refereeType - 裁判类型(1-裁判2-普通裁判)
* @param {String} data.level - 等级/职称 * @param {String} data.level - 等级/职称
* @param {String} data.specialty - 擅长项目 * @param {String} data.specialty - 擅长项目
* @param {String} data.photoUrl - 照片URL * @param {String} data.photoUrl - 照片URL
-24
View File
@@ -194,27 +194,3 @@ export const exportSchedulePlans = (params) => {
responseType: 'blob' responseType: 'blob'
}) })
} }
/**
* 更新参赛者签到状态
* @param {Number} participantId - 参赛者ID
* @param {String} status - 状态:未签到/已签到/异常
*/
export const updateCheckInStatus = (participantId, status) => {
return request({
url: '/api/martial/schedule/update-check-in-status',
method: 'post',
data: { participantId, status }
})
}
/**
* 获取赛程配置
* @returns {Promise} 返回赛程配置信息
*/
export const getScheduleConfig = () => {
return request({
url: '/api/martial/schedule/config',
method: 'get'
})
}
+2 -2
View File
@@ -48,7 +48,7 @@ export default [
redirect: '/martial/order/list', redirect: '/martial/order/list',
children: [ children: [
{ {
path: 'competition/index', path: 'competition/list',
name: '赛事管理', name: '赛事管理',
meta: { meta: {
keepAlive: false, keepAlive: false,
@@ -65,7 +65,7 @@ export default [
}, },
{ {
path: 'order/list', path: 'order/list',
name: '赛事管理', name: '订单管理',
meta: { meta: {
keepAlive: false, keepAlive: false,
}, },
+17
View File
@@ -361,6 +361,23 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column
label="参赛人数限制"
width="130"
align="center"
>
<template #default="scope">
<el-input-number
v-if="mode !== 'view'"
v-model="scope.row.maxParticipants"
:min="1"
:max="9999"
size="small"
style="width: 100%"
/>
<span v-else>{{ scope.row.maxParticipants || '不限' }}</span>
</template>
</el-table-column>
<el-table-column <el-table-column
label="项目说明" label="项目说明"
+160 -82
View File
@@ -85,10 +85,10 @@
> >
<template #default="scope"> <template #default="scope">
<el-tag <el-tag
:type="getStatusType(calculateStatus(scope.row))" :type="getStatusType(scope.row.status)"
size="small" size="small"
> >
{{ getStatusText(calculateStatus(scope.row)) }} {{ getStatusText(scope.row.status) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -646,51 +646,121 @@
<i class="el-icon-medal"></i> <i class="el-icon-medal"></i>
项目列表 项目列表
</div> </div>
<el-alert <el-form-item label="" class="button-right">
title="项目配置提示" <el-button
type="info" v-if="currentView !== 'view'"
:closable="false" type="primary"
show-icon icon="el-icon-plus"
style="margin-bottom: 15px;" size="small"
@click="handleAddProject"
>
添加项目
</el-button>
</el-form-item>
<el-table
:data="formData.projects"
border
style="width: 100%"
> >
<template #default> <el-table-column
<p>请先保存赛事基本信息然后前往 <strong>项目管理</strong> 功能配置比赛项目</p> label="项目名称"
<el-button min-width="150"
v-if="competitionId" >
type="primary" <template #default="scope">
size="small" <el-input
style="margin-top: 10px;" v-if="currentView !== 'view'"
@click="goToProjectManagement" v-model="scope.row.projectName"
> placeholder="例如:太极拳"
前往项目管理 size="small"
</el-button> />
</template> <span v-else>{{ scope.row.projectName }}</span>
</el-alert> </template>
</el-table-column>
<div v-if="formData.projects && formData.projects.length > 0">
<el-table :data="formData.projects" border size="small" style="width: 100%"> <el-table-column
<el-table-column prop="projectCode" label="项目码" width="120" /> label="项目码"
<el-table-column prop="projectName" label="项目名称" min-width="150" /> width="120"
<el-table-column prop="category" label="组别" width="120" /> >
<el-table-column label="项目类型" width="100" align="center"> <template #default="scope">
<template #default="scope"> <el-input
<el-tag v-if="scope.row.eventType === 1" type="primary" size="small">套路</el-tag> v-if="currentView !== 'view'"
<el-tag v-else-if="scope.row.eventType === 2" type="danger" size="small">散打</el-tag> v-model="scope.row.projectCode"
<el-tag v-else-if="scope.row.eventType === 3" type="success" size="small">器械</el-tag> placeholder="例如:TJQ001"
<el-tag v-else-if="scope.row.eventType === 4" type="warning" size="small">对练</el-tag> size="small"
<span v-else>-</span> />
</template> <span v-else>{{ scope.row.projectCode }}</span>
</el-table-column> </template>
<el-table-column prop="price" label="报名费(元)" width="100" align="center"> </el-table-column>
<template #default="scope">
<span style="color: #f56c6c">¥{{ scope.row.price || 0 }}</span> <el-table-column
</template> label="组别"
</el-table-column> width="120"
<el-table-column prop="maxParticipants" label="容纳人数" width="100" align="center" /> >
</el-table> <template #default="scope">
</div> <el-input
<el-empty v-else description="暂无项目,请前往项目管理添加" :image-size="60" /> v-if="currentView !== 'view'"
v-model="scope.row.category"
placeholder="例如:成年男子组"
size="small"
/>
<span v-else>{{ scope.row.category }}</span>
</template>
</el-table-column>
<el-table-column
label="参赛人数限制"
width="130"
align="center"
>
<template #default="scope">
<el-input-number
v-if="currentView !== 'view'"
v-model="scope.row.maxParticipants"
:min="1"
:max="9999"
size="small"
style="width: 100%"
/>
<span v-else>{{ scope.row.maxParticipants || '不限' }}</span>
</template>
</el-table-column>
<el-table-column
label="项目说明"
min-width="200"
>
<template #default="scope">
<el-input
v-if="currentView !== 'view'"
v-model="scope.row.description"
placeholder="请输入项目说明"
size="small"
/>
<span v-else>{{ scope.row.description }}</span>
</template>
</el-table-column>
<el-table-column
v-if="currentView !== 'view'"
label="操作"
width="80"
align="center"
>
<template #default="scope">
<el-button
type="danger"
link
size="small"
icon="el-icon-delete"
@click="handleDeleteProject(scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div> </div>
<!-- 场地配置 --> <!-- 场地配置 -->
@@ -886,7 +956,6 @@ import {
ATTACHMENT_TYPES, ATTACHMENT_TYPES,
ATTACHMENT_TYPE_LABELS ATTACHMENT_TYPE_LABELS
} from '@/api/martial/attachment' } from '@/api/martial/attachment'
import { getToken } from '@/utils/auth'
export default { export default {
name: 'CompetitionManagement', name: 'CompetitionManagement',
@@ -930,7 +999,6 @@ export default {
propsHttp: { propsHttp: {
res: 'data', res: 'data',
}, },
action: '/blade-resource/oss/endpoint/put-file' action: '/blade-resource/oss/endpoint/put-file'
} }
] ]
@@ -1201,7 +1269,18 @@ export default {
console.log('项目列表长度:', projectList.length); console.log('项目列表长度:', projectList.length);
if (projectList.length > 0) { if (projectList.length > 0) {
this.formData.projects = projectList.map(item => ({ ...item })); this.formData.projects = projectList.map(item => ({
id: item.id,
projectName: item.projectName,
projectCode: item.projectCode,
category: item.category,
type: item.type,
minParticipants: item.minParticipants,
maxParticipants: item.maxParticipants,
estimatedDuration: item.estimatedDuration,
price: item.price,
description: item.description
}));
console.log('✅ 加载的项目列表:', this.formData.projects); console.log('✅ 加载的项目列表:', this.formData.projects);
console.log('✅ 项目数量:', this.formData.projects.length); console.log('✅ 项目数量:', this.formData.projects.length);
} else { } else {
@@ -1251,8 +1330,7 @@ export default {
venueCode: item.venueCode, venueCode: item.venueCode,
capacity: item.capacity, capacity: item.capacity,
location: item.location, location: item.location,
remark: item.facilities || '', remark: item.facilities || ''
venueType: item.venueType || 'indoor'
})); }));
console.log('✅ 加载的场地列表:', this.formData.venues); console.log('✅ 加载的场地列表:', this.formData.venues);
console.log('✅ 场地数量:', this.formData.venues.length); console.log('✅ 场地数量:', this.formData.venues.length);
@@ -1324,7 +1402,6 @@ export default {
handleOpenAttachmentUpload(type) { handleOpenAttachmentUpload(type) {
this.currentAttachmentType = type; this.currentAttachmentType = type;
this.attachmentUploadForm = {}; this.attachmentUploadForm = {};
this.attachmentUploadOption.column[0].headers = { "Blade-Auth": "bearer " + getToken() };
this.attachmentUploadDialogVisible = true; this.attachmentUploadDialogVisible = true;
}, },
@@ -1497,7 +1574,7 @@ export default {
handleCreate() { handleCreate() {
this.$router.push({ this.$router.push({
path: '/martial/competition/index', path: '/martial/competition/list',
query: { mode: 'create' } query: { mode: 'create' }
}); });
}, },
@@ -1505,21 +1582,21 @@ export default {
handleView(row) { handleView(row) {
console.log(row) console.log(row)
this.$router.push({ this.$router.push({
path: '/martial/competition/index', path: '/martial/competition/list',
query: { mode: 'view', id: row.id } query: { mode: 'view', id: row.id }
}); });
}, },
handleEdit(row) { handleEdit(row) {
this.$router.push({ this.$router.push({
path: '/martial/competition/index', path: '/martial/competition/list',
query: { mode: 'edit', id: row.id } query: { mode: 'edit', id: row.id }
}); });
}, },
switchToEdit() { switchToEdit() {
this.$router.push({ this.$router.push({
path: '/martial/competition/index', path: '/martial/competition/list',
query: { mode: 'edit', id: this.competitionId } query: { mode: 'edit', id: this.competitionId }
}); });
}, },
@@ -1561,16 +1638,19 @@ export default {
this.formData.schedule.splice(index, 1); this.formData.schedule.splice(index, 1);
}, },
// 项目列表管理 - 跳转到项目管理页面 // 项目列表管理
goToProjectManagement() { handleAddProject() {
if (this.competitionId) { this.formData.projects.push({
this.$router.push({ projectName: '',
path: '/martial/project/list', projectCode: '',
query: { competitionId: this.competitionId } category: '',
}); maxParticipants: null,
} else { description: ''
this.$message.warning('请先保存赛事基本信息'); });
} },
handleDeleteProject(index) {
this.formData.projects.splice(index, 1);
}, },
// 场地配置管理 // 场地配置管理
@@ -1619,10 +1699,10 @@ export default {
savePromises.push(this.saveActivitySchedules(savedCompetitionId)); savePromises.push(this.saveActivitySchedules(savedCompetitionId));
} }
// 2. 项目列表 - 已移至项目管理功能配置 // 2. 保存项目列表
// if (this.formData.projects && this.formData.projects.length > 0) { if (this.formData.projects && this.formData.projects.length > 0) {
// savePromises.push(this.saveProjects(savedCompetitionId)); savePromises.push(this.saveProjects(savedCompetitionId));
// } }
// 3. 保存场地配置 // 3. 保存场地配置
if (this.formData.venues && this.formData.venues.length > 0) { if (this.formData.venues && this.formData.venues.length > 0) {
@@ -1706,7 +1786,9 @@ export default {
}, },
async saveProjects(competitionId) { async saveProjects(competitionId) {
const projects = this.formData.projects.filter(item => item.projectName); const projects = this.formData.projects.filter(item =>
item.projectName && item.projectCode
);
if (projects.length === 0) { if (projects.length === 0) {
return Promise.resolve(); return Promise.resolve();
@@ -1724,19 +1806,16 @@ export default {
const promises = projects.map(project => { const promises = projects.map(project => {
const projectData = { const projectData = {
...project,
competitionId: finalCompetitionId, competitionId: finalCompetitionId,
projectName: project.projectName, projectName: project.projectName,
projectCode: project.projectCode,
category: project.category || '', category: project.category || '',
eventType: project.eventType || 1, type: project.type || 1, // 默认个人项目
type: project.type || 1,
minParticipants: project.minParticipants || 1, minParticipants: project.minParticipants || 1,
maxParticipants: project.maxParticipants || 100, maxParticipants: project.maxParticipants || 1,
estimatedDuration: project.estimatedDuration || 5, estimatedDuration: project.estimatedDuration || 5,
price: project.price || 0, price: project.price || 0,
sortOrder: project.sortOrder || 0, description: project.description || ''
description: project.description || '',
rules: project.rules || ''
}; };
// 如果有 id,说明是编辑已有的项目 // 如果有 id,说明是编辑已有的项目
@@ -1777,8 +1856,7 @@ export default {
venueCode: venue.venueCode, venueCode: venue.venueCode,
capacity: venue.capacity || 100, capacity: venue.capacity || 100,
location: venue.location || '', location: venue.location || '',
facilities: venue.remark || '', facilities: venue.remark || ''
venueType: venue.venueType || 'indoor'
}; };
// 如果有 id,说明是编辑已有的场地 // 如果有 id,说明是编辑已有的场地
@@ -1795,7 +1873,7 @@ export default {
backToList() { backToList() {
this.$router.push({ this.$router.push({
path: '/martial/competition/index' path: '/martial/competition/list'
}); });
// 路由跳转后,在 initPage 中会自动加载列表 // 路由跳转后,在 initPage 中会自动加载列表
}, },
@@ -1966,7 +2044,7 @@ export default {
return 3; return 3;
} }
// 报名中 // 报名中
if (regStart && regEnd && now >= regStart && now <= new Date(regEnd.getFullYear(), regEnd.getMonth(), regEnd.getDate(), 23, 59, 59)) { if (regStart && regEnd && now >= regStart && now <= regEnd) {
return 2; return 2;
} }
// 未开始(默认状态) // 未开始(默认状态)
+2 -2
View File
@@ -60,10 +60,10 @@
> >
<template #default="scope"> <template #default="scope">
<el-tag <el-tag
:type="getStatusType(calculateStatus(scope.row))" :type="getStatusType(scope.row.status)"
size="small" size="small"
> >
{{ getStatusText(calculateStatus(scope.row)) }} {{ getStatusText(scope.row.status) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
+15 -35
View File
@@ -26,7 +26,7 @@
v-for="item in competitionList" v-for="item in competitionList"
:key="item.id" :key="item.id"
:label="item.competitionName" :label="item.competitionName"
:value="String(item.id)" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -43,7 +43,7 @@
v-for="item in projectList" v-for="item in projectList"
:key="item.id" :key="item.id"
:label="item.projectName" :label="item.projectName"
:value="String(item.id)" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -146,14 +146,14 @@
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
prop="deductionPoint" prop="deductionPoints"
label="扣分值(分)" label="扣分值(分)"
width="120" width="120"
align="center" align="center"
> >
<template #default="{ row }"> <template #default="{ row }">
<el-tag type="danger" effect="dark"> <el-tag type="danger" effect="dark">
-{{ row.deductionPoint }} -{{ row.deductionPoints }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -225,7 +225,7 @@
v-for="item in competitionList" v-for="item in competitionList"
:key="item.id" :key="item.id"
:label="item.competitionName" :label="item.competitionName"
:value="String(item.id)" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -240,7 +240,7 @@
v-for="item in projectList" v-for="item in projectList"
:key="item.id" :key="item.id"
:label="item.projectName" :label="item.projectName"
:value="String(item.id)" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -251,9 +251,9 @@
maxlength="100" maxlength="100"
/> />
</el-form-item> </el-form-item>
<el-form-item label="扣分值(分)" prop="deductionPoint"> <el-form-item label="扣分值(分)" prop="deductionPoints">
<el-input-number <el-input-number
v-model="form.deductionPoint" v-model="form.deductionPoints"
:min="0.1" :min="0.1"
:max="10" :max="10"
:precision="1" :precision="1"
@@ -317,7 +317,7 @@
v-for="item in projectList.filter(p => p.id !== cloneForm.sourceProjectId)" v-for="item in projectList.filter(p => p.id !== cloneForm.sourceProjectId)"
:key="item.id" :key="item.id"
:label="item.projectName" :label="item.projectName"
:value="String(item.id)" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -389,7 +389,6 @@ const queryParams = reactive({
size: 10, size: 10,
competitionId: null, competitionId: null,
projectId: null, projectId: null,
itemName: '',
}) })
// 表单数据 // 表单数据
@@ -397,8 +396,7 @@ const form = reactive({
id: null, id: null,
competitionId: null, competitionId: null,
projectId: null, projectId: null,
itemName: '', deductionPoints: 0.5,
deductionPoint: 0.5,
sortOrder: 0, sortOrder: 0,
description: '' description: ''
}) })
@@ -423,7 +421,7 @@ const rules = {
{ required: true, message: '请输入扣分项名称', trigger: 'blur' }, { required: true, message: '请输入扣分项名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' } { min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
], ],
deductionPoint: [ deductionPoints: [
{ required: true, message: '请输入扣分值', trigger: 'blur' } { required: true, message: '请输入扣分值', trigger: 'blur' }
], ],
sortOrder: [ sortOrder: [
@@ -506,17 +504,10 @@ const fetchData = async () => {
loading.value = true loading.value = true
try { try {
// 过滤掉空字符串的参数
const params = {}
Object.keys(queryParams).forEach(key => {
if (queryParams[key] !== '' && queryParams[key] !== null && queryParams[key] !== undefined) {
params[key] = queryParams[key]
}
})
const res = await getDeductionList( const res = await getDeductionList(
queryParams.current, queryParams.current,
queryParams.size, queryParams.size,
params queryParams
) )
// 根据axios响应拦截器的处理,数据在 res.data.data 中 // 根据axios响应拦截器的处理,数据在 res.data.data 中
const data = res.data?.data || {} const data = res.data?.data || {}
@@ -544,7 +535,7 @@ const handleReset = () => {
size: 10, size: 10,
competitionId: competitionId, competitionId: competitionId,
projectId: null, projectId: null,
itemName: '', itemName: ''
}) })
if (competitionId) { if (competitionId) {
fetchData() fetchData()
@@ -564,22 +555,11 @@ const handleAdd = () => {
} }
// 编辑 // 编辑
const handleEdit = async (row) => { const handleEdit = (row) => {
dialogTitle.value = '编辑扣分项' dialogTitle.value = '编辑扣分项'
Object.keys(form).forEach((key) => { Object.keys(form).forEach((key) => {
form[key] = row[key] form[key] = row[key]
}) })
// Convert competitionId to string for el-select matching
if (form.competitionId) {
form.competitionId = String(form.competitionId)
}
if (form.projectId) {
form.projectId = String(form.projectId)
}
// Load project list for the competition first
if (form.competitionId) {
await loadProjectList(form.competitionId)
}
dialogVisible.value = true dialogVisible.value = true
} }
@@ -690,7 +670,7 @@ const resetForm = () => {
competitionId: null, competitionId: null,
projectId: null, projectId: null,
itemName: '', itemName: '',
deductionPoint: 0.5, deductionPoints: 0.5,
sortOrder: 0, sortOrder: 0,
description: '' description: ''
}) })
+13 -50
View File
@@ -130,7 +130,6 @@ export default {
selectedVenueId: null, selectedVenueId: null,
venues: [], // 场地列表 venues: [], // 场地列表
timeSlots: [], // 时间段列表 timeSlots: [], // 时间段列表
timeSlotIndexMap: {}, // 显示索引到实际时间段索引的映射
dispatchGroups: [], // 调度分组列表 dispatchGroups: [], // 调度分组列表
hasChanges: false, // 是否有未保存的更改 hasChanges: false, // 是否有未保存的更改
originalData: null // 原始数据(用于取消时恢复) originalData: null // 原始数据(用于取消时恢复)
@@ -156,13 +155,12 @@ export default {
async checkScheduleStatus() { async checkScheduleStatus() {
try { try {
const res = await getScheduleResult(this.competitionId) const res = await getScheduleResult(this.competitionId)
const scheduleData = res.data?.data const scheduleStatus = res.data?.data?.scheduleStatus || res.data?.scheduleStatus || 0
const isCompleted = scheduleData?.isCompleted || false
console.log('编排完成状态:', isCompleted) console.log('编排状态:', scheduleStatus)
// 如果编排未完成,提示用户并返回 // 如果编排未完成(状态不是2,提示用户并返回
if (!isCompleted) { if (scheduleStatus !== 2) {
this.$message.warning('请先完成编排并锁定后,才能进行调度操作') this.$message.warning('请先完成编排并锁定后,才能进行调度操作')
// 延迟返回上一页 // 延迟返回上一页
setTimeout(() => { setTimeout(() => {
@@ -190,16 +188,8 @@ export default {
const data = res.data?.data const data = res.data?.data
if (data) { if (data) {
// 获取编排数据以确定有项目的时间段 // 生成时间段
let scheduleGroups = [] this.generateTimeSlots(data.competitionStartTime || data.competition_start_time, data.competitionEndTime || data.competition_end_time)
try {
const scheduleRes = await getScheduleResult(this.competitionId)
scheduleGroups = scheduleRes.data?.data?.competitionGroups || scheduleRes.data?.data?.scheduleGroups || []
} catch (e) {
console.warn('获取编排数据失败:', e)
}
// 生成时间段(只显示有项目的时间段)
this.generateTimeSlots(data.competitionStartTime || data.competition_start_time, data.competitionEndTime || data.competition_end_time, scheduleGroups)
} }
} catch (err) { } catch (err) {
console.error('加载赛事信息失败', err) console.error('加载赛事信息失败', err)
@@ -209,29 +199,17 @@ export default {
} }
}, },
// 生成时间段列表(只显示有项目的时间段) // 生成时间段列表
generateTimeSlots(startTime, endTime, scheduleGroups) { generateTimeSlots(startTime, endTime) {
if (!startTime || !endTime) { if (!startTime || !endTime) {
this.timeSlots = ['2025年11月6日 上午8:30', '2025年11月6日 下午13:30'] this.timeSlots = ['2025年11月6日 上午8:30', '2025年11月6日 下午13:30']
this.timeSlotIndexMap = { 0: 0, 1: 1 }
return return
} }
// 从编排数据中提取实际有项目的时间段索引 const slots = []
const usedTimeSlotIndices = new Set()
if (scheduleGroups && scheduleGroups.length > 0) {
scheduleGroups.forEach(group => {
if (group.timeSlotIndex !== undefined && group.timeSlotIndex !== null) {
usedTimeSlotIndices.add(group.timeSlotIndex)
}
})
}
const allSlots = []
const start = new Date(startTime) const start = new Date(startTime)
const end = new Date(endTime) const end = new Date(endTime)
let currentDate = new Date(start) let currentDate = new Date(start)
let slotIndex = 0
while (currentDate <= end) { while (currentDate <= end) {
const year = currentDate.getFullYear() const year = currentDate.getFullYear()
@@ -239,28 +217,13 @@ export default {
const day = currentDate.getDate() const day = currentDate.getDate()
const dateStr = `${year}${month}${day}` const dateStr = `${year}${month}${day}`
allSlots.push({ label: `${dateStr} 上午8:30`, index: slotIndex++ }) slots.push(`${dateStr} 上午8:30`)
allSlots.push({ label: `${dateStr} 下午13:30`, index: slotIndex++ }) slots.push(`${dateStr} 下午13:30`)
currentDate.setDate(currentDate.getDate() + 1) currentDate.setDate(currentDate.getDate() + 1)
} }
// 如果有编排数据,只显示有项目的时间段 this.timeSlots = slots
if (usedTimeSlotIndices.size > 0) {
const filteredSlots = allSlots.filter(slot => usedTimeSlotIndices.has(slot.index))
this.timeSlots = filteredSlots.map(slot => slot.label)
// 保存映射关系:显示索引 -> 实际时间段索引
this.timeSlotIndexMap = {}
filteredSlots.forEach((slot, displayIndex) => {
this.timeSlotIndexMap[displayIndex] = slot.index
})
} else {
this.timeSlots = allSlots.map(slot => slot.label)
this.timeSlotIndexMap = {}
allSlots.forEach((slot, displayIndex) => {
this.timeSlotIndexMap[displayIndex] = slot.index
})
}
}, },
// 加载场地列表 // 加载场地列表
@@ -305,7 +268,7 @@ export default {
const res = await getDispatchData({ const res = await getDispatchData({
competitionId: this.competitionId, competitionId: this.competitionId,
venueId: this.selectedVenueId, venueId: this.selectedVenueId,
timeSlotIndex: this.timeSlotIndexMap[this.selectedTime] !== undefined ? this.timeSlotIndexMap[this.selectedTime] : this.selectedTime timeSlotIndex: this.selectedTime
}) })
if (res.data.success) { if (res.data.success) {
+11 -121
View File
@@ -143,15 +143,10 @@
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="venueName" label="负责场地" align="center">
<template #default="{ row }">
<span>{{ row.venueName || "全部场地" }}</span>
</template>
</el-table-column>
<el-table-column prop="refereeType" label="裁判类型" align="center"> <el-table-column prop="refereeType" label="裁判类型" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.refereeType === 1 ? 'danger' : (row.refereeType === 3 ? 'warning' : 'primary')" size="small"> <el-tag :type="row.refereeType === 1 ? 'danger' : 'primary'" size="small">
{{ row.refereeType === 1 ? '主裁判' : (row.refereeType === 3 ? '总裁' : '裁判') }} {{ row.refereeType === 1 ? '主裁判' : '普通裁判' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -190,26 +185,6 @@
width="900px" width="900px"
:close-on-click-modal="false" :close-on-click-modal="false"
> >
<!-- 场地和项目选择 -->
<el-form :inline="true" class="venue-project-form" style="margin-bottom: 15px; padding: 15px; background: #f5f7fa; border-radius: 8px;">
<el-form-item label="分配场地" required>
<el-select
v-model="importForm.venueId"
placeholder="请选择场地"
clearable
style="width: 200px"
>
<el-option
v-for="item in venueList"
:key="item.id"
:label="item.venueName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<!-- 搜索表单 --> <!-- 搜索表单 -->
<el-form :inline="true" :model="judgeQueryParams" class="judge-search-form"> <el-form :inline="true" :model="judgeQueryParams" class="judge-search-form">
<el-form-item label="姓名"> <el-form-item label="姓名">
@@ -236,8 +211,7 @@
style="width: 130px" style="width: 130px"
> >
<el-option label="主裁判" :value="1" /> <el-option label="主裁判" :value="1" />
<el-option label="裁判" :value="2" /> <el-option label="普通裁判" :value="2" />
<el-option label="总裁" :value="3" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@@ -267,8 +241,8 @@
<el-table-column prop="phone" label="手机号" width="130" /> <el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="refereeType" label="裁判类型" width="100" align="center"> <el-table-column prop="refereeType" label="裁判类型" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.refereeType === 1 ? 'danger' : (row.refereeType === 3 ? 'warning' : 'primary')" size="small"> <el-tag :type="row.refereeType === 1 ? 'danger' : 'primary'" size="small">
{{ row.refereeType === 1 ? '主裁判' : (row.refereeType === 3 ? '总裁' : '裁判') }} {{ row.refereeType === 1 ? '主裁判' : '普通裁判' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -329,8 +303,6 @@ import {
removeInvite removeInvite
} from '@/api/martial/judgeInvite' } from '@/api/martial/judgeInvite'
import { getCompetitionList } from '@/api/martial/competition' import { getCompetitionList } from '@/api/martial/competition'
import { getVenuesByCompetition } from '@/api/martial/venue'
import { getProjectsByCompetition } from '@/api/martial/project'
import { getRefereeList } from '@/api/martial/referee' import { getRefereeList } from '@/api/martial/referee'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -344,17 +316,6 @@ const competitionLoading = ref(false) // 赛事列表加载状态
// 裁判选择对话框 // 裁判选择对话框
const judgeDialogVisible = ref(false) const judgeDialogVisible = ref(false)
// 场地和项目列表
const venueList = ref([])
const filterVenueList = ref([])
const projectList = ref([])
// 导入表单
const importForm = reactive({
venueId: null,
projectIds: []
})
const judgeLoading = ref(false) const judgeLoading = ref(false)
const judgeList = ref([]) const judgeList = ref([])
const judgeTotal = ref(0) const judgeTotal = ref(0)
@@ -382,8 +343,7 @@ const queryParams = reactive({
competitionId: null, competitionId: null,
judgeName: '', judgeName: '',
judgeLevel: '', judgeLevel: '',
inviteStatus: '', inviteStatus: ''
venueId: null
}) })
// 加载赛事列表 // 加载赛事列表
@@ -419,31 +379,11 @@ const loadCompetitionList = async () => {
} }
// 赛事切换 // 赛事切换
const handleCompetitionChange = async (competitionId) => { const handleCompetitionChange = (competitionId) => {
// 重置场地筛选
queryParams.venueId = null
// 加载该赛事的场地列表用于筛选
await loadFilterVenueList()
fetchData() fetchData()
loadStatistics() loadStatistics()
} }
// 加载筛选用的场地列表
const loadFilterVenueList = async () => {
if (!queryParams.competitionId) {
filterVenueList.value = []
return
}
try {
const res = await getVenuesByCompetition(queryParams.competitionId)
const venueData = res.data?.data || res.data || {}
filterVenueList.value = venueData.records || []
} catch (error) {
console.error('加载场地列表失败:', error)
filterVenueList.value = []
}
}
// 加载统计数据 // 加载统计数据
const loadStatistics = async () => { const loadStatistics = async () => {
if (queryParams.competitionId === null || queryParams.competitionId === '') return if (queryParams.competitionId === null || queryParams.competitionId === '') return
@@ -494,8 +434,7 @@ const handleReset = () => {
size: 10, size: 10,
judgeName: '', judgeName: '',
judgeLevel: '', judgeLevel: '',
inviteStatus: '', inviteStatus: ''
venueId: null
}) })
fetchData() fetchData()
} }
@@ -515,48 +454,12 @@ const handleImportFromPool = async () => {
return return
} }
// 重置导入表单
importForm.venueId = null
importForm.projectIds = []
// 加载场地和项目列表
await loadVenueAndProjectList()
// 打开裁判选择对话框 // 打开裁判选择对话框
judgeDialogVisible.value = true judgeDialogVisible.value = true
selectedJudges.value = [] selectedJudges.value = []
loadJudgeList() loadJudgeList()
} }
// 加载场地和项目列表
const loadVenueAndProjectList = async () => {
try {
// 并行加载场地和项目
const [venueRes, projectRes] = await Promise.all([
getVenuesByCompetition(queryParams.competitionId),
getProjectsByCompetition(queryParams.competitionId)
])
// 处理场地数据
const venueData = venueRes.data?.data || venueRes.data || {}
venueList.value = venueData.records || []
// 处理项目数据
const projectData = projectRes.data?.data || projectRes.data || {}
projectList.value = projectData.records || []
if (venueList.value.length === 0) {
ElMessage.warning('该赛事暂无场地,请先添加场地')
}
if (projectList.value.length === 0) {
ElMessage.warning('该赛事暂无项目,请先添加项目')
}
} catch (error) {
console.error('加载场地/项目列表失败:', error)
ElMessage.error('加载场地/项目列表失败')
}
}
// 加载裁判列表 // 加载裁判列表
const loadJudgeList = async () => { const loadJudgeList = async () => {
judgeLoading.value = true judgeLoading.value = true
@@ -620,13 +523,6 @@ const handleConfirmImport = async () => {
return return
} }
// 验证场地和项目
if (!importForm.venueId) {
ElMessage.warning('请选择分配的场地')
return
}
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`确定为选中的 ${selectedJudges.value.length} 位裁判生成邀请码吗?`, `确定为选中的 ${selectedJudges.value.length} 位裁判生成邀请码吗?`,
@@ -645,8 +541,6 @@ const handleConfirmImport = async () => {
competitionId: queryParams.competitionId, competitionId: queryParams.competitionId,
judgeIds: judgeIds, judgeIds: judgeIds,
role: 'judge', role: 'judge',
venueId: importForm.venueId,
// projects不传,裁判默认负责整个场地
expireDays: 30 expireDays: 30
}) })
@@ -731,7 +625,7 @@ const handleGenerateCode = async (row) => {
const res = await generateInviteCode({ const res = await generateInviteCode({
competitionId: queryParams.competitionId, competitionId: queryParams.competitionId,
judgeId: row.judgeId, judgeId: row.judgeId,
role: row.refereeType === 1 ? 'chief_judge' : (row.refereeType === 3 ? 'general_judge' : 'judge'), // 根据评委类型设置角色 role: row.refereeType === 1 ? 'chief_judge' : 'judge', // 根据评委类型设置角色
venueId: row.venueId || null, venueId: row.venueId || null,
projects: row.projects ? JSON.stringify(row.projects) : null, projects: row.projects ? JSON.stringify(row.projects) : null,
expireDays: 30 expireDays: 30
@@ -927,12 +821,8 @@ const fallbackCopyToClipboard = (text, label) => {
} }
// 挂载 // 挂载
onMounted(async () => { onMounted(() => {
await loadCompetitionList() loadCompetitionList()
// 加载筛选用的场地列表
if (queryParams.competitionId) {
await loadFilterVenueList()
}
}) })
</script> </script>
+3 -16
View File
@@ -2,7 +2,7 @@
<div class="martial-order-container"> <div class="martial-order-container">
<el-card shadow="hover"> <el-card shadow="hover">
<div class="page-header"> <div class="page-header">
<h2 class="page-title">赛事管理</h2> <h2 class="page-title">订单管理</h2>
</div> </div>
<el-form :inline="true" :model="searchForm" class="search-form"> <el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item> <el-form-item>
@@ -60,8 +60,8 @@
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center"> <el-table-column prop="status" label="状态" width="90" align="center">
<template #default="scope"> <template #default="scope">
<el-tag :type="getStatusType(calculateStatus(scope.row))" size="small"> <el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusText(calculateStatus(scope.row)) }} {{ getStatusText(scope.row.status) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -252,19 +252,6 @@ export default {
return `${start} ~ ${end}` return `${start} ~ ${end}`
}, },
// 根据时间计算赛事状态
calculateStatus(row) {
const now = new Date()
const regStart = row.registrationStartTime ? new Date(row.registrationStartTime) : null
const regEnd = row.registrationEndTime ? new Date(row.registrationEndTime) : null
const compStart = row.competitionStartTime ? new Date(row.competitionStartTime) : null
const compEnd = row.competitionEndTime ? new Date(row.competitionEndTime) : null
if (compEnd && now > compEnd) return 4
if (compStart && now >= compStart) return 3
if (regStart && regEnd && now >= regStart && now <= new Date(regEnd.getFullYear(), regEnd.getMonth(), regEnd.getDate(), 23, 59, 59)) return 2
return 1
},
getStatusType(status) { getStatusType(status) {
const statusMap = { const statusMap = {
1: 'info', // 未开始 1: 'info', // 未开始
+139 -98
View File
@@ -28,12 +28,17 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="分组类别"> <el-form-item label="分组类别">
<el-input <el-select
v-model="queryParams.category" v-model="queryParams.category"
placeholder="请输入分组类别" placeholder="请选择分组类别"
clearable clearable
style="width: 150px" style="width: 150px"
/> >
<el-option label="男子" value="1" />
<el-option label="女子" value="2" />
<el-option label="团体" value="3" />
<el-option label="混合" value="4" />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="项目类型"> <el-form-item label="项目类型">
<el-select <el-select
@@ -42,15 +47,15 @@
clearable clearable
style="width: 150px" style="width: 150px"
> >
<el-option label="套路" :value="1" /> <el-option label="套路" value="1" />
<el-option label="散打" :value="2" /> <el-option label="散打" value="2" />
<el-option label="器械" :value="3" /> <el-option label="器械" value="3" />
<el-option label="对练" :value="4" /> <el-option label="对练" value="4" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="参赛类型"> <el-form-item label="参赛类型">
<el-select <el-select
v-model="queryParams.type" v-model="queryParams.participantType"
placeholder="请选择参赛类型" placeholder="请选择参赛类型"
clearable clearable
style="width: 150px" style="width: 150px"
@@ -128,56 +133,66 @@
min-width="180" min-width="180"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column label="所属赛事" min-width="150" show-overflow-tooltip> <el-table-column
<template #default="{ row }"> prop="competitionName"
{{ getCompetitionName(row.competitionId) }} label="所属赛事"
</template> min-width="150"
</el-table-column> show-overflow-tooltip
/>
<el-table-column prop="category" label="分组类别" width="100" align="center"> <el-table-column prop="category" label="分组类别" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.category == 1" type="primary" size="small">男子</el-tag> <el-tag v-if="row.category === 1" type="primary">男子</el-tag>
<el-tag v-else-if="row.category == 2" type="danger" size="small">女子</el-tag> <el-tag v-else-if="row.category === 2" type="danger">女子</el-tag>
<el-tag v-else-if="row.category == 3" type="success" size="small">混合</el-tag> <el-tag v-else-if="row.category === 3" type="success">团体</el-tag>
<span v-else>-</span> <el-tag v-else-if="row.category === 4" type="warning">混合</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="eventType" label="项目类型" width="100" align="center"> <el-table-column prop="eventType" label="项目类型" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.eventType === 1" type="primary" size="small">套路</el-tag> <span v-if="row.eventType === 1">套路</span>
<el-tag v-else-if="row.eventType === 2" type="danger" size="small">散打</el-tag> <span v-else-if="row.eventType === 2">散打</span>
<el-tag v-else-if="row.eventType === 3" type="success" size="small">器械</el-tag> <span v-else-if="row.eventType === 3">器械</span>
<el-tag v-else-if="row.eventType === 4" type="warning" size="small">对练</el-tag> <span v-else-if="row.eventType === 4">对练</span>
<span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="type" label="参赛类型" width="100" align="center"> <el-table-column prop="participantType" label="参赛类型" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.type === 1" type="success" size="small">单人</el-tag> <el-tag v-if="row.participantType === 1" type="success" size="small">单人</el-tag>
<el-tag v-else-if="row.type === 2" type="warning" size="small">集体</el-tag> <el-tag v-else-if="row.participantType === 2" type="warning" size="small">集体</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="price" prop="registrationFee"
label="报名费(元)" label="报名费(元)"
width="110" width="110"
align="center" align="center"
> >
<template #default="{ row }"> <template #default="{ row }">
<span style="color: #f56c6c">¥{{ row.price || 0 }}</span> <span style="color: #f56c6c">¥{{ row.registrationFee || 0 }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="报名时间" width="180" align="center">
<el-table-column prop="estimatedDuration" label="预计时长" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span>{{ row.estimatedDuration || 5 }}分钟</span> <div v-if="row.registrationStartTime && row.registrationEndTime">
<div style="font-size: 12px">
{{ formatDate(row.registrationStartTime) }}
</div>
<div style="font-size: 12px"></div>
<div style="font-size: 12px">
{{ formatDate(row.registrationEndTime) }}
</div>
</div>
<span v-else style="color: #909399">未设置</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="单位容纳人数" width="120" align="center"> <el-table-column label="报名人数" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
{{ row.maxParticipants || 0 }} <span :style="{ color: row.currentCount >= row.maxParticipants ? '#f56c6c' : '#67c23a' }">
{{ row.currentCount || 0 }}
</span>
/ {{ row.maxParticipants || 0 }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="createTime" prop="createTime"
label="创建时间" label="创建时间"
@@ -249,6 +264,15 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item label="项目编码" prop="projectCode">
<el-input
v-model="form.projectCode"
placeholder="请输入项目编码"
maxlength="50"
/>
</el-form-item>
</el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
@@ -291,9 +315,9 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="参赛类型" prop="type"> <el-form-item label="参赛类型" prop="participantType">
<el-select <el-select
v-model="form.type" v-model="form.participantType"
placeholder="请选择参赛类型" placeholder="请选择参赛类型"
style="width: 100%" style="width: 100%"
> >
@@ -304,17 +328,6 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预计时长(分钟)" prop="estimatedDuration">
<el-input-number
v-model="form.estimatedDuration"
:min="1"
:max="120"
placeholder="每人/队预计比赛时长"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="报名费(元)" prop="registrationFee"> <el-form-item label="报名费(元)" prop="registrationFee">
<el-input-number <el-input-number
@@ -329,7 +342,33 @@
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="单位容纳人数" prop="maxParticipants"> <el-form-item label="报名开始时间" prop="registrationStartTime">
<el-date-picker
v-model="form.registrationStartTime"
type="datetime"
placeholder="选择开始时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报名结束时间" prop="registrationEndTime">
<el-date-picker
v-model="form.registrationEndTime"
type="datetime"
placeholder="选择结束时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最大参赛人数" prop="maxParticipants">
<el-input-number <el-input-number
v-model="form.maxParticipants" v-model="form.maxParticipants"
:min="1" :min="1"
@@ -387,7 +426,7 @@
{{ detailData.projectName }} {{ detailData.projectName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="所属赛事"> <el-descriptions-item label="所属赛事">
{{ getCompetitionName(detailData.competitionId) }} {{ detailData.competitionName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="分组类别"> <el-descriptions-item label="分组类别">
<el-tag v-if="detailData.category === 1" type="primary">男子</el-tag> <el-tag v-if="detailData.category === 1" type="primary">男子</el-tag>
@@ -402,18 +441,24 @@
<span v-else-if="detailData.eventType === 4">对练</span> <span v-else-if="detailData.eventType === 4">对练</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="参赛类型"> <el-descriptions-item label="参赛类型">
<el-tag v-if="detailData.type === 1" type="success" size="small">单人</el-tag> <el-tag v-if="detailData.participantType === 1" type="success" size="small">单人</el-tag>
<el-tag v-else-if="detailData.type === 2" type="warning" size="small">集体</el-tag> <el-tag v-else-if="detailData.participantType === 2" type="warning" size="small">集体</el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="报名费"> <el-descriptions-item label="报名费">
<span style="color: #f56c6c; font-weight: bold"> <span style="color: #f56c6c; font-weight: bold">
¥{{ detailData.registrationFee || 0 }} ¥{{ detailData.registrationFee || 0 }}
</span> </span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="单位容纳人数"> <el-descriptions-item label="报名开始时间">
{{ formatDate(detailData.registrationStartTime) }}
</el-descriptions-item>
<el-descriptions-item label="报名结束时间">
{{ formatDate(detailData.registrationEndTime) }}
</el-descriptions-item>
<el-descriptions-item label="最大参赛人数">
{{ detailData.maxParticipants }} {{ detailData.maxParticipants }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="报名人数"> <el-descriptions-item label="当前报名人数">
<span :style="{ <span :style="{
color: detailData.currentCount >= detailData.maxParticipants ? '#f56c6c' : '#67c23a', color: detailData.currentCount >= detailData.maxParticipants ? '#f56c6c' : '#67c23a',
fontWeight: 'bold' fontWeight: 'bold'
@@ -450,7 +495,8 @@ import {
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { import {
getProjectList, getProjectList,
submitProject, addProject,
updateProject,
removeProject, removeProject,
importProjects, importProjects,
exportProjects exportProjects
@@ -480,7 +526,7 @@ const queryParams = reactive({
projectName: '', projectName: '',
category: '', category: '',
eventType: '', eventType: '',
type: '' participantType: ''
}) })
// 表单数据 // 表单数据
@@ -491,9 +537,10 @@ const form = reactive({
projectName: '', projectName: '',
category: null, category: null,
eventType: null, eventType: null,
type: null, participantType: null,
estimatedDuration: 5,
registrationFee: 0, registrationFee: 0,
registrationStartTime: '',
registrationEndTime: '',
maxParticipants: 100, maxParticipants: 100,
sortOrder: 0, sortOrder: 0,
rules: '', rules: '',
@@ -515,6 +562,10 @@ const rules = {
competitionId: [ competitionId: [
{ required: true, message: '请选择所属赛事', trigger: 'change' } { required: true, message: '请选择所属赛事', trigger: 'change' }
], ],
projectCode: [
{ required: true, message: '请输入项目编码', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
projectName: [ projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' }, { required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' } { min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
@@ -525,26 +576,39 @@ const rules = {
eventType: [ eventType: [
{ required: true, message: '请选择项目类型', trigger: 'change' } { required: true, message: '请选择项目类型', trigger: 'change' }
], ],
estimatedDuration: [ participantType: [
{ required: true, message: '请输入预计时长', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择参赛类型', trigger: 'change' } { required: true, message: '请选择参赛类型', trigger: 'change' }
], ],
registrationFee: [ registrationFee: [
{ required: true, message: '请输入报名费', trigger: 'blur' } { required: true, message: '请输入报名费', trigger: 'blur' }
], ],
registrationStartTime: [
{ required: true, message: '请选择报名开始时间', trigger: 'change' }
],
registrationEndTime: [
{ required: true, message: '请选择报名结束时间', trigger: 'change' },
{
validator: (rule, value, callback) => {
if (value && form.registrationStartTime && value <= form.registrationStartTime) {
callback(new Error('结束时间必须大于开始时间'))
} else {
callback()
}
},
trigger: 'change'
}
],
maxParticipants: [ maxParticipants: [
{ required: true, message: '请输入单位容纳人数', trigger: 'blur' } { required: true, message: '请输入最大参赛人数', trigger: 'blur' }
] ]
} }
// 加载赛事列表 // 加载赛事列表
const loadCompetitionList = async () => { const loadCompetitionList = async () => {
try { try {
const res = await getCompetitionList(1, 1000, {}) const res = await getCompetitionList(1, 1000, { status: 1 })
if (res.data && res.data.data && res.data.data.records) { if (res.data && res.data.records) {
competitionList.value = res.data.data.records competitionList.value = res.data.records
} }
} catch (error) { } catch (error) {
console.error('加载赛事列表失败:', error) console.error('加载赛事列表失败:', error)
@@ -555,22 +619,14 @@ const loadCompetitionList = async () => {
const fetchData = async () => { const fetchData = async () => {
loading.value = true loading.value = true
try { try {
// Only pass parameters that backend supports
const params = {
competitionId: queryParams.competitionId || undefined,
projectName: queryParams.projectName || undefined,
category: queryParams.category || undefined,
eventType: queryParams.eventType || undefined,
type: queryParams.type || undefined
}
const res = await getProjectList( const res = await getProjectList(
queryParams.current, queryParams.current,
queryParams.size, queryParams.size,
params queryParams
) )
if (res.data && res.data.data) { if (res.data) {
tableData.value = res.data.data.records || [] tableData.value = res.data.records || []
total.value = res.data.data.total || 0 total.value = res.data.total || 0
} }
} catch (error) { } catch (error) {
ElMessage.error('获取数据失败') ElMessage.error('获取数据失败')
@@ -595,7 +651,7 @@ const handleReset = () => {
projectName: '', projectName: '',
category: '', category: '',
eventType: '', eventType: '',
type: '' participantType: ''
}) })
fetchData() fetchData()
} }
@@ -608,15 +664,11 @@ const handleAdd = () => {
} }
// 编辑 // 编辑
const handleEdit = async (row) => { const handleEdit = (row) => {
dialogTitle.value = '编辑项目' dialogTitle.value = '编辑项目'
Object.keys(form).forEach((key) => { Object.keys(form).forEach((key) => {
form[key] = row[key] form[key] = row[key]
}) })
// 处理字段名映射:后端返回 price,表单使用 registrationFee
if (row.price !== undefined) {
form.registrationFee = row.price
}
dialogVisible.value = true dialogVisible.value = true
} }
@@ -673,16 +725,11 @@ const handleSubmit = async () => {
if (valid) { if (valid) {
submitLoading.value = true submitLoading.value = true
try { try {
// 构建提交数据,确保字段名与后端一致
const submitData = {
...form,
price: form.registrationFee // 后端使用 price 字段
}
if (form.id) { if (form.id) {
await submitProject(submitData) await updateProject(form)
ElMessage.success('修改成功') ElMessage.success('修改成功')
} else { } else {
await submitProject(submitData) await addProject(form)
ElMessage.success('新增成功') ElMessage.success('新增成功')
} }
dialogVisible.value = false dialogVisible.value = false
@@ -715,9 +762,10 @@ const resetForm = () => {
projectName: '', projectName: '',
category: null, category: null,
eventType: null, eventType: null,
type: null, participantType: null,
estimatedDuration: 5,
registrationFee: 0, registrationFee: 0,
registrationStartTime: '',
registrationEndTime: '',
maxParticipants: 100, maxParticipants: 100,
sortOrder: 0, sortOrder: 0,
rules: '', rules: '',
@@ -782,13 +830,6 @@ const handleExport = async () => {
} }
} }
// 根据ID获取赛事名称
const getCompetitionName = (competitionId) => {
if (!competitionId) return '-'
const competition = competitionList.value.find(item => item.id === competitionId)
return competition ? competition.competitionName : '-'
}
// 格式化日期 // 格式化日期
const formatDate = (date) => { const formatDate = (date) => {
if (!date) return '-' if (!date) return '-'
-931
View File
@@ -1,931 +0,0 @@
<template>
<div class="project-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="赛事">
<el-select
v-model="queryParams.competitionId"
placeholder="请选择赛事"
clearable
filterable
style="width: 200px"
>
<el-option
v-for="item in competitionList"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="项目名称">
<el-input
v-model="queryParams.projectName"
placeholder="请输入项目名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="分组类别">
<el-input
v-model="queryParams.category"
placeholder="请输入分组类别"
clearable
style="width: 150px"
/>
</el-form-item>
<el-form-item label="项目类型">
<el-select
v-model="queryParams.eventType"
placeholder="请选择项目类型"
clearable
style="width: 150px"
>
<el-option label="套路" :value="1" />
<el-option label="散打" :value="2" />
<el-option label="器械" :value="3" />
<el-option label="对练" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="参赛类型">
<el-select
v-model="queryParams.type"
placeholder="请选择参赛类型"
clearable
style="width: 150px"
>
<el-option label="单人" :value="1" />
<el-option label="集体" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">
查询
</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 工具栏 -->
<el-card shadow="never" class="toolbar-card">
<div class="toolbar">
<div class="toolbar-left">
<el-button type="primary" :icon="Plus" @click="handleAdd">
新增项目
</el-button>
<el-button
type="danger"
:icon="Delete"
:disabled="!selection.length"
@click="handleBatchDelete"
>
批量删除
</el-button>
<el-upload
:action="uploadUrl"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:show-file-list="false"
accept=".xlsx,.xls"
>
<el-button type="success" :icon="Upload">导入Excel</el-button>
</el-upload>
<el-button type="warning" :icon="Download" @click="handleExport">
导出Excel
</el-button>
</div>
<div class="toolbar-right">
<el-tooltip content="刷新" placement="top">
<el-button circle :icon="Refresh" @click="fetchData" />
</el-tooltip>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never" class="table-card">
<el-table
v-loading="loading"
:data="tableData"
stripe
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column
prop="projectCode"
label="项目编码"
width="120"
align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="180"
show-overflow-tooltip
/>
<el-table-column label="所属赛事" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ getCompetitionName(row.competitionId) }}
</template>
</el-table-column>
<el-table-column prop="category" label="分组类别" width="100" align="center">
<template #default="{ row }">
<span>{{ row.category || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="eventType" label="项目类型" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.eventType === 1" type="primary" size="small">套路</el-tag>
<el-tag v-else-if="row.eventType === 2" type="danger" size="small">散打</el-tag>
<el-tag v-else-if="row.eventType === 3" type="success" size="small">器械</el-tag>
<el-tag v-else-if="row.eventType === 4" type="warning" size="small">对练</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="type" label="参赛类型" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.type === 1" type="success" size="small">单人</el-tag>
<el-tag v-else-if="row.type === 2" type="warning" size="small">集体</el-tag>
</template>
</el-table-column>
<el-table-column
prop="price"
label="报名费(元)"
width="110"
align="center"
>
<template #default="{ row }">
<span style="color: #f56c6c">¥{{ row.price || 0 }}</span>
</template>
</el-table-column>
<el-table-column prop="estimatedDuration" label="预计时长" width="100" align="center">
<template #default="{ row }">
<span>{{ row.estimatedDuration || 5 }}分钟</span>
</template>
</el-table-column>
<el-table-column label="单位容纳人数" width="120" align="center">
<template #default="{ row }">
{{ row.maxParticipants || 0 }}
</template>
</el-table-column>
<el-table-column label="所属场地" width="120" align="center">
<template #default="{ row }">
<span>{{ getVenueName(row.venueId) }}</span>
</template>
</el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
width="160"
align="center"
>
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">
编辑
</el-button>
<el-button link type="primary" :icon="View" @click="handleView(row)">
查看
</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
/>
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="800px"
:close-on-click-modal="false"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属赛事" prop="competitionId">
<el-select
v-model="form.competitionId"
placeholder="请选择赛事"
filterable
style="width: 100%"
@change="handleCompetitionChangeInForm"
>
<el-option
v-for="item in competitionList"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属场地" prop="venueId">
<el-select
v-model="form.venueId"
placeholder="请选择场地"
clearable
style="width: 100%"
>
<el-option
v-for="item in venueList"
:key="item.id"
:label="item.venueName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目编码" prop="projectCode">
<el-input
v-model="form.projectCode"
placeholder="请输入项目编码"
maxlength="50"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="form.projectName"
placeholder="请输入项目名称"
maxlength="100"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分组类别" prop="category">
<el-select
v-model="form.category"
placeholder="请选择分组类别"
style="width: 100%"
>
<el-option label="男子" :value="1" />
<el-option label="女子" :value="2" />
<el-option label="团体" :value="3" />
<el-option label="混合" :value="4" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目类型" prop="eventType">
<el-select
v-model="form.eventType"
placeholder="请选择项目类型"
style="width: 100%"
>
<el-option label="套路" :value="1" />
<el-option label="散打" :value="2" />
<el-option label="器械" :value="3" />
<el-option label="对练" :value="4" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="参赛类型" prop="type">
<el-select
v-model="form.type"
placeholder="请选择参赛类型"
style="width: 100%"
>
<el-option label="单人" :value="1" />
<el-option label="集体" :value="2" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预计时长(分钟)" prop="estimatedDuration">
<el-input-number
v-model="form.estimatedDuration"
:min="1"
:max="120"
placeholder="每人/队预计比赛时长"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报名费(元)" prop="registrationFee">
<el-input-number
v-model="form.registrationFee"
:min="0"
:precision="2"
:step="10"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="单位容纳人数" prop="maxParticipants">
<el-input-number
v-model="form.maxParticipants"
:min="1"
:max="1000"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序序号" prop="sortOrder">
<el-input-number
v-model="form.sortOrder"
:min="0"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="比赛规则" prop="rules">
<el-input
v-model="form.rules"
type="textarea"
:rows="4"
placeholder="请输入比赛规则说明"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
<!-- 查看详情弹窗 -->
<el-dialog v-model="detailVisible" title="项目详情" width="700px">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目编码">
{{ detailData.projectCode }}
</el-descriptions-item>
<el-descriptions-item label="项目名称">
{{ detailData.projectName }}
</el-descriptions-item>
<el-descriptions-item label="所属赛事">
{{ getCompetitionName(detailData.competitionId) }}
</el-descriptions-item>
<el-descriptions-item label="分组类别">
<el-tag v-if="detailData.category === 1" type="primary">男子</el-tag>
<el-tag v-else-if="detailData.category === 2" type="danger">女子</el-tag>
<el-tag v-else-if="detailData.category === 3" type="success">团体</el-tag>
<el-tag v-else-if="detailData.category === 4" type="warning">混合</el-tag>
</el-descriptions-item>
<el-descriptions-item label="项目类型">
<span v-if="detailData.eventType === 1">套路</span>
<span v-else-if="detailData.eventType === 2">散打</span>
<span v-else-if="detailData.eventType === 3">器械</span>
<span v-else-if="detailData.eventType === 4">对练</span>
</el-descriptions-item>
<el-descriptions-item label="参赛类型">
<el-tag v-if="detailData.type === 1" type="success" size="small">单人</el-tag>
<el-tag v-else-if="detailData.type === 2" type="warning" size="small">集体</el-tag>
</el-descriptions-item>
<el-descriptions-item label="报名费">
<span style="color: #f56c6c; font-weight: bold">
¥{{ detailData.registrationFee || 0 }}
</span>
</el-descriptions-item>
<el-descriptions-item label="单位容纳人数">
{{ detailData.maxParticipants }}
</el-descriptions-item>
<el-descriptions-item label="已报名人数">
<span :style="{
color: detailData.currentCount >= detailData.maxParticipants ? '#f56c6c' : '#67c23a',
fontWeight: 'bold'
}">
{{ detailData.currentCount || 0 }}
</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ formatDate(detailData.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="比赛规则" :span="2">
{{ detailData.rules || '暂无' }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ detailData.remark || '暂无' }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Search,
Refresh,
Plus,
Delete,
Edit,
View,
Upload,
Download
} from '@element-plus/icons-vue'
import {
getProjectList,
submitProject,
removeProject,
importProjects,
exportProjects
} from '@/api/martial/project'
import { getCompetitionList } from '@/api/martial/competition'
import { getVenuesByCompetition } from '@/api/martial/venue'
import { getToken } from '@/utils/auth'
import dayjs from 'dayjs'
// 数据定义
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref([])
const total = ref(0)
const selection = ref([])
const competitionList = ref([])
const venueList = ref([])
const allVenuesCache = ref(new Map()) // 全局场地缓存
const dialogVisible = ref(false)
const detailVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref(null)
const detailData = ref({})
// 查询参数
const queryParams = reactive({
current: 1,
size: 10,
competitionId: '',
projectName: '',
category: '',
eventType: '',
type: ''
})
// 表单数据
const form = reactive({
id: null,
competitionId: '',
venueId: null,
projectCode: '',
projectName: '',
category: null,
eventType: null,
type: null,
estimatedDuration: 5,
registrationFee: 0,
maxParticipants: 100,
sortOrder: 0,
rules: '',
remark: ''
})
// 上传配置
const uploadUrl = computed(() => {
return import.meta.env.VITE_APP_BASE_URL + '/api/blade-martial/project/import'
})
const uploadHeaders = computed(() => {
return {
'Blade-Auth': 'bearer ' + getToken()
}
})
// 表单验证规则
const rules = {
competitionId: [
{ required: true, message: '请选择所属赛事', trigger: 'change' }
],
projectCode: [
{ required: true, message: '请输入项目编码', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
],
category: [
{ required: true, message: '请选择分组类别', trigger: 'change' }
],
eventType: [
{ required: true, message: '请选择项目类型', trigger: 'change' }
],
estimatedDuration: [
{ required: true, message: '请输入预计时长', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择参赛类型', trigger: 'change' }
],
registrationFee: [
{ required: true, message: '请输入报名费', trigger: 'blur' }
],
maxParticipants: [
{ required: true, message: '请输入单位容纳人数', trigger: 'blur' }
]
}
// 加载赛事列表
const loadCompetitionList = async () => {
try {
const res = await getCompetitionList(1, 1000, {})
if (res.data && res.data.data && res.data.data.records) {
competitionList.value = res.data.data.records
}
} catch (error) {
console.error('加载赛事列表失败:', error)
}
}
// 表单中赛事变更时加载场地列表
const handleCompetitionChangeInForm = async (competitionId) => {
form.venueId = null
venueList.value = []
if (competitionId) {
try {
const res = await getVenuesByCompetition(competitionId)
venueList.value = res.data?.data?.records || []
} catch (error) {
console.error('加载场地列表失败:', error)
}
}
}
// 查询数据
const fetchData = async () => {
loading.value = true
try {
// Only pass parameters that backend supports
const params = {
competitionId: queryParams.competitionId || undefined,
projectName: queryParams.projectName || undefined,
category: queryParams.category || undefined,
eventType: queryParams.eventType || undefined,
type: queryParams.type || undefined
}
const res = await getProjectList(
queryParams.current,
queryParams.size,
params
)
if (res.data && res.data.data) {
tableData.value = res.data.data.records || []
total.value = res.data.data.total || 0
// 加载项目对应的场地信息
await loadVenuesForProjects(tableData.value)
}
} catch (error) {
ElMessage.error('获取数据失败')
console.error(error)
} finally {
loading.value = false
}
}
// 加载项目对应的场地信息
const loadVenuesForProjects = async (projects) => {
// 获取所有不同的赛事ID
const competitionIds = [...new Set(projects.map(p => p.competitionId).filter(Boolean))]
for (const compId of competitionIds) {
try {
const res = await getVenuesByCompetition(compId)
const venues = res.data?.data?.records || []
// 缓存场地信息
venues.forEach(v => {
allVenuesCache.value.set(v.id, v.venueName)
})
} catch (err) {
console.error('加载场地失败:', err)
}
}
}
// 搜索
const handleSearch = () => {
queryParams.current = 1
fetchData()
}
// 重置
const handleReset = () => {
Object.assign(queryParams, {
current: 1,
size: 10,
competitionId: '',
projectName: '',
category: '',
eventType: '',
type: ''
})
fetchData()
}
// 新增
const handleAdd = () => {
dialogTitle.value = '新增项目'
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = async (row) => {
dialogTitle.value = '编辑项目'
Object.keys(form).forEach((key) => {
form[key] = row[key]
})
// 处理字段名映射:后端返回 price,表单使用 registrationFee
if (row.price !== undefined) {
form.registrationFee = row.price
}
// 加载该赛事的场地列表
if (row.competitionId) {
try {
const res = await getVenuesByCompetition(row.competitionId)
venueList.value = res.data?.data?.records || []
} catch (error) {
console.error('加载场地列表失败:', error)
}
}
dialogVisible.value = true
}
// 查看
const handleView = (row) => {
detailData.value = { ...row }
detailVisible.value = true
}
// 删除
const handleDelete = (row) => {
ElMessageBox.confirm('确定要删除该项目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await removeProject(row.id)
ElMessage.success('删除成功')
fetchData()
} catch (error) {
ElMessage.error('删除失败')
}
})
.catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
ElMessageBox.confirm(`确定要删除选中的 ${selection.value.length} 个项目吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const ids = selection.value.map((item) => item.id).join(',')
await removeProject(ids)
ElMessage.success('删除成功')
fetchData()
} catch (error) {
ElMessage.error('删除失败')
}
})
.catch(() => {})
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
// 构建提交数据,确保字段名与后端一致
const submitData = {
...form,
price: form.registrationFee // 后端使用 price 字段
}
if (form.id) {
await submitProject(submitData)
ElMessage.success('修改成功')
} else {
await submitProject(submitData)
ElMessage.success('新增成功')
}
dialogVisible.value = false
fetchData()
} catch (error) {
ElMessage.error(form.id ? '修改失败' : '新增失败')
} finally {
submitLoading.value = false
}
}
})
}
// 选择变化
const handleSelectionChange = (val) => {
selection.value = val
}
// 关闭弹窗
const handleDialogClose = () => {
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(form, {
id: null,
competitionId: '',
projectCode: '',
projectName: '',
category: null,
eventType: null,
type: null,
estimatedDuration: 5,
registrationFee: 0,
maxParticipants: 100,
sortOrder: 0,
rules: '',
remark: ''
})
if (formRef.value) {
formRef.value.clearValidate()
}
}
// 上传前检查
const beforeUpload = (file) => {
if (!queryParams.competitionId) {
ElMessage.warning('请先选择赛事')
return false
}
const isExcel =
file.type === 'application/vnd.ms-excel' ||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件!')
return false
}
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
ElMessage.error('文件大小不能超过 5MB!')
return false
}
return true
}
// 上传成功
const handleUploadSuccess = (response) => {
if (response.success) {
ElMessage.success('导入成功')
fetchData()
} else {
ElMessage.error(response.msg || '导入失败')
}
}
// 上传失败
const handleUploadError = () => {
ElMessage.error('导入失败')
}
// 导出
const handleExport = async () => {
try {
const res = await exportProjects(queryParams)
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `项目列表_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`
link.click()
window.URL.revokeObjectURL(link.href)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
}
}
// 根据ID获取赛事名称
const getCompetitionName = (competitionId) => {
if (!competitionId) return '-'
const competition = competitionList.value.find(item => item.id === competitionId)
return competition ? competition.competitionName : '-'
}
const getVenueName = (venueId) => {
if (!venueId) return '-'
// 先从当前场地列表查找
let venue = venueList.value.find(item => item.id === venueId)
if (venue) return venue.venueName
// 再从全局缓存查找
if (allVenuesCache.value.has(venueId)) {
return allVenuesCache.value.get(venueId)
}
return '-'
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
// 生命周期
onMounted(() => {
loadCompetitionList()
fetchData()
})
</script>
<style scoped lang="scss">
.project-container {
padding: 20px;
.search-card,
.toolbar-card,
.table-card {
margin-bottom: 20px;
}
.search-form {
.el-form-item {
margin-bottom: 0;
}
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-left {
display: flex;
gap: 10px;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
</style>
+5 -7
View File
@@ -33,9 +33,8 @@
<el-form-item> <el-form-item>
<el-select v-model="searchForm.refereeType" placeholder="裁判类型" clearable size="small" style="width: 150px"> <el-select v-model="searchForm.refereeType" placeholder="裁判类型" clearable size="small" style="width: 150px">
<el-option label="全部" :value="null"></el-option> <el-option label="全部" :value="null"></el-option>
<el-option label="裁判" :value="1"></el-option> <el-option label="裁判" :value="1"></el-option>
<el-option label="裁判" :value="2"></el-option> <el-option label="普通裁判" :value="2"></el-option>
<el-option label="总裁" :value="3"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@@ -64,8 +63,8 @@
<el-table-column prop="idCard" label="身份证号" width="180" show-overflow-tooltip></el-table-column> <el-table-column prop="idCard" label="身份证号" width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="refereeType" label="裁判类型" width="100" align="center"> <el-table-column prop="refereeType" label="裁判类型" width="100" align="center">
<template #default="scope"> <template #default="scope">
<el-tag :type="scope.row.refereeType === 1 ? 'danger' : (scope.row.refereeType === 3 ? 'warning' : 'primary')" size="small"> <el-tag :type="scope.row.refereeType === 1 ? 'danger' : 'primary'" size="small">
{{ scope.row.refereeType === 1 ? '主裁判' : (scope.row.refereeType === 3 ? '总裁' : '裁判') }} {{ scope.row.refereeType === 1 ? '主裁判' : '普通裁判' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -138,8 +137,7 @@
<el-form-item label="裁判类型" prop="refereeType"> <el-form-item label="裁判类型" prop="refereeType">
<el-select v-model="formData.refereeType" placeholder="请选择裁判类型" style="width: 100%"> <el-select v-model="formData.refereeType" placeholder="请选择裁判类型" style="width: 100%">
<el-option label="主裁判" :value="1"></el-option> <el-option label="主裁判" :value="1"></el-option>
<el-option label="裁判" :value="2"></el-option> <el-option label="普通裁判" :value="2"></el-option>
<el-option label="总裁" :value="3"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
+41 -78
View File
@@ -101,10 +101,12 @@
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div> <div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="singleCount" label="单人项目" width="100" align="center"></el-table-column> <el-table-column prop="category" label="类别" width="80"></el-table-column>
<el-table-column prop="teamCount" label="集体项目" width="100" align="center"></el-table-column> <el-table-column prop="individual" label="2队" width="60" align="center"></el-table-column>
<el-table-column prop="male" label="" width="60" align="center"></el-table-column> <el-table-column prop="dual" label="2组" width="60" align="center"></el-table-column>
<el-table-column prop="female" label="" width="60" align="center"></el-table-column> <el-table-column prop="team1101" label="1101" width="60" align="center"></el-table-column>
<el-table-column prop="workers" label="工作人员" width="90" align="center"></el-table-column>
<el-table-column prop="female" label="女性组别" width="90" align="center"></el-table-column>
<el-table-column prop="total" label="合计" width="60" align="center"> <el-table-column prop="total" label="合计" width="60" align="center">
<template #default="scope"> <template #default="scope">
<span class="total-num">+{{ scope.row.total }}</span> <span class="total-num">+{{ scope.row.total }}</span>
@@ -123,11 +125,10 @@
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div> <div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="projectType" label="人/集体" width="100" align="center"></el-table-column> <el-table-column prop="participantCategory" label="人/单位" width="100"></el-table-column>
<el-table-column prop="athleteCount" label="人数/集体" width="100" align="center"></el-table-column> <el-table-column prop="teamCount" label="队伍" width="80" align="center"></el-table-column>
<el-table-column prop="groupCount" label="数" width="80" align="center"></el-table-column> <el-table-column prop="singleTeamPeople" label="单位型号人数" width="120" align="center"></el-table-column>
<el-table-column prop="estimatedDuration" label="时长(分)" width="100" align="center"></el-table-column> <el-table-column prop="estimatedDuration" label="剩下显时(公共)" width="150" align="center"></el-table-column>
<el-table-column prop="projectCode" label="项目编码" width="120" align="center"></el-table-column>
</el-table> </el-table>
</div> </div>
@@ -162,7 +163,6 @@ import {
import { getCompetitionDetail } from '@/api/martial/competition' import { getCompetitionDetail } from '@/api/martial/competition'
import { getProjectDetail } from '@/api/martial/project' import { getProjectDetail } from '@/api/martial/project'
import { getParticipantList } from '@/api/martial/participant' import { getParticipantList } from '@/api/martial/participant'
import { getScheduleResult } from '@/api/martial/activitySchedule'
export default { export default {
name: 'MartialRegistrationDetail', name: 'MartialRegistrationDetail',
@@ -335,113 +335,76 @@ export default {
const participants = await this.getParticipants() const participants = await this.getParticipants()
console.log('参赛人员列表返回:', participants) console.log('参赛人员列表返回:', participants)
// 预加载项目信息
await this.preloadProjectInfo(participants)
// 按单位分组统计 // 按单位分组统计
const unitMap = new Map() const unitMap = new Map()
participants.forEach(p => { participants.forEach(p => {
// 兼容驼峰和下划线命名 // 兼容驼峰和下划线命名
const unit = p.organization || p.teamName || p.team_name || '未知单位' const unit = p.organization || p.teamName || p.team_name || '未知单位'
const projectId = p.projectId || p.project_id
const project = this.projectCache.get(projectId)
const projectType = project ? (project.type || 1) : 1 // 1=单人, 2=集体
if (!unitMap.has(unit)) { if (!unitMap.has(unit)) {
unitMap.set(unit, { unitMap.set(unit, {
schoolUnit: unit, schoolUnit: unit,
singleCount: 0, category: '',
teamCount: 0, individual: 0,
male: 0, dual: 0,
team1101: 0,
workers: 0,
female: 0, female: 0,
total: 0 total: 0
}) })
} }
const stat = unitMap.get(unit) const stat = unitMap.get(unit)
stat.total++ stat.total++
if (p.gender === 2) stat.female++
// 按项目类型统计
if (projectType === 1) {
stat.singleCount++
} else {
stat.teamCount++
}
// 按性别统计
if (p.gender === 1) {
stat.male++
} else if (p.gender === 2) {
stat.female++
}
}) })
this.participantsData = Array.from(unitMap.values()) this.participantsData = Array.from(unitMap.values())
} catch (err) { } catch (err) {
console.error('加载参赛人员统计失败', err) console.error('加载参赛人员统计失败', err)
this.$message.warning('加载参赛人员统计失败') this.$message.warning('加载参赛人员统计失败')
// 使用空数组作为默认值
this.participantsData = [] this.participantsData = []
} }
}, },
// 加载项目时间统计(该赛事的所有项目及参赛人数) // 加载项目时间统计(该赛事的所有项目及参赛人数)
async loadProjectTimeStats() { async loadProjectTimeStats() {
try { try {
// 使用缓存的参赛者列表 // 使用缓存的参赛者列表
const participants = await this.getParticipants() const participants = await this.getParticipants()
// 预加载项目信息 // 2. 按项目ID分组
await this.preloadProjectInfo(participants) const projectMap = new Map()
// 获取编排数据,统计每个项目的分组数
let projectGroupCount = new Map()
try {
const scheduleRes = await getScheduleResult(this.competitionId)
const scheduleData = scheduleRes.data?.data
const scheduleGroups = scheduleData?.competitionGroups || scheduleData?.scheduleGroups || []
scheduleGroups.forEach(group => {
const groupProjectId = group.projectId
let key = null
if (groupProjectId && groupProjectId !== -1 && groupProjectId !== '-1') {
key = String(groupProjectId)
} else if (group.title) {
key = group.title.split(' ')[0]
}
if (key) {
projectGroupCount.set(key, (projectGroupCount.get(key) || 0) + 1)
}
})
} catch (scheduleErr) {
console.warn("获取编排数据失败,组数将显示为-:", scheduleErr)
}
// 按项目ID分组统计人数
const projectAthleteCount = new Map()
participants.forEach(athlete => { participants.forEach(athlete => {
// 兼容驼峰和下划线命名
const projectId = athlete.projectId || athlete.project_id const projectId = athlete.projectId || athlete.project_id
if (projectId) { if (projectId) {
projectAthleteCount.set(projectId, (projectAthleteCount.get(projectId) || 0) + 1) if (!projectMap.has(projectId)) {
projectMap.set(projectId, [])
}
projectMap.get(projectId).push(athlete)
} }
}) })
// 从缓存中获取项目信息并构建统计数据 // 3. 从缓存中获取项目信息并统计(项目信息已经在 loadRegistrationStats 中预加载)
const projectStats = [] const projectStats = []
for (const [projectId, count] of projectAthleteCount) { for (const [projectId, athleteList] of projectMap) {
const project = this.projectCache.get(projectId) const project = this.projectCache.get(projectId)
if (project) { if (project) {
const projectType = project.type || 1 // 1=单人, 2=集体
projectStats.push({ projectStats.push({
projectName: project.projectName || project.project_name || '未知项目', projectName: project.projectName || project.project_name || '未知项目',
projectType: projectType === 1 ? '单人' : '集体', participantCategory: project.category || '',
athleteCount: count, teamCount: 1, // 简化处理,设为1
groupCount: singleTeamPeople: athleteList.length,
projectGroupCount.get(String(projectId)) || estimatedDuration: project.estimatedDuration || project.estimated_duration || 0
projectGroupCount.get(project.projectName || project.project_name || '') || })
'-', } else {
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0, // 如果缓存中没有(理论上应该发生),添加基本信息
projectCode: project.projectCode || project.project_code || '' projectStats.push({
projectName: `项目ID:${projectId}`,
participantCategory: '',
teamCount: 1,
singleTeamPeople: athleteList.length,
estimatedDuration: 0
}) })
} }
} }
@@ -454,7 +417,7 @@ export default {
} }
}, },
// 加载金额统计(该赛事所有单位的报名金额) // 加载金额统计(该赛事所有单位的报名金额)
async loadAmountStats() { async loadAmountStats() {
try { try {
// 使用缓存的参赛者列表 // 使用缓存的参赛者列表
@@ -1,145 +0,0 @@
<template>
<el-dialog
title="导出预览"
:model-value="visible"
@update:model-value="handleVisibleChange"
width="90%"
top="5vh"
:before-close="handleClose"
>
<div class="export-options">
<el-form :inline="true" size="small">
<el-form-item label="导出模板">
<el-select v-model="exportTemplate" placeholder="选择模板" style="width: 200px">
<el-option label="模板1-详细赛程表" value="template1" />
<el-option label="模板2-比赛时间表" value="template2" />
</el-select>
</el-form-item>
<el-form-item label="场地" v-if="exportTemplate === 'template2'">
<el-select v-model="selectedVenue" placeholder="选择场地" style="width: 150px">
<el-option label="全部场地" :value="null" />
<el-option v-for="venue in venues" :key="venue.id" :label="venue.venueName" :value="venue.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadPreviewData" :loading="loading">刷新预览</el-button>
</el-form-item>
</el-form>
</div>
<div class="preview-table" v-loading="loading">
<el-table v-if="exportTemplate === 'template1'" :data="previewData" border stripe max-height="500" style="width: 100%">
<el-table-column prop="scheduleDate" label="比赛日期" width="120" />
<el-table-column prop="timeSlot" label="时间段" width="100" />
<el-table-column prop="venueName" label="场地" width="120" />
<el-table-column prop="projectName" label="项目名称" width="180" />
<el-table-column prop="category" label="组别" width="80" />
<el-table-column prop="athleteName" label="运动员姓名" width="120" />
<el-table-column prop="teamName" label="单位/队伍" width="150" />
<el-table-column prop="sortOrder" label="出场顺序" width="90" />
<el-table-column prop="status" label="状态" width="80" />
</el-table>
<el-table v-if="exportTemplate === 'template2'" :data="previewData" border stripe max-height="500" style="width: 100%">
<el-table-column prop="sequenceNo" label="序号" width="80" />
<el-table-column prop="projectName" label="项目" width="200" />
<el-table-column prop="participantCount" label="人数" width="80" />
<el-table-column prop="groupCount" label="组数" width="80" />
<el-table-column prop="durationMinutes" label="时间" width="80" />
<el-table-column prop="tableNo" label="表号" width="100" />
</el-table>
<div v-if="previewData.length === 0 && !loading" class="empty-tip">暂无数据</div>
</div>
<div class="preview-stats" v-if="previewData.length > 0">
<span> {{ previewData.length }} 条记录</span>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleExport" :loading="exporting">确认导出</el-button>
</template>
</el-dialog>
</template>
<script>
import { getScheduleExportPreview, exportSchedule, exportScheduleTemplate2 } from '@/api/martial/activitySchedule'
export default {
name: 'ExportPreview',
props: {
visible: { type: Boolean, default: false },
competitionId: { type: [String, Number], required: true },
competitionName: { type: String, default: '' },
venues: { type: Array, default: () => [] }
},
emits: ['update:visible'],
data() {
return {
exportTemplate: 'template1',
selectedVenue: null,
previewData: [],
loading: false,
exporting: false
}
},
watch: {
visible(val) { if (val) this.loadPreviewData() },
exportTemplate() { this.loadPreviewData() },
selectedVenue() { if (this.exportTemplate === 'template2') this.loadPreviewData() }
},
methods: {
handleVisibleChange(val) { this.$emit('update:visible', val) },
async loadPreviewData() {
this.loading = true
try {
const res = await getScheduleExportPreview(this.competitionId, this.exportTemplate, this.selectedVenue)
this.previewData = res.data?.data || res.data || []
} catch (error) {
console.error('加载预览数据失败:', error)
this.$message.error('加载预览数据失败')
this.previewData = []
} finally {
this.loading = false
}
},
async handleExport() {
this.exporting = true
try {
let res, filename
if (this.exportTemplate === 'template1') {
res = await exportSchedule(this.competitionId)
filename = '赛程表_' + (this.competitionName || this.competitionId) + '.xlsx'
} else {
const venue = this.venues.find(v => v.id === this.selectedVenue)
const venueName = venue ? venue.venueName : null
res = await exportScheduleTemplate2(this.competitionId, this.selectedVenue, venueName, null)
filename = '比赛时间_' + (venueName || '全部场地') + '_' + (this.competitionName || this.competitionId) + '.xlsx'
}
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = filename
link.click()
window.URL.revokeObjectURL(link.href)
this.$message.success('导出成功')
this.handleClose()
} catch (error) {
console.error('导出失败:', error)
this.$message.error('导出失败')
} finally {
this.exporting = false
}
},
handleClose() { this.$emit('update:visible', false) }
}
}
</script>
<style scoped>
.export-options { margin-bottom: 20px; padding: 15px; background: #f5f7fa; border-radius: 4px; }
.preview-table { min-height: 300px; }
.preview-stats { margin-top: 15px; color: #666; font-size: 14px; }
.empty-tip { text-align: center; padding: 60px 0; color: #999; }
</style>
+84 -246
View File
@@ -7,13 +7,12 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<el-button <el-button
v-if="!isScheduleCompleted"
size="small" size="small"
type="primary" type="primary"
:loading="autoArrangeLoading" :loading="autoArrangeLoading"
@click="handleAutoArrange" @click="handleAutoArrange"
> >
自动编排 {{ isScheduleCompleted ? '重新编排' : '自动编排' }}
</el-button> </el-button>
<el-button size="small" type="danger" @click="showExceptionDialog"> <el-button size="small" type="danger" @click="showExceptionDialog">
异常组 <el-badge :value="exceptionList.length" :hidden="exceptionList.length === 0" /> 异常组 <el-badge :value="exceptionList.length" :hidden="exceptionList.length === 0" />
@@ -86,9 +85,8 @@
<span class="project-title">{{ group.title }}</span> <span class="project-title">{{ group.title }}</span>
<span class="project-meta">{{ group.type }}</span> <span class="project-meta">{{ group.type }}</span>
<span class="project-meta">{{ getTeamCount(group) }}</span> <span class="project-meta">{{ getTeamCount(group) }}</span>
<span class="project-meta">{{ group.type === '集体' ? getTeamCount(group) : 1 }}</span> <span class="project-meta">{{ group.items?.length || 0 }}</span>
<!-- <span class="project-meta">{{ group.code }}</span> --> <span class="project-meta">{{ group.code }}</span>
<span class="project-table-no">表号: {{ generateTableNo(group) }}</span>
</div> </div>
<div class="project-actions" @click.stop> <div class="project-actions" @click.stop>
<el-popover <el-popover
@@ -104,7 +102,7 @@
</template> </template>
<div class="move-popover"> <div class="move-popover">
<div class="move-label">移动到</div> <div class="move-label">移动到</div>
<el-select v-model="moveTargetVenueId" placeholder="选择场地" size="small" style="width: 100%; margin-bottom: 10px;" :teleported="false"> <el-select v-model="moveTargetVenueId" placeholder="选择场地" size="small" style="width: 100%; margin-bottom: 10px;">
<el-option <el-option
v-for="venue in venues" v-for="venue in venues"
:key="venue.id" :key="venue.id"
@@ -186,7 +184,36 @@
> >
<span class="player-detail-index">{{ playerIndex + 1 }}</span> <span class="player-detail-index">{{ playerIndex + 1 }}</span>
<span class="player-detail-name">{{ player.playerName }}</span> <span class="player-detail-name">{{ player.playerName }}</span>
<!-- 签到状态和异常操作只在调度功能中显示 --> <span class="player-detail-status">
<el-tag
:type="player.status === '已签到' ? 'success' : player.status === '异常' ? 'danger' : 'info'"
size="small"
>
{{ player.status || '未签到' }}
</el-tag>
</span>
<span class="player-detail-actions">
<el-button
v-if="(player.status || '未签到') === '未签到'"
link
size="small"
@click="markPlayerAsException(group, team, playerIndex)"
:disabled="isScheduleCompleted"
style="color: #f56c6c;"
>
标记异常
</el-button>
<el-button
v-if="player.status === '异常'"
link
size="small"
@click="removePlayerException(group, team, playerIndex)"
:disabled="isScheduleCompleted"
style="color: #67c23a;"
>
取消异常
</el-button>
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -240,7 +267,7 @@
<el-table-column prop="duration" label="合计时间" width="100" align="center"></el-table-column> <el-table-column prop="duration" label="合计时间" width="100" align="center"></el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center"> <el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope"> <template #default="scope">
<span :class="['status-tag', scope.row.status === 2 ? 'status-completed' : scope.row.status === 1 ? 'status-progress' : 'status-pending']">{{ scope.row.status === 2 ? '已完成' : scope.row.status === 1 ? '进行中' : '未开始' }}</span> <span v-if="scope.row.status" class="status-active">{{ scope.row.status }}</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -249,7 +276,7 @@
<div class="footer-actions"> <div class="footer-actions">
<el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button> <el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button>
<el-button v-if="isScheduleCompleted" size="small" @click="exportPreviewVisible = true">导出</el-button> <el-button size="small" @click="handleExport" v-if="isScheduleCompleted">导出</el-button>
<el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button> <el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button>
</div> </div>
@@ -346,13 +373,6 @@
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
<!-- 导出预览对话框 -->
<ExportPreview
v-model:visible="exportPreviewVisible"
:competition-id="competitionId"
:competition-name="competitionInfo.competitionName"
:venues="venues"
/>
</div> </div>
</template> </template>
@@ -360,14 +380,11 @@
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue' import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { getVenuesByCompetition } from '@/api/martial/venue' import { getVenuesByCompetition } from '@/api/martial/venue'
import { getCompetitionDetail } from '@/api/martial/competition' import { getCompetitionDetail } from '@/api/martial/competition'
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule, exportScheduleTemplate2 } from '@/api/martial/activitySchedule' import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule } from '@/api/martial/activitySchedule'
import { updateCheckInStatus, getScheduleConfig } from '@/api/martial/schedulePlan'
import ExportPreview from './ExportPreview.vue'
export default { export default {
name: 'MartialScheduleList', name: 'MartialScheduleList',
components: { components: {
ExportPreview,
ArrowDown, ArrowDown,
ArrowRight ArrowRight
}, },
@@ -381,7 +398,6 @@ export default {
selectedVenueIdForVenueTab: null, // 选中的场地ID(场地Tab selectedVenueIdForVenueTab: null, // 选中的场地ID(场地Tab
confirmDialogVisible: false, confirmDialogVisible: false,
isScheduleCompleted: false, // 是否已完成编排 isScheduleCompleted: false, // 是否已完成编排
exportPreviewVisible: false, // 导出预览对话框
loading: false, loading: false,
autoArrangeLoading: false, // 自动编排加载状态 autoArrangeLoading: false, // 自动编排加载状态
venues: [], // 场地列表(从后端加载) venues: [], // 场地列表(从后端加载)
@@ -403,8 +419,7 @@ export default {
exceptionDialogVisible: false, exceptionDialogVisible: false,
exceptionList: [], // 异常参赛人员列表 exceptionList: [], // 异常参赛人员列表
expandedTeams: {}, // 展开的队伍 { 'groupId-teamId': true } expandedTeams: {}, // 展开的队伍 { 'groupId-teamId': true }
expandedProjects: {}, // 展开的项目 { 'groupId': true },默认收起 expandedProjects: {} // 展开的项目 { 'groupId': true },默认收起
scheduleConfig: { morningStartTime: '08:00', afternoonStartTime: '14:00' } // 赛程配置
} }
}, },
computed: { computed: {
@@ -422,7 +437,7 @@ export default {
const filtered = this.competitionGroups.filter(group => { const filtered = this.competitionGroups.filter(group => {
console.log(`分组"${group.title}": venueId=${group.venueId}(${typeof group.venueId}), timeSlotIndex=${group.timeSlotIndex}(${typeof group.timeSlotIndex})`) console.log(`分组"${group.title}": venueId=${group.venueId}(${typeof group.venueId}), timeSlotIndex=${group.timeSlotIndex}(${typeof group.timeSlotIndex})`)
const venueMatch = String(group.venueId) === String(this.selectedVenueId) const venueMatch = group.venueId === this.selectedVenueId || Number(group.venueId) === Number(this.selectedVenueId)
const timeMatch = group.timeSlotIndex === this.selectedTime const timeMatch = group.timeSlotIndex === this.selectedTime
console.log(` 场地匹配:${venueMatch}, 时间匹配:${timeMatch}`) console.log(` 场地匹配:${venueMatch}, 时间匹配:${timeMatch}`)
return venueMatch && timeMatch return venueMatch && timeMatch
@@ -446,7 +461,8 @@ export default {
// 如果选中了场地,进一步过滤 // 如果选中了场地,进一步过滤
if (this.selectedVenueIdForVenueTab) { if (this.selectedVenueIdForVenueTab) {
groupsInTimeSlot = groupsInTimeSlot.filter(group => { groupsInTimeSlot = groupsInTimeSlot.filter(group => {
return String(group.venueId) === String(this.selectedVenueIdForVenueTab) return group.venueId === this.selectedVenueIdForVenueTab ||
Number(group.venueId) === Number(this.selectedVenueIdForVenueTab)
}) })
} }
@@ -456,8 +472,8 @@ export default {
participant: group.type, participant: group.type,
team: 1, // 队伍数量,可以从分组中计算 team: 1, // 队伍数量,可以从分组中计算
number: group.items?.length || 0, // 参赛人数 number: group.items?.length || 0, // 参赛人数
duration: group.estimatedDuration || 0, // 合计时间 duration: 0, // 合计时间,如果后端有数据可以显示
status: group.status, // 状态: 0-未开始, 1-进行中, 2-已完成 status: group.venueName, // 显示场地名称
venueName: group.venueName, venueName: group.venueName,
hint: '' // 可以根据需要添加提示信息 hint: '' // 可以根据需要添加提示信息
})) }))
@@ -469,7 +485,6 @@ export default {
this.orderId = this.$route.query.orderId this.orderId = this.$route.query.orderId
if (this.competitionId) { if (this.competitionId) {
this.loadScheduleConfig()
this.loadCompetitionInfo() this.loadCompetitionInfo()
this.loadVenues() this.loadVenues()
this.loadScheduleData() this.loadScheduleData()
@@ -478,43 +493,6 @@ export default {
} }
}, },
methods: { methods: {
// 生成表号: 场地(1位) + 时段(1位,上午=1/下午=2) + 序号(2位)
generateTableNo(group) {
// 1. 获取场地编号
let venueNo = 1
if (group.venueId) {
const venue = this.venues.find(v => v.id === group.venueId || String(v.id) === String(group.venueId))
if (venue && venue.venueName) {
// 从场地名称提取数字
const match = venue.venueName.match(/\d+/)
if (match) {
venueNo = parseInt(match[0])
}
}
}
// 2. 获取时段:上午=1, 下午=2
let period = 1
if (group.timeSlot) {
const hour = parseInt(group.timeSlot.split(':')[0])
period = hour < 12 ? 1 : 2
}
// 3. 获取序号:在同场地同时段中的顺序(与显示顺序一致)
const sameSlotGroups = this.competitionGroups.filter(g => {
const gVenueMatch = String(g.venueId) === String(group.venueId)
if (!gVenueMatch) return false
const gHour = parseInt((g.timeSlot || '08:30').split(':')[0])
const gPeriod = gHour < 12 ? 1 : 2
return gPeriod === period
})
// 使用原始数组顺序(与显示顺序一致),不再按id排序
const orderIndex = sameSlotGroups.findIndex(g => g.id === group.id) + 1
// 4. 格式化: 场地(1位) + 时段(1位) + 序号(2位)
return `${venueNo}${period}${String(orderIndex).padStart(2, '0')}`
},
// 检查项目是否展开 // 检查项目是否展开
isProjectExpanded(groupId) { isProjectExpanded(groupId) {
return this.expandedProjects[groupId] === true return this.expandedProjects[groupId] === true
@@ -551,11 +529,6 @@ export default {
// 标记选手为异常 // 标记选手为异常
markPlayerAsException(group, team, playerIndex) { markPlayerAsException(group, team, playerIndex) {
if (this.isScheduleCompleted) {
this.$message.warning('编排已完成,无法标记异常')
return
}
const player = team.players[playerIndex] const player = team.players[playerIndex]
if (player) { if (player) {
player.status = '异常' player.status = '异常'
@@ -569,23 +542,12 @@ export default {
playerName: player.playerName, playerName: player.playerName,
status: '异常' status: '异常'
}) })
this.$message.success('已标记为异常')
// 调用后端API保存状态
updateCheckInStatus(player.id, '异常').then(() => {
this.$message.success(`已将 ${player.playerName} 标记为异常`)
}).catch(err => {
console.error('保存异常状态失败:', err)
})
} }
}, },
// 取消选手异常状态 // 取消选手异常状态
removePlayerException(group, team, playerIndex) { removePlayerException(group, team, playerIndex) {
if (this.isScheduleCompleted) {
this.$message.warning('编排已完成,无法取消异常')
return
}
const player = team.players[playerIndex] const player = team.players[playerIndex]
if (player) { if (player) {
player.status = '未签到' player.status = '未签到'
@@ -596,13 +558,7 @@ export default {
if (idx !== -1) { if (idx !== -1) {
this.exceptionList.splice(idx, 1) this.exceptionList.splice(idx, 1)
} }
this.$message.success('已取消异常标记')
// 调用后端API恢复状态
updateCheckInStatus(player.id, '未签到').then(() => {
this.$message.success(`已将 ${player.playerName} 取消异常标记`)
}).catch(err => {
console.error('恢复状态失败:', err)
})
} }
}, },
@@ -620,22 +576,8 @@ export default {
playerIds: [] playerIds: []
}) })
} }
// If item has members array (from API), use it as players teamMap.get(key).players.push(item)
if (item.members && item.members.length > 0) { teamMap.get(key).playerIds.push(item.id)
item.members.forEach(member => {
teamMap.get(key).players.push({
id: member.id,
playerName: member.name || member.playerName,
organization: member.organization,
status: member.status
})
teamMap.get(key).playerIds.push(member.id)
})
} else {
// Fallback: use item itself as player
teamMap.get(key).players.push(item)
teamMap.get(key).playerIds.push(item.id)
}
}) })
return Array.from(teamMap.values()) return Array.from(teamMap.values())
}, },
@@ -656,14 +598,6 @@ export default {
const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId) const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)
// In draft mode (negative groupId), just update frontend state
if (group.id && group.id.toString().startsWith("-")) {
group.venueId = this.moveTargetVenueId
group.venueName = targetVenue ? targetVenue.venueName : ""
this.$message.success(`已移动到 ${group.venueName}`)
return
}
try { try {
// 调用后端API移动分组 // 调用后端API移动分组
const res = await moveScheduleGroup({ const res = await moveScheduleGroup({
@@ -781,6 +715,32 @@ export default {
this.$message.success('下移成功') this.$message.success('下移成功')
}, },
// 标记单个选手为异常
markPlayerAsException(group, player) {
if (this.isScheduleCompleted) {
this.$message.warning('编排已完成,无法标记异常')
return
}
// 在 group.items 中找到该选手并修改状态
const item = group.items.find(i => i.id === player.id)
if (item) {
item.status = '异常'
// 添加到异常列表
this.exceptionList.push({
groupId: group.id,
groupTitle: group.title,
participantId: player.id,
schoolUnit: player.schoolUnit,
playerName: player.playerName,
status: '异常'
})
this.$message.success(`已将 ${player.playerName} 标记为异常`)
}
},
goBack() { goBack() {
this.$router.go(-1) this.$router.go(-1)
}, },
@@ -809,43 +769,15 @@ export default {
}, },
// 根据开始和结束时间生成时间段列表 // 根据开始和结束时间生成时间段列表
// 加载赛程配置
async loadScheduleConfig() {
try {
const res = await getScheduleConfig()
if (res.data?.data) {
this.scheduleConfig = res.data.data
console.log('赛程配置:', this.scheduleConfig)
}
} catch (err) {
console.error('加载赛程配置失败', err)
}
},
// 格式化时间为显示格式 (08:00 -> 8:00)
formatTimeForDisplay(time) {
if (!time) return '8:00'
const parts = time.split(':')
const hour = parseInt(parts[0], 10)
const minute = parts[1] || '00'
return `${hour}:${minute}`
},
generateTimeSlots() { generateTimeSlots() {
const startTime = this.competitionInfo.competitionStartTime const startTime = this.competitionInfo.competitionStartTime
const endTime = this.competitionInfo.competitionEndTime const endTime = this.competitionInfo.competitionEndTime
// 从配置获取时间,格式化为显示格式
const morningTime = this.formatTimeForDisplay(this.scheduleConfig.morningStartTime || '08:00')
const afternoonTime = this.formatTimeForDisplay(this.scheduleConfig.afternoonStartTime || '14:00')
if (!startTime || !endTime) { if (!startTime || !endTime) {
this.$message.warning('赛事时间信息不完整,使用默认时间段') this.$message.warning('赛事时间信息不完整,使用默认时间段')
const today = new Date()
const dateStr = `${today.getFullYear()}${today.getMonth() + 1}${today.getDate()}`
this.timeSlots = [ this.timeSlots = [
`${dateStr} 上午${morningTime}`, '2025年11月6日 上午8:30',
`${dateStr} 下午${afternoonTime}` '2025年11月6日 下午13:30'
] ]
return return
} }
@@ -856,6 +788,7 @@ export default {
// 遍历每一天 // 遍历每一天
let currentDate = new Date(start) let currentDate = new Date(start)
let dayIndex = 1
while (currentDate <= end) { while (currentDate <= end) {
const year = currentDate.getFullYear() const year = currentDate.getFullYear()
@@ -863,63 +796,21 @@ export default {
const day = currentDate.getDate() const day = currentDate.getDate()
const dateStr = `${year}${month}${day}` const dateStr = `${year}${month}${day}`
// 添加上午时段 // 添加上午时段 8:30
slots.push(`${dateStr} 上午${morningTime}`) slots.push(`${dateStr} 上午8:30`)
// 添加下午时段 // 添加下午时段 13:30
slots.push(`${dateStr} 下午${afternoonTime}`) slots.push(`${dateStr} 下午13:30`)
// 下一天 // 下一天
currentDate.setDate(currentDate.getDate() + 1) currentDate.setDate(currentDate.getDate() + 1)
dayIndex++
} }
this.timeSlots = slots this.timeSlots = slots
console.log('生成的时间段:', this.timeSlots) console.log('生成的时间段:', this.timeSlots)
}, },
// 根据实际编排数据更新显示的时间段(只显示有比赛的时段)
updateTimeSlotsFromGroups() {
if (!this.competitionGroups || this.competitionGroups.length === 0) {
return
}
// 从分组数据中提取实际使用的时段索引
const usedTimeSlotIndexes = new Set()
this.competitionGroups.forEach(group => {
if (group.timeSlotIndex !== null && group.timeSlotIndex !== undefined) {
usedTimeSlotIndexes.add(group.timeSlotIndex)
}
})
if (usedTimeSlotIndexes.size === 0) {
return
}
// 保存原始时段用于映射
const allSlots = [...this.timeSlots]
const filteredSlots = []
const indexMapping = {}
let newIndex = 0
allSlots.forEach((slot, oldIndex) => {
if (usedTimeSlotIndexes.has(oldIndex)) {
filteredSlots.push(slot)
indexMapping[oldIndex] = newIndex
newIndex++
}
})
// 更新分组的时段索引
this.competitionGroups.forEach(group => {
if (group.timeSlotIndex !== null && group.timeSlotIndex !== undefined) {
const newIdx = indexMapping[group.timeSlotIndex]
if (newIdx !== undefined) {
group.timeSlotIndex = newIdx
}
}
})
this.timeSlots = filteredSlots
console.log("过滤后的时间段:", this.timeSlots)
if (this.selectedTime >= this.timeSlots.length) {
this.selectedTime = 0
}
},
// 加载场地列表 // 加载场地列表
async loadVenues() { async loadVenues() {
try { try {
@@ -981,14 +872,12 @@ export default {
venueName: group.venueName, venueName: group.venueName,
timeSlot: group.timeSlot, timeSlot: group.timeSlot,
timeSlotIndex: group.timeSlotIndex, timeSlotIndex: group.timeSlotIndex,
estimatedDuration: group.estimatedDuration || 0,
items: (group.participants || []).map(p => ({ items: (group.participants || []).map(p => ({
id: p.id, id: p.id,
schoolUnit: p.schoolUnit, schoolUnit: p.schoolUnit,
playerName: p.playerName, playerName: p.playerName,
status: p.status || '未签到', status: p.status || '未签到',
sortOrder: p.sortOrder, sortOrder: p.sortOrder
members: p.members || []
})) }))
})) }))
@@ -1011,7 +900,6 @@ export default {
console.log('解析后的competitionGroups:', this.competitionGroups) console.log('解析后的competitionGroups:', this.competitionGroups)
console.log('当前selectedVenueId:', this.selectedVenueId) console.log('当前selectedVenueId:', this.selectedVenueId)
console.log('当前selectedTime:', this.selectedTime) console.log('当前selectedTime:', this.selectedTime)
this.updateTimeSlotsFromGroups()
this.$message.success(data.isDraft ? '已加载草稿数据' : '已加载编排数据') this.$message.success(data.isDraft ? '已加载草稿数据' : '已加载编排数据')
} else { } else {
// 后端还没有数据,使用默认数据进行展示 // 后端还没有数据,使用默认数据进行展示
@@ -1105,12 +993,7 @@ export default {
status: '异常' status: '异常'
}) })
// 调用后端API保存状态 this.$message.success(`已将 ${item.schoolUnit} 标记为异常`)
updateCheckInStatus(item.id, '异常').then(() => {
this.$message.success(`已将 ${item.schoolUnit} 标记为异常`)
}).catch(err => {
console.error('保存异常状态失败:', err)
})
}, },
// 显示异常组对话框 // 显示异常组对话框
@@ -1137,12 +1020,7 @@ export default {
// 从异常列表中移除 // 从异常列表中移除
this.exceptionList.splice(index, 1) this.exceptionList.splice(index, 1)
// 调用后端API恢复状态 this.$message.success(`已将 ${exceptionItem.schoolUnit} 从异常组移除`)
updateCheckInStatus(exceptionItem.participantId, '未签到').then(() => {
this.$message.success(`已将 ${exceptionItem.schoolUnit} 从异常组移除`)
}).catch(err => {
console.error('恢复状态失败:', err)
})
}, },
// 触发自动编排 // 触发自动编排
@@ -1197,7 +1075,6 @@ export default {
venueName: group.venueName, venueName: group.venueName,
timeSlot: group.timeSlot, timeSlot: group.timeSlot,
timeSlotIndex: group.timeSlotIndex, timeSlotIndex: group.timeSlotIndex,
estimatedDuration: group.estimatedDuration || 0,
participants: group.items.map((item, index) => ({ participants: group.items.map((item, index) => ({
id: item.id, id: item.id,
schoolUnit: item.schoolUnit, schoolUnit: item.schoolUnit,
@@ -1236,34 +1113,6 @@ export default {
group.items.splice(itemIndex + 1, 0, temp) group.items.splice(itemIndex + 1, 0, temp)
this.$message.success('下移成功') this.$message.success('下移成功')
}, },
handleExportCommand(command) {
if (command === 'template1') {
this.handleExport()
} else if (command === 'template2') {
this.handleExportTemplate2()
}
},
async handleExportTemplate2() {
try {
this.loading = true
const venueId = this.selectedVenueId
const venue = this.venues.find(v => v.id === venueId)
const venueName = venue ? venue.venueName : null
const res = await exportScheduleTemplate2(this.competitionId, venueId, venueName, null)
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `比赛时间_${venueName || '全部场地'}_${this.competitionInfo.competitionName || this.competitionId}.xlsx`
link.click()
window.URL.revokeObjectURL(link.href)
this.$message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
this.$message.error('导出失败,请稍后重试')
} finally {
this.loading = false
}
},
async handleExport() { async handleExport() {
try { try {
this.loading = true this.loading = true
@@ -1314,7 +1163,6 @@ export default {
venueName: group.venueName, venueName: group.venueName,
timeSlot: group.timeSlot, timeSlot: group.timeSlot,
timeSlotIndex: group.timeSlotIndex, timeSlotIndex: group.timeSlotIndex,
estimatedDuration: group.estimatedDuration || 0,
participants: group.items.map((item, index) => ({ participants: group.items.map((item, index) => ({
id: item.id, id: item.id,
schoolUnit: item.schoolUnit, schoolUnit: item.schoolUnit,
@@ -1538,16 +1386,6 @@ export default {
color: #606266; color: #606266;
font-size: 13px; font-size: 13px;
} }
.project-table-no {
color: #409EFF;
font-size: 13px;
font-weight: 500;
margin-left: 10px;
padding: 2px 8px;
background-color: #ecf5ff;
border-radius: 4px;
}
} }
.project-actions { .project-actions {
+8 -26
View File
@@ -94,8 +94,7 @@
<el-table-column label="总裁判分数" width="120" align="center" fixed="right"> <el-table-column label="总裁判分数" width="120" align="center" fixed="right">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.scoreStatus === 2" class="total-score">{{ formatScore(scope.row.chiefJudgeScore) }}</span> <span class="total-score">{{ formatScore(scope.row.totalScore) }}</span>
<span v-else class="pending-score">待确认</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -154,17 +153,12 @@
<div class="total-score-display"> <div class="total-score-display">
<span class="label">总分</span> <span class="label">总分</span>
<template v-if="currentDetail.scoreStatus === 2"> <span class="value">{{ formatScore(currentDetail.totalScore) }}</span>
<span class="value">{{ formatScore(currentDetail.chiefJudgeScore) }}</span> <div class="calculation-note">
<div class="calculation-note">(主裁判已确认)</div> <span v-if="currentDetail.judgeScores.length > 2">
</template> (去掉最高分和最低分后的平均分)
<template v-else> </span>
<span class="value pending">待确认最终得分</span> </div>
<div class="calculation-note">
裁判员评分: {{ formatScore(currentDetail.totalScore) }}
<span v-if="currentDetail.judgeScores.length > 2">(去掉最高最低分后平均)</span>
</div>
</template>
</div> </div>
</div> </div>
</div> </div>
@@ -404,9 +398,7 @@ export default {
playerNo: score.playerNo || '', playerNo: score.playerNo || '',
judgeScores: [], judgeScores: [],
scoreDetails: [], scoreDetails: [],
totalScore: 0, totalScore: 0
chiefJudgeScore: score.chiefJudgeScore,
scoreStatus: score.scoreStatus || 0
}) })
} }
@@ -573,16 +565,6 @@ export default {
} }
.total-score { .total-score {
}
.pending-score {
color: #e6a23c;
font-weight: 500;
}
.value.pending {
color: #e6a23c;
font-weight: 500;
color: #1b7c5e; color: #1b7c5e;
font-weight: 700; font-weight: 700;
font-size: 14px; font-size: 14px;
+1 -1
View File
@@ -59,7 +59,7 @@
<i class="el-icon-s-order"></i> <i class="el-icon-s-order"></i>
</div> </div>
<div class="card-info"> <div class="card-info">
<h3>赛事管理</h3> <h3>订单管理</h3>
<p>查看和管理订单</p> <p>查看和管理订单</p>
</div> </div>
<i class="card-arrow el-icon-arrow-right"></i> <i class="card-arrow el-icon-arrow-right"></i>
+1 -2
View File
@@ -14,8 +14,7 @@ export default ({ mode, command }) => {
__INTLIFY_PROD_DEVTOOLS__: false, __INTLIFY_PROD_DEVTOOLS__: false,
}, },
server: { server: {
port: 8083, port: 2888,
host: '0.0.0.0',
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8123', target: 'http://localhost:8123',