Compare commits

..

48 Commits

Author SHA1 Message Date
DevOps
b67a1e039c feat: 移除项目编码手动输入
- 项目编码由后端自动生成
- 移除表单中的项目编码输入框和验证规则

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 17:19:33 +08:00
DevOps
c7e78612bf feat: 添加单位统计API调用
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 16:08:47 +08:00
DevOps
49c1cd81c6 重构项3: 添加表号生成和显示功能
- 在编排页面项目头部添加表号显示
- 实现generateTableNo方法,格式为: 场地(1位)+时段(1位)+序号(2位)
- 时段规则: 上午=1, 下午=2
- 序号在同场地同时段中按id排序确定

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:46:48 +08:00
DevOps
420bd29eff 重构项1: 移除项目管理中的场地分配功能
- 移除表格中的所属场地列
- 移除表单中的场地选择字段
- 移除场地相关的API导入和数据定义
- 移除handleCompetitionChangeInForm、loadVenuesForProjects、getVenueName等函数
- 后端已实现动态场地分配,无需前端指定

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:37:07 +08:00
DevOps
a1b26208a4 feat: 赛事管理页面状态根据时间自动计算
- 添加 calculateStatus 方法根据报名时间和比赛时间计算状态
- 状态显示不再依赖数据库字段,而是实时计算

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 12:28:07 +08:00
DevOps
41c67e1ddf fix: 将订单管理改为赛事管理
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 11:58:03 +08:00
DevOps
e5b028f084 fix: 修复场地类型(venueType)加载和保存问题
- 在loadVenues中添加venueType字段映射,确保从后端加载时正确回显
- 在saveVenues中添加venueType字段,确保保存时正确提交
- 修复附件上传headers认证问题

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 15:17:28 +08:00
DevOps
6385acd43b fix(deduction): 修复扣分项编辑功能
- form对象添加itemName字段解决编辑时名称不显示
- 查询时过滤空字符串参数避免无数据问题
- 字段名deductionPoints改为deductionPoint与后端一致
2026-01-06 14:56:10 +08:00
DevOps
c37b6d8f6f fix(competition): 修复附件上传按钮失灵问题
- 添加 getToken 导入
- 为 avue-form 上传组件添加 Blade-Auth headers 认证

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 12:52:28 +08:00
DevOps
98c831eff0 fix: 修复报名详情页面文案和统计逻辑
- 修正错误文案: 单位型号人数 → 报名人数, 剩下显时(公共) → 预计时长(分钟)
- 重新设计参赛人数统计表格: 单位/单人项目/集体项目/男/女/合计
- 修复统计逻辑按项目类型和性别正确统计

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 11:47:56 +08:00
DevOps
586ad7e66e fix: 移除项目列表中的参赛人数限制列
- 从competition/index.vue移除参赛人数限制列
- 从competition/create.vue移除参赛人数限制列
- 该功能已不再使用

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 10:59:51 +08:00
DevOps
8656aa5abc feat(project): add venue selection for project management
- Add venueId field to project form
- Load venue list when competition is selected
- Allow assigning projects to specific venues

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-05 15:59:09 +08:00
DevOps
c1f5acb644 Revert "feat(judgeInvite): add project assignment editing feature"
This reverts commit ecd569337d.
2026-01-05 15:20:01 +08:00
DevOps
ecd569337d feat(judgeInvite): add project assignment editing feature
- Add edit projects button in judge invite list
- Add edit projects dialog with project multi-select
- Add updateInviteProjects API method
- Fix: load project list before opening edit dialog

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-05 15:08:04 +08:00
DevOps
be8b887a1c feat(judgeInvite): 增加场地过滤条件 2025-12-31 16:20:50 +08:00
DevOps
6a5b220f6e fix(project): 将最大参赛人数改为单位容纳人数,用于编排分组计算 2025-12-30 16:55:20 +08:00
DevOps
8f14a165e5 fix(project): 移除项目编辑/新增页面的报名时间字段 2025-12-30 16:48:56 +08:00
DevOps
21fc12b18d feat(schedule): 前端支持动态时间段配置
- 添加 getScheduleConfig API 调用
- 更新 generateTimeSlots 从后端获取时间配置
- 添加 loadScheduleConfig 和 formatTimeForDisplay 方法
- 时间段不再硬编码,从 ScheduleConfig 动态获取
2025-12-30 10:51:28 +08:00
DevOps
21274e9639 fix(schedule): 修复markPlayerAsException重复定义导致的事件绑定问题 2025-12-29 17:28:03 +08:00
DevOps
564374250b fix(api): 修复updateCheckInStatus API路径 2025-12-29 15:42:43 +08:00
DevOps
77c2c51d8a fix(schedule): 修复展开选手详情中的异常状态持久化API调用
- markPlayerAsException方法添加updateCheckInStatus API调用
- removePlayerException方法添加updateCheckInStatus API调用
- 添加编排完成状态检查
2025-12-29 15:33:19 +08:00
DevOps
578b94aa39 feat: add estimated duration field and exception status persistence
- Add estimatedDuration field to project form with validation
- Add estimatedDuration column to project table
- Add updateCheckInStatus API for exception status persistence
- Call backend API when marking/removing exception status
2025-12-29 15:08:29 +08:00
DevOps
a9b82d7aae docs: 更新README,简化内容并更新域名 2025-12-29 14:21:48 +08:00
DevOps
f412a9c759 fix: 项目管理表格显示所属赛事名称 2025-12-29 13:24:14 +08:00
Developer
0b9f107b2a fix: 修复项目编辑时报名费字段映射问题 2025-12-29 11:48:03 +08:00
DevOps
5bbe374ebf fix: 修复项目管理页面编辑项目API调用错误
- 将updateProject改为submitProject,后端submit接口同时支持新增和修改
2025-12-29 11:21:55 +08:00
DevOps
39ff98e6c0 fix: 修复项目管理页面新增项目API调用错误
- 将addProject改为submitProject,匹配后端API端点/martial/project/submit
2025-12-29 11:16:13 +08:00
DevOps
f1c2501afc 裁判邀请表格添加负责场地列
- 显示裁判负责的场地名称
- 总裁显示"全部场地"

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-29 10:32:46 +08:00
DevOps
657c4210a4 添加总裁(裁判长)角色支持到裁判邀请和评委管理页面
- judgeInvite/index.vue: 添加总裁选项和显示
- referee/index.vue: 添加总裁筛选和显示

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 20:06:08 +08:00
DevOps
a98b18275f feat: 添加项目类型筛选和显示功能
- 添加项目类型下拉筛选框(套路/散打/器械/对练)
- 在表格中添加项目类型列
- 支持项目类型查询参数
2025-12-28 19:03:48 +08:00
DevOps
6267d87b18 fix: 修复项目管理页面筛选功能
- 将分组类别从下拉框改为文本输入框,支持模糊搜索
- 移除项目类型筛选(后端不支持该字段)
- 修复handleReset中的缩进问题
2025-12-28 17:28:18 +08:00
DevOps
67ffd4fc23 fix: 移除赛事筛选的 status 限制,显示所有赛事
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 17:12:18 +08:00
DevOps
6befd3644a fix: 修复项目管理页面数据显示问题
- 修复 API 响应数据解析 (res.data.data.records)
- 移除后端不支持的 eventType 参数
- 修复报名费字段映射 (registrationFee -> price)
- 修复分组类别显示为文本而非数字

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 17:09:38 +08:00
DevOps
a6768c394a fix: change participantType to type to match backend field name
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 16:37:31 +08:00
DevOps
ac7587ef7e refactor: 裁判角色名称修改 - 裁判长→主裁判, 普通裁判→裁判员
- 修改referee/index.vue中的UI显示文字
- 修改judgeInvite/index.vue中的UI显示文字
- 更新referee.js中的注释

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:37:29 +08:00
DevOps
4f1d0b5888 feat(judgeInvite): 移除项目分配功能,裁判默认负责整个场地所有项目
- 移除导入裁判时的项目选择下拉框
- 移除项目必选验证
- 不传projects参数,后端自动获取场地所有项目

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 16:35:27 +08:00
DevOps
cc6fabe576 feat: 裁判邀请导入功能添加场地和项目选择
- 导入对话框添加场地下拉选择
- 导入对话框添加项目多选
- 调用API时传递venueId和projects参数

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 15:45:10 +08:00
04cd85cbe3 fix bugs 2025-12-26 13:15:26 +08:00
c12fb79444 Merge branch 'main' of git.waypeak.work:martial/martial-web 2025-12-26 11:06:52 +08:00
1744adcf92 fix bugs 2025-12-26 11:06:38 +08:00
DevOps
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
DevOps
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
DevOps
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
DevOps
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
DevOps
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
DevOps
5e75688e13 feat: 赛程编排表格按队伍分组显示选手
Some checks are pending
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
DevOps
226d92f725 fix: 修复P2和P3问题
Some checks failed
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
DevOps
3d314fe84f chore: 更新package-lock.json
All checks were successful
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
27 changed files with 5257 additions and 731 deletions

298
README.md
View File

@@ -1,270 +1,72 @@
# 武术赛事通 - 前端项目 # 武术赛事管理系统 - 管理后台
基于 Vue 3 + Vite + Element Plus 构建 基于 Vue 3 + Vite + Element Plus 构建的武术比赛管理后台。
## 🌐 在线访问 ## 在线访问
- **生产环境**: https://martial.johnsion.club | 服务 | 地址 |
- **后端 API**: https://martial-api.johnsion.club |------|------|
- **API 文档**: https://martial-doc.johnsion.club | 管理后台 | https://martial-admin.aitisai.com |
| 后端 API | https://martial-api.aitisai.com |
## 📦 技术栈 ## 技术栈
- **框架**: Vue 3.4 (Composition API) - **框架**: Vue 3.4 (Composition API)
- **构建工具**: Vite 5 - **构建工具**: Vite 5
- **UI 组件**: Element Plus - **UI 组件**: Element Plus + Avue
- **表单/表格**: Avue
- **HTTP 库**: Axios
- **路由**: Vue Router 4
- **状态管理**: Vuex 4 - **状态管理**: Vuex 4
- **样式**: Sass/SCSS - **路由**: Vue Router 4
## 📁 项目结构 ## 快速开始
```bash
# 安装依赖
npm install
# 开发环境
npm run dev
# 构建生产版本
npm run build
```
访问 http://localhost:8083
## 项目结构
``` ```
martial-web/ martial-web/
├── src/ ├── src/
│ ├── main.js # 应用入口 │ ├── views/martial/ # 武术业务页面
│ ├── App.vue # 根组件 │ ├── competition/ # 赛事管理
│ ├── router/ # 路由配置 │ ├── project/ # 项目管理
│ ├── store/ # Vuex 状态管理 │ ├── participant/ # 参赛选手
│ ├── views/ # 页面组件 │ ├── judgeInvite/ # 裁判邀请
│ ├── components/ # 通用组件 │ ├── score/ # 评分管理
├── api/ # API 接口 │ └── ...
── utils/ # 工具函数 ── api/ # API 接口
├── public/ # 静态资源 │ ├── router/ # 路由配置
├── .env.development # 开发环境配置 │ └── store/ # 状态管理
├── .env.production # 生产环境配置 ├── .env.development # 开发环境配置
├── vite.config.js # Vite 配置 ├── .env.production # 生产环境配置
── nginx.conf # Nginx 配置(生产环境容器使用) ── vite.config.js # Vite 配置
├── Dockerfile # 完整构建 Dockerfile
├── Dockerfile.deploy # 部署 Dockerfile
└── .drone.yml # Drone CI/CD 配置
``` ```
## 🚀 本地开发 ## Docker 部署
### 环境要求
- Node.js >= 18
- npm >= 9
### 安装依赖
```bash ```bash
npm install docker build -t martial-web .
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

46
package-lock.json generated
View File

@@ -851,7 +851,6 @@
"version": "4.17.12", "version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"peer": true,
"dependencies": { "dependencies": {
"@types/lodash": "*" "@types/lodash": "*"
} }
@@ -874,7 +873,6 @@
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz", "resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==", "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
"peer": true,
"dependencies": { "dependencies": {
"@transloadit/prettier-bytes": "0.0.7", "@transloadit/prettier-bytes": "0.0.7",
"@uppy/store-default": "^2.1.1", "@uppy/store-default": "^2.1.1",
@@ -903,7 +901,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==", "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
"peer": true,
"dependencies": { "dependencies": {
"@uppy/companion-client": "^2.2.2", "@uppy/companion-client": "^2.2.2",
"@uppy/utils": "^4.1.2", "@uppy/utils": "^4.1.2",
@@ -1058,7 +1055,6 @@
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz", "resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==", "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
"peer": true,
"dependencies": { "dependencies": {
"is-url": "^1.2.4" "is-url": "^1.2.4"
}, },
@@ -1089,7 +1085,6 @@
"version": "1.1.19", "version": "1.1.19",
"resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz", "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==", "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
"peer": true,
"dependencies": { "dependencies": {
"@types/event-emitter": "^0.3.3", "@types/event-emitter": "^0.3.3",
"event-emitter": "^0.3.5", "event-emitter": "^0.3.5",
@@ -1119,7 +1114,6 @@
"version": "5.1.23", "version": "5.1.23",
"resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz", "resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==", "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
"peer": true,
"dependencies": { "dependencies": {
"@uppy/core": "^2.1.1", "@uppy/core": "^2.1.1",
"@uppy/xhr-upload": "^2.0.3", "@uppy/xhr-upload": "^2.0.3",
@@ -1476,7 +1470,6 @@
"version": "11.13.1", "version": "11.13.1",
"resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-11.13.1.tgz", "resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-11.13.1.tgz",
"integrity": "sha512-6kO0rBN6aBIQiMELfv1oX2Ohes/brlIPuOVZUYAioeWM0EyuazhAXgHeq8iKFt29daU9NGRr4n78esGx8QjtjQ==", "integrity": "sha512-6kO0rBN6aBIQiMELfv1oX2Ohes/brlIPuOVZUYAioeWM0EyuazhAXgHeq8iKFt29daU9NGRr4n78esGx8QjtjQ==",
"peer": true,
"dependencies": { "dependencies": {
"@bpmn-io/diagram-js-ui": "^0.2.2", "@bpmn-io/diagram-js-ui": "^0.2.2",
"clsx": "^1.2.1", "clsx": "^1.2.1",
@@ -1514,7 +1507,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==", "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
"peer": true,
"dependencies": { "dependencies": {
"ssr-window": "^3.0.0-alpha.1" "ssr-window": "^3.0.0-alpha.1"
} }
@@ -1540,7 +1532,6 @@
"version": "2.7.3", "version": "2.7.3",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz",
"integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==", "integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==",
"peer": true,
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.1", "@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
@@ -1866,8 +1857,7 @@
"node_modules/is-hotkey": { "node_modules/is-hotkey": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz", "resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==", "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
"peer": true
}, },
"node_modules/is-number": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
@@ -1942,14 +1932,12 @@
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"peer": true
}, },
"node_modules/lodash-es": { "node_modules/lodash-es": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
"peer": true
}, },
"node_modules/lodash-unified": { "node_modules/lodash-unified": {
"version": "1.0.3", "version": "1.0.3",
@@ -1964,44 +1952,37 @@
"node_modules/lodash.camelcase": { "node_modules/lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
"peer": true
}, },
"node_modules/lodash.clonedeep": { "node_modules/lodash.clonedeep": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
"peer": true
}, },
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
"peer": true
}, },
"node_modules/lodash.foreach": { "node_modules/lodash.foreach": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz", "resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="
"peer": true
}, },
"node_modules/lodash.isequal": { "node_modules/lodash.isequal": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
"peer": true
}, },
"node_modules/lodash.throttle": { "node_modules/lodash.throttle": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
"peer": true
}, },
"node_modules/lodash.toarray": { "node_modules/lodash.toarray": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz", "resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==", "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw=="
"peer": true
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.10", "version": "0.30.10",
@@ -2116,7 +2097,6 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@@ -2308,7 +2288,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
"integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.5" "@types/estree": "1.0.5"
}, },
@@ -2367,7 +2346,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz",
"integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==", "integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0", "immutable": "^4.0.0",
@@ -2403,7 +2381,6 @@
"version": "0.72.8", "version": "0.72.8",
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz", "resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==", "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
"peer": true,
"dependencies": { "dependencies": {
"immer": "^9.0.6", "immer": "^9.0.6",
"is-plain-object": "^5.0.0", "is-plain-object": "^5.0.0",
@@ -2425,7 +2402,6 @@
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz", "resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz",
"integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==", "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==",
"peer": true,
"engines": { "engines": {
"node": ">=8.3.0" "node": ">=8.3.0"
} }
@@ -2623,7 +2599,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.20.1", "esbuild": "^0.20.1",
"postcss": "^8.4.38", "postcss": "^8.4.38",
@@ -2791,7 +2766,6 @@
"version": "3.4.27", "version": "3.4.27",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz",
"integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.4.27", "@vue/compiler-dom": "3.4.27",
"@vue/compiler-sfc": "3.4.27", "@vue/compiler-sfc": "3.4.27",

View File

@@ -183,3 +183,26 @@ export const saveDispatch = (data) => {
data data
}) })
} }
/**
* 导出赛程表
* @param {Number} competitionId - 赛事ID
*/
export const exportSchedule = (competitionId) => {
return request({
url: '/martial/export/schedule',
method: 'get',
params: { competitionId },
responseType: 'blob'
})
}
// Export schedule template 2 (competition time format)
export const exportScheduleTemplate2 = (competitionId, venueId, venueName, timeSlot) => {
return request({
url: '/martial/export/schedule2',
method: 'get',
params: { competitionId, venueId, venueName, timeSlot },
responseType: 'blob'
})
}

View File

@@ -0,0 +1,136 @@
import request from '@/axios';
// ==================== 赛事附件管理接口 ====================
/**
* 获取附件详情
* @param {Number} id - 附件ID
*/
export const getAttachmentDetail = (id) => {
return request({
url: '/api/martial/competition/attachment/detail',
method: 'get',
params: { id }
})
}
/**
* 附件列表查询(分页)
* @param {Number} current - 当前页
* @param {Number} size - 每页条数
* @param {Object} params - 查询参数
*/
export const getAttachmentList = (current, size, params) => {
return request({
url: '/api/martial/competition/attachment/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 根据赛事ID和类型获取附件列表
* @param {Number} competitionId - 赛事ID
* @param {String} attachmentType - 附件类型info-赛事发布, rules-赛事规程, schedule-活动日程, results-成绩, medals-奖牌榜, photos-图片直播
*/
export const getAttachmentsByType = (competitionId, attachmentType) => {
return request({
url: '/api/martial/competition/attachment/getByType',
method: 'get',
params: { competitionId, attachmentType }
})
}
/**
* 根据赛事ID获取所有附件
* @param {Number} competitionId - 赛事ID
*/
export const getAttachmentsByCompetition = (competitionId) => {
return request({
url: '/api/martial/competition/attachment/getByCompetition',
method: 'get',
params: { competitionId }
})
}
/**
* 新增或修改附件
* @param {Object} data - 附件数据
* @param {Number} data.id - ID修改时必传
* @param {Number} data.competitionId - 赛事ID
* @param {String} data.attachmentType - 附件类型
* @param {String} data.fileName - 文件名称
* @param {String} data.fileUrl - 文件URL
* @param {Number} data.fileSize - 文件大小(字节)
* @param {String} data.fileType - 文件类型(扩展名)
* @param {Number} data.orderNum - 排序序号
* @param {Number} data.status - 状态1-启用 0-禁用)
*/
export const submitAttachment = (data) => {
return request({
url: '/api/martial/competition/attachment/submit',
method: 'post',
data
})
}
/**
* 批量保存附件
* @param {Array} attachments - 附件列表
*/
export const batchSubmitAttachments = (attachments) => {
return request({
url: '/api/martial/competition/attachment/batchSubmit',
method: 'post',
data: attachments
})
}
/**
* 删除附件
* @param {String} ids - 附件ID,多个用逗号分隔
*/
export const removeAttachment = (ids) => {
return request({
url: '/api/martial/competition/attachment/remove',
method: 'post',
params: { ids }
})
}
/**
* 删除赛事的指定类型附件
* @param {Number} competitionId - 赛事ID
* @param {String} attachmentType - 附件类型
*/
export const removeAttachmentByType = (competitionId, attachmentType) => {
return request({
url: '/api/martial/competition/attachment/removeByType',
method: 'post',
params: { competitionId, attachmentType }
})
}
// 附件类型常量
export const ATTACHMENT_TYPES = {
INFO: 'info', // 赛事发布
RULES: 'rules', // 赛事规程
SCHEDULE: 'schedule', // 活动日程
RESULTS: 'results', // 成绩
MEDALS: 'medals', // 奖牌榜
PHOTOS: 'photos' // 图片直播
}
// 附件类型标签映射
export const ATTACHMENT_TYPE_LABELS = {
info: '赛事发布',
rules: '赛事规程',
schedule: '活动日程',
results: '成绩',
medals: '奖牌榜',
photos: '图片直播'
}

View File

@@ -141,3 +141,16 @@ 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 }
})
}

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

View File

@@ -194,3 +194,27 @@ 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'
})
}

View File

@@ -98,6 +98,10 @@ axios.interceptors.request.use(
axios.interceptors.response.use( axios.interceptors.response.use(
res => { res => {
NProgress.done(); NProgress.done();
// 如果是 blob 类型响应(文件下载),直接返回
if (res.config.responseType === 'blob') {
return res;
}
const status = res.data.code || res.status; const status = res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || []; const statusWhiteList = website.statusWhiteList || [];
const message = res.data.msg || res.data.error_description || '系统错误'; const message = res.data.msg || res.data.error_description || '系统错误';

View File

@@ -48,7 +48,7 @@ export default [
redirect: '/martial/order/list', redirect: '/martial/order/list',
children: [ children: [
{ {
path: 'competition/list', path: 'competition/index',
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,
}, },

View File

@@ -188,7 +188,7 @@
</div> </div>
<!-- 活动日程 --> <!-- 活动日程 -->
<div class="form-section"> <!-- <div class="form-section">
<div class="section-title"> <div class="section-title">
<i class="el-icon-date"></i> <i class="el-icon-date"></i>
活动日程 活动日程
@@ -290,7 +290,7 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div> -->
<!-- 项目列表 --> <!-- 项目列表 -->
<div class="form-section"> <div class="form-section">
@@ -361,23 +361,6 @@
</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="项目说明"

View File

@@ -370,8 +370,134 @@
</el-form-item> </el-form-item>
</div> </div>
<!-- 活动日程 --> <!-- 附件管理 -->
<div class="form-section"> <div class="form-section">
<div class="section-title">
<i class="el-icon-folder-opened"></i>
附件管理
</div>
<!-- 附件类型选项卡 -->
<el-tabs v-model="activeAttachmentTab" type="card">
<el-tab-pane
v-for="tab in attachmentTabs"
:key="tab.type"
:label="tab.label"
:name="tab.type"
>
<div class="attachment-section">
<!-- 上传按钮 -->
<div class="attachment-upload" v-if="currentView !== 'view'">
<el-button
type="primary"
icon="el-icon-upload"
size="small"
@click="handleOpenAttachmentUpload(tab.type)"
>
上传{{ tab.label }}附件
</el-button>
<span class="upload-tip">支持 PDFWordExcel图片等格式单个文件不超过 50MB</span>
</div>
<!-- 附件列表 -->
<el-table
:data="formData.attachments[tab.type] || []"
border
style="width: 100%; margin-top: 15px;"
v-if="(formData.attachments[tab.type] || []).length > 0"
>
<el-table-column
label="文件名"
min-width="200"
show-overflow-tooltip
>
<template #default="scope">
<div class="file-name-cell">
<i :class="getFileIcon(scope.row.fileType)" class="file-icon"></i>
<span>{{ scope.row.fileName }}</span>
</div>
</template>
</el-table-column>
<el-table-column
label="文件大小"
width="120"
align="center"
>
<template #default="scope">
{{ formatFileSize(scope.row.fileSize) }}
</template>
</el-table-column>
<el-table-column
label="上传时间"
width="180"
align="center"
>
<template #default="scope">
{{ scope.row.createTime || '-' }}
</template>
</el-table-column>
<el-table-column
label="排序"
width="100"
align="center"
>
<template #default="scope">
<el-input-number
v-if="currentView !== 'view'"
v-model="scope.row.orderNum"
:min="0"
:max="999"
size="small"
style="width: 80px"
controls-position="right"
/>
<span v-else>{{ scope.row.orderNum }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="150"
align="center"
fixed="right"
>
<template #default="scope">
<el-button
type="primary"
link
size="small"
@click="handlePreviewAttachment(scope.row)"
>
预览
</el-button>
<el-button
v-if="currentView !== 'view'"
type="danger"
link
size="small"
@click="handleDeleteAttachment(tab.type, scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 空状态 -->
<div class="empty-attachment" v-else>
<i class="el-icon-folder-opened"></i>
<p>暂无{{ tab.label }}附件</p>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 活动日程 -->
<!-- <div class="form-section">
<div class="section-title"> <div class="section-title">
<i class="el-icon-date"></i> <i class="el-icon-date"></i>
活动日程 活动日程
@@ -512,7 +638,7 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div> -->
<!-- 项目列表 --> <!-- 项目列表 -->
<div class="form-section"> <div class="form-section">
@@ -583,21 +709,24 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
label="参赛人数限制" label="报名费用(元)"
width="130" width="130"
align="center" align="center"
> >
<template #default="scope"> <template #default="scope">
<el-input-number <el-input-number
v-if="currentView !== 'view'" v-if="currentView !== 'view'"
v-model="scope.row.maxParticipants" v-model="scope.row.price"
:min="1" :min="0"
:max="9999" :precision="2"
:step="10"
size="small" size="small"
style="width: 100%" style="width: 100%"
placeholder="0.00"
/> />
<span v-else>{{ scope.row.maxParticipants || '不限' }}</span> <span v-else style="color: #f56c6c">¥{{ scope.row.price || 0 }}</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -781,6 +910,22 @@
</el-form> </el-form>
</el-card> </el-card>
</div> </div>
<!-- 附件上传对话框 -->
<el-dialog
title="上传附件"
v-model="attachmentUploadDialogVisible"
width="555px"
append-to-body
:close-on-click-modal="false"
>
<avue-form
ref="attachmentUploadForm"
:option="attachmentUploadOption"
v-model="attachmentUploadForm"
:upload-after="attachmentUploadAfter"
/>
</el-dialog>
</div> </div>
</template> </template>
@@ -806,6 +951,15 @@ import {
removeVenue, removeVenue,
getVenuesByCompetition getVenuesByCompetition
} from '@/api/martial/venue' } from '@/api/martial/venue'
import {
getAttachmentsByCompetition,
submitAttachment,
removeAttachment,
batchSubmitAttachments,
ATTACHMENT_TYPES,
ATTACHMENT_TYPE_LABELS
} from '@/api/martial/attachment'
import { getToken } from '@/utils/auth'
export default { export default {
name: 'CompetitionManagement', name: 'CompetitionManagement',
@@ -820,6 +974,40 @@ export default {
size: 10, size: 10,
total: 0 total: 0
}, },
// 附件相关数据
activeAttachmentTab: 'info',
attachmentTabs: [
{ type: 'info', label: '赛事发布' },
{ type: 'rules', label: '赛事规程' },
{ type: 'schedule', label: '活动日程' },
{ type: 'results', label: '成绩' },
{ type: 'medals', label: '奖牌榜' },
{ type: 'photos', label: '图片直播' }
],
attachmentUploadDialogVisible: false,
currentAttachmentType: '',
attachmentUploadForm: {},
attachmentUploadOption: {
submitBtn: false,
emptyBtn: false,
column: [
{
label: '附件上传',
prop: 'attachmentFile',
type: 'upload',
drag: true,
loadText: '文件上传中,请稍等',
span: 24,
accept: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.zip,.rar',
tip: '支持 PDF、Word、Excel、PPT、图片、压缩包等格式单个文件不超过 50MB',
propsHttp: {
res: 'data',
},
action: '/blade-resource/oss/endpoint/put-file'
}
]
},
formData: { formData: {
competitionName: '', competitionName: '',
competitionCode: '', // 比赛编码 competitionCode: '', // 比赛编码
@@ -840,7 +1028,15 @@ export default {
awards: '', awards: '',
schedule: [], schedule: [],
projects: [], projects: [],
venues: [] venues: [],
attachments: {
info: [],
rules: [],
schedule: [],
results: [],
medals: [],
photos: []
}
}, },
formRules: { formRules: {
competitionName: [ competitionName: [
@@ -988,10 +1184,11 @@ export default {
try { try {
this.formData = this.formatBackendData(detailData); this.formData = this.formatBackendData(detailData);
console.log('格式化后的表单数据:', this.formData); console.log('格式化后的表单数据:', this.formData);
// 加载关联数据:活动日程、项目列表、场地配置 // 加载关联数据:活动日程、项目列表、场地配置、附件
this.loadActivitySchedules(); this.loadActivitySchedules();
this.loadProjects(); this.loadProjects();
this.loadVenues(); this.loadVenues();
this.loadAttachments();
} catch (error) { } catch (error) {
console.error('格式化数据时出错:', error); console.error('格式化数据时出错:', error);
this.$message.error('数据格式化失败: ' + error.message); this.$message.error('数据格式化失败: ' + error.message);
@@ -1138,7 +1335,8 @@ 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);
@@ -1154,6 +1352,213 @@ export default {
}); });
}, },
// 加载附件列表
loadAttachments() {
if (!this.competitionId) {
console.warn('loadAttachments: competitionId 为空,跳过加载');
return;
}
console.log('开始加载附件列表competitionId:', this.competitionId);
getAttachmentsByCompetition(this.competitionId)
.then(res => {
console.log('附件列表返回数据:', res);
const responseData = res.data?.data;
// 初始化附件对象
const attachments = {
info: [],
rules: [],
schedule: [],
results: [],
medals: [],
photos: []
};
if (responseData && Array.isArray(responseData)) {
// 按类型分组
responseData.forEach(item => {
const type = item.attachmentType;
if (attachments[type]) {
attachments[type].push({
id: item.id,
fileName: item.fileName,
fileUrl: item.fileUrl,
fileSize: item.fileSize,
fileType: item.fileType,
orderNum: item.orderNum || 0,
createTime: item.createTime
});
}
});
console.log('✅ 加载的附件列表:', attachments);
} else {
console.log('⚠️ 附件列表为空');
}
this.formData.attachments = attachments;
})
.catch(err => {
console.error('❌ 加载附件列表失败:', err);
// 不显示错误消息,因为可能是新建赛事还没有附件
});
},
// 打开附件上传对话框
handleOpenAttachmentUpload(type) {
this.currentAttachmentType = type;
this.attachmentUploadForm = {};
this.attachmentUploadOption.column[0].headers = { "Blade-Auth": "bearer " + getToken() };
this.attachmentUploadDialogVisible = true;
},
// 附件上传成功回调
attachmentUploadAfter(res, done, loading, column) {
console.log('附件上传响应:', res);
if (res && (res.link || res.url)) {
const fileUrl = res.link || res.url;
const fileName = res.originalName || res.name || this.getFileNameFromUrl(fileUrl);
const fileType = this.getFileExtension(fileName);
const fileSize = res.size || 0;
// 添加到对应类型的附件列表
if (!this.formData.attachments[this.currentAttachmentType]) {
this.formData.attachments[this.currentAttachmentType] = [];
}
this.formData.attachments[this.currentAttachmentType].push({
fileName: fileName,
fileUrl: fileUrl,
fileSize: fileSize,
fileType: fileType,
orderNum: this.formData.attachments[this.currentAttachmentType].length,
isNew: true // 标记为新上传的附件
});
this.$message.success('附件上传成功');
this.attachmentUploadDialogVisible = false;
} else {
this.$message.error('上传失败,未获取到文件地址');
}
done();
},
// 预览附件
handlePreviewAttachment(attachment) {
if (!attachment.fileUrl) {
this.$message.warning('文件地址不存在');
return;
}
window.open(attachment.fileUrl, '_blank');
},
// 删除附件
handleDeleteAttachment(type, index) {
this.$confirm('确定要删除该附件吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const attachment = this.formData.attachments[type][index];
// 如果是已保存的附件,需要调用后端删除
if (attachment.id) {
removeAttachment(attachment.id.toString())
.then(() => {
this.formData.attachments[type].splice(index, 1);
this.$message.success('删除成功');
})
.catch(err => {
console.error('删除附件失败:', err);
this.$message.error('删除失败');
});
} else {
// 新上传的附件直接从列表中移除
this.formData.attachments[type].splice(index, 1);
this.$message.success('删除成功');
}
}).catch(() => {});
},
// 获取文件图标
getFileIcon(fileType) {
const iconMap = {
'pdf': 'el-icon-document',
'doc': 'el-icon-document',
'docx': 'el-icon-document',
'xls': 'el-icon-document',
'xlsx': 'el-icon-document',
'ppt': 'el-icon-document',
'pptx': 'el-icon-document',
'jpg': 'el-icon-picture',
'jpeg': 'el-icon-picture',
'png': 'el-icon-picture',
'gif': 'el-icon-picture',
'zip': 'el-icon-folder',
'rar': 'el-icon-folder'
};
return iconMap[fileType?.toLowerCase()] || 'el-icon-document';
},
// 格式化文件大小
formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 B';
if (typeof bytes === 'string') return bytes;
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
},
// 从URL获取文件名
getFileNameFromUrl(url) {
if (!url) return 'unknown';
const parts = url.split('/');
return parts[parts.length - 1] || 'unknown';
},
// 获取文件扩展名
getFileExtension(fileName) {
if (!fileName) return '';
const parts = fileName.split('.');
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
},
// 保存附件
async saveAttachments(competitionId) {
const allAttachments = [];
// 收集所有需要保存的附件
for (const type of Object.keys(this.formData.attachments)) {
const attachments = this.formData.attachments[type] || [];
for (const attachment of attachments) {
// 只保存新上传的附件或已修改的附件
if (attachment.isNew || attachment.isModified) {
allAttachments.push({
id: attachment.id || null,
competitionId: competitionId,
attachmentType: type,
fileName: attachment.fileName,
fileUrl: attachment.fileUrl,
fileSize: attachment.fileSize,
fileType: attachment.fileType,
orderNum: attachment.orderNum || 0,
status: 1
});
}
}
}
if (allAttachments.length === 0) {
return Promise.resolve();
}
console.log('准备保存的附件:', allAttachments);
return batchSubmitAttachments(allAttachments);
},
getStatusText(status) { getStatusText(status) {
const statusMap = { const statusMap = {
1: '未开始', 1: '未开始',
@@ -1176,7 +1581,7 @@ export default {
handleCreate() { handleCreate() {
this.$router.push({ this.$router.push({
path: '/martial/competition/list', path: '/martial/competition/index',
query: { mode: 'create' } query: { mode: 'create' }
}); });
}, },
@@ -1184,21 +1589,21 @@ export default {
handleView(row) { handleView(row) {
console.log(row) console.log(row)
this.$router.push({ this.$router.push({
path: '/martial/competition/list', path: '/martial/competition/index',
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/list', path: '/martial/competition/index',
query: { mode: 'edit', id: row.id } query: { mode: 'edit', id: row.id }
}); });
}, },
switchToEdit() { switchToEdit() {
this.$router.push({ this.$router.push({
path: '/martial/competition/list', path: '/martial/competition/index',
query: { mode: 'edit', id: this.competitionId } query: { mode: 'edit', id: this.competitionId }
}); });
}, },
@@ -1247,6 +1652,7 @@ export default {
projectCode: '', projectCode: '',
category: '', category: '',
maxParticipants: null, maxParticipants: null,
price: 0,
description: '' description: ''
}); });
}, },
@@ -1311,6 +1717,9 @@ export default {
savePromises.push(this.saveVenues(savedCompetitionId)); savePromises.push(this.saveVenues(savedCompetitionId));
} }
// 4. 保存附件
savePromises.push(this.saveAttachments(savedCompetitionId));
// 等待所有保存操作完成 // 等待所有保存操作完成
if (savePromises.length > 0) { if (savePromises.length > 0) {
Promise.all(savePromises) Promise.all(savePromises)
@@ -1455,7 +1864,8 @@ 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说明是编辑已有的场地
@@ -1472,7 +1882,7 @@ export default {
backToList() { backToList() {
this.$router.push({ this.$router.push({
path: '/martial/competition/list' path: '/martial/competition/index'
}); });
// 路由跳转后,在 initPage 中会自动加载列表 // 路由跳转后,在 initPage 中会自动加载列表
}, },
@@ -1498,7 +1908,15 @@ export default {
awards: '', awards: '',
schedule: [], schedule: [],
projects: [], projects: [],
venues: [] venues: [],
attachments: {
info: [],
rules: [],
schedule: [],
results: [],
medals: [],
photos: []
}
}; };
}, },
@@ -1789,4 +2207,55 @@ export default {
width: 100%; width: 100%;
} }
} }
// 附件管理样式
.attachment-section {
padding: 10px 0;
}
.attachment-upload {
display: flex;
align-items: center;
gap: 15px;
.upload-tip {
font-size: 12px;
color: #909399;
}
}
.file-name-cell {
display: flex;
align-items: center;
gap: 8px;
.file-icon {
font-size: 18px;
color: #409eff;
}
}
.empty-attachment {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #909399;
i {
font-size: 48px;
margin-bottom: 10px;
opacity: 0.5;
}
p {
margin: 0;
font-size: 14px;
}
}
:deep(.el-tabs__content) {
padding: 10px 0;
}
</style> </style>

View File

@@ -284,7 +284,7 @@
</div> </div>
<!-- 活动日程 --> <!-- 活动日程 -->
<div class="form-section"> <!-- <div class="form-section">
<div class="section-title"> <div class="section-title">
<i class="el-icon-date"></i> <i class="el-icon-date"></i>
活动日程 活动日程
@@ -386,7 +386,7 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div> -->
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">

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="item.id" :value="String(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="item.id" :value="String(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="deductionPoints" prop="deductionPoint"
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.deductionPoints }} -{{ row.deductionPoint }}
</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="item.id" :value="String(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="item.id" :value="String(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="deductionPoints"> <el-form-item label="扣分值(分)" prop="deductionPoint">
<el-input-number <el-input-number
v-model="form.deductionPoints" v-model="form.deductionPoint"
: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="item.id" :value="String(item.id)"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -389,6 +389,7 @@ const queryParams = reactive({
size: 10, size: 10,
competitionId: null, competitionId: null,
projectId: null, projectId: null,
itemName: '',
}) })
// 表单数据 // 表单数据
@@ -396,7 +397,8 @@ const form = reactive({
id: null, id: null,
competitionId: null, competitionId: null,
projectId: null, projectId: null,
deductionPoints: 0.5, itemName: '',
deductionPoint: 0.5,
sortOrder: 0, sortOrder: 0,
description: '' description: ''
}) })
@@ -421,7 +423,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' }
], ],
deductionPoints: [ deductionPoint: [
{ required: true, message: '请输入扣分值', trigger: 'blur' } { required: true, message: '请输入扣分值', trigger: 'blur' }
], ],
sortOrder: [ sortOrder: [
@@ -504,10 +506,17 @@ 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,
queryParams params
) )
// 根据axios响应拦截器的处理数据在 res.data.data 中 // 根据axios响应拦截器的处理数据在 res.data.data 中
const data = res.data?.data || {} const data = res.data?.data || {}
@@ -535,7 +544,7 @@ const handleReset = () => {
size: 10, size: 10,
competitionId: competitionId, competitionId: competitionId,
projectId: null, projectId: null,
itemName: '' itemName: '',
}) })
if (competitionId) { if (competitionId) {
fetchData() fetchData()
@@ -555,11 +564,22 @@ const handleAdd = () => {
} }
// 编辑 // 编辑
const handleEdit = (row) => { const handleEdit = async (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
} }
@@ -670,7 +690,7 @@ const resetForm = () => {
competitionId: null, competitionId: null,
projectId: null, projectId: null,
itemName: '', itemName: '',
deductionPoints: 0.5, deductionPoint: 0.5,
sortOrder: 0, sortOrder: 0,
description: '' description: ''
}) })

View File

@@ -143,10 +143,15 @@
</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' : 'primary'" size="small"> <el-tag :type="row.refereeType === 1 ? 'danger' : (row.refereeType === 3 ? 'warning' : 'primary')" size="small">
{{ row.refereeType === 1 ? '主裁判' : '普通裁判' }} {{ row.refereeType === 1 ? '主裁判' : (row.refereeType === 3 ? '总裁' : '裁判员') }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -185,6 +190,26 @@
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="姓名">
@@ -211,7 +236,8 @@
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>
@@ -241,8 +267,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' : 'primary'" size="small"> <el-tag :type="row.refereeType === 1 ? 'danger' : (row.refereeType === 3 ? 'warning' : 'primary')" size="small">
{{ row.refereeType === 1 ? '主裁判' : '普通裁判' }} {{ row.refereeType === 1 ? '主裁判' : (row.refereeType === 3 ? '总裁' : '裁判员') }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -303,6 +329,8 @@ 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'
@@ -316,6 +344,17 @@ 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)
@@ -343,7 +382,8 @@ const queryParams = reactive({
competitionId: null, competitionId: null,
judgeName: '', judgeName: '',
judgeLevel: '', judgeLevel: '',
inviteStatus: '' inviteStatus: '',
venueId: null
}) })
// 加载赛事列表 // 加载赛事列表
@@ -379,11 +419,31 @@ const loadCompetitionList = async () => {
} }
// 赛事切换 // 赛事切换
const handleCompetitionChange = (competitionId) => { const handleCompetitionChange = async (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
@@ -434,7 +494,8 @@ const handleReset = () => {
size: 10, size: 10,
judgeName: '', judgeName: '',
judgeLevel: '', judgeLevel: '',
inviteStatus: '' inviteStatus: '',
venueId: null
}) })
fetchData() fetchData()
} }
@@ -454,12 +515,48 @@ 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
@@ -523,6 +620,13 @@ 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} 位裁判生成邀请码吗?`,
@@ -541,6 +645,8 @@ 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
}) })
@@ -625,7 +731,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' : 'judge', // 根据评委类型设置角色 role: row.refereeType === 1 ? 'chief_judge' : (row.refereeType === 3 ? 'general_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
@@ -821,8 +927,12 @@ const fallbackCopyToClipboard = (text, label) => {
} }
// 挂载 // 挂载
onMounted(() => { onMounted(async () => {
loadCompetitionList() await loadCompetitionList()
// 加载筛选用的场地列表
if (queryParams.competitionId) {
await loadFilterVenueList()
}
}) })
</script> </script>

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(scope.row.status)" size="small"> <el-tag :type="getStatusType(calculateStatus(scope.row))" size="small">
{{ getStatusText(scope.row.status) }} {{ getStatusText(calculateStatus(scope.row)) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -181,13 +181,13 @@ export default {
try { try {
const res = await getScheduleResult(competition.id) const res = await getScheduleResult(competition.id)
if (res.data?.data) { if (res.data?.data) {
this.$set(this.scheduleStatusMap, competition.id, res.data.data.isCompleted || false) this.scheduleStatusMap[competition.id] = res.data.data.isCompleted || false
} else { } else {
this.$set(this.scheduleStatusMap, competition.id, false) this.scheduleStatusMap[competition.id] = false
} }
} catch (err) { } catch (err) {
// 如果获取失败,默认为未完成 // 如果获取失败,默认为未完成
this.$set(this.scheduleStatusMap, competition.id, false) this.scheduleStatusMap[competition.id] = false
} }
} }
}, },
@@ -252,6 +252,19 @@ 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 <= regEnd) return 2
return 1
},
getStatusType(status) { getStatusType(status) {
const statusMap = { const statusMap = {
1: 'info', // 未开始 1: 'info', // 未开始

View File

@@ -0,0 +1,355 @@
<template>
<div class="martial-order-container">
<el-card shadow="hover">
<div class="page-header">
<h2 class="page-title">订单管理</h2>
</div>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item>
<el-input
v-model="searchForm.keyword"
placeholder="搜索赛事名称"
clearable
size="small"
style="width: 240px"
>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
</el-form-item>
<el-form-item>
<el-select
v-model="searchForm.status"
placeholder="赛事状态"
clearable
size="small"
style="width: 180px"
>
<el-option label="未开始" :value="1"></el-option>
<el-option label="报名中" :value="2"></el-option>
<el-option label="进行中" :value="3"></el-option>
<el-option label="已结束" :value="4"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="handleSearch">查询</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
size="small"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column prop="competitionName" label="赛事名称" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="competitionCode" label="赛事编号" width="150"></el-table-column>
<el-table-column prop="organizer" label="主办单位" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="location" label="举办地点" width="120"></el-table-column>
<el-table-column prop="registrationTime" label="报名时间" width="180" show-overflow-tooltip>
<template #default="scope">
<span>{{ formatDateRange(scope.row.registrationStartTime, scope.row.registrationEndTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="competitionTime" label="比赛时间" width="180" show-overflow-tooltip>
<template #default="scope">
<span>{{ formatDateRange(scope.row.competitionStartTime, scope.row.competitionEndTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="320" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleRegistrationDetail(scope.row)">报名详情</el-button>
<el-button type="success" size="small" @click="handleSchedule(scope.row)">编排</el-button>
<el-button
type="warning"
size="small"
@click="handleDispatch(scope.row)"
:title="isScheduleCompleted(scope.row.id) ? '进入调度' : '请先完成编排'"
>
调度
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.current"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.size"
layout="total, sizes, prev, pager, next"
:total="pagination.total"
small
></el-pagination>
</el-card>
</div>
</template>
<script>
import { getCompetitionList } from '@/api/martial/competition'
import { getScheduleResult } from '@/api/martial/activitySchedule'
export default {
name: 'MartialOrderList',
data() {
return {
loading: false,
searchForm: {
keyword: '',
status: null
},
tableData: [],
pagination: {
current: 1,
size: 10,
total: 0
},
scheduleStatusMap: {} // 存储每个赛事的编排状态
}
},
mounted() {
this.loadCompetitionList()
},
activated() {
// 当页面被激活时(从其他页面返回),重新加载编排状态
if (this.tableData.length > 0) {
this.loadScheduleStatus()
}
},
methods: {
// 加载赛事列表
loadCompetitionList() {
this.loading = true
const params = {}
if (this.searchForm.keyword) {
params.competitionName = this.searchForm.keyword
}
if (this.searchForm.status !== null && this.searchForm.status !== '') {
params.status = this.searchForm.status
}
getCompetitionList(this.pagination.current, this.pagination.size, params)
.then(res => {
console.log('赛事列表返回数据:', res)
const responseData = res.data?.data
if (responseData && responseData.records) {
// 处理赛事数据,兼容驼峰和下划线命名
this.tableData = responseData.records.map(competition => ({
id: competition.id,
competitionName: competition.competitionName || competition.competition_name,
competitionCode: competition.competitionCode || competition.competition_code,
organizer: competition.organizer,
location: competition.location,
venue: competition.venue,
registrationStartTime: competition.registrationStartTime || competition.registration_start_time,
registrationEndTime: competition.registrationEndTime || competition.registration_end_time,
competitionStartTime: competition.competitionStartTime || competition.competition_start_time,
competitionEndTime: competition.competitionEndTime || competition.competition_end_time,
status: competition.status,
createTime: competition.createTime || competition.create_time
}))
this.pagination.total = responseData.total || 0
// 加载每个赛事的编排状态
this.loadScheduleStatus()
}
})
.catch(err => {
console.error('加载赛事列表失败', err)
this.$message.error('加载赛事列表失败')
})
.finally(() => {
this.loading = false
})
},
// 加载编排状态
async loadScheduleStatus() {
for (const competition of this.tableData) {
try {
const res = await getScheduleResult(competition.id)
if (res.data?.data) {
this.$set(this.scheduleStatusMap, competition.id, res.data.data.isCompleted || false)
} else {
this.$set(this.scheduleStatusMap, competition.id, false)
}
} catch (err) {
// 如果获取失败,默认为未完成
this.$set(this.scheduleStatusMap, competition.id, false)
}
}
},
// 检查编排是否完成
isScheduleCompleted(competitionId) {
return this.scheduleStatusMap[competitionId] === true
},
handleSearch() {
this.pagination.current = 1
this.loadCompetitionList()
},
handleSizeChange(size) {
this.pagination.size = size
this.pagination.current = 1
this.loadCompetitionList()
},
handleCurrentChange(current) {
this.pagination.current = current
this.loadCompetitionList()
},
// 查看报名详情 - 传递赛事ID
handleRegistrationDetail(row) {
this.$router.push({
path: '/martial/registration/detail',
query: { competitionId: row.id }
})
},
// 编排 - 传递赛事ID
handleSchedule(row) {
this.$router.push({
path: '/martial/schedule/list',
query: { competitionId: row.id }
})
},
// 调度 - 传递赛事ID
handleDispatch(row) {
// 检查编排是否完成
if (!this.isScheduleCompleted(row.id)) {
this.$message.warning('请先完成编排后再进行调度')
return
}
this.$router.push({
path: '/martial/dispatch/list',
query: { competitionId: row.id }
})
},
// 格式化日期范围
formatDateRange(startTime, endTime) {
if (!startTime || !endTime) return '-'
// 简单格式化,只显示日期部分
const start = startTime.split(' ')[0]
const end = endTime.split(' ')[0]
return `${start} ~ ${end}`
},
getStatusType(status) {
const statusMap = {
1: 'info', // 未开始
2: 'success', // 报名中
3: 'warning', // 进行中
4: 'info' // 已结束
}
return statusMap[status] || 'info'
},
getStatusText(status) {
const statusMap = {
1: '未开始',
2: '报名中',
3: '进行中',
4: '已结束'
}
return statusMap[status] || '未知'
}
}
}
</script>
<style lang="scss" scoped>
.martial-order-container {
min-height: 100%;
padding: 15px;
background: #fff;
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
}
.tip-message {
display: flex;
gap: 12px;
padding: 10px 14px;
margin-bottom: 15px;
background: linear-gradient(90deg, #ffd54f 0%, #ffecb3 100%);
border: 1px solid #ffc107;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(255, 193, 7, 0.2);
.tip-header {
flex-shrink: 0;
.tip-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: #ff9800;
color: #fff;
font-size: 11px;
font-weight: bold;
border-radius: 50%;
}
}
.tip-content {
flex: 1;
color: #5d4037;
font-size: 12px;
line-height: 1.5;
.tip-subtitle {
margin-top: 3px;
color: #6d4c41;
font-size: 11px;
font-style: italic;
}
}
}
.search-form {
margin-bottom: 15px;
}
.amount-text {
color: #e6a23c;
font-weight: 600;
font-size: 14px;
}
.pagination {
margin-top: 15px;
text-align: right;
}
}
</style>

View File

@@ -94,11 +94,14 @@
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="age"
label="年龄" label="年龄"
width="80" width="80"
align="center" align="center"
/> >
<template #default="scope">
{{ scope.row.age === -1 || scope.row.age === null || scope.row.age === undefined ? '--' : scope.row.age }}
</template>
</el-table-column>
<el-table-column <el-table-column
prop="organization" prop="organization"

View File

@@ -0,0 +1,990 @@
<template>
<div class="participant-container">
<!-- 列表视图 -->
<div v-if="currentView === 'list'" class="list-view">
<el-card shadow="hover">
<div class="list-header">
<h2 class="page-title">参赛选手管理</h2>
<el-button type="primary" icon="el-icon-plus" @click="handleCreate">
添加选手
</el-button>
</div>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item>
<el-input
v-model="searchForm.keyword"
placeholder="搜索选手姓名"
clearable
size="small"
style="width: 240px"
>
<template #prefix>
<i class="el-input__icon el-icon-search"></i>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.competitionId" placeholder="选择赛事" clearable size="small" style="width: 200px">
<el-option label="全部赛事" :value="null" />
<el-option
v-for="item in allCompetitionOptions"
:key="item.id"
:label="item.competitionName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="handleSearch">查询</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="displayList"
border
stripe
style="width: 100%"
class="data-table"
>
<el-table-column
type="index"
label="序号"
width="60"
align="center"
/>
<el-table-column
prop="playerName"
label="选手姓名"
width="120"
/>
<el-table-column
prop="competitionName"
label="所属赛事"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="projectName"
label="参赛项目"
width="120"
/>
<el-table-column
prop="category"
label="组别"
width="100"
/>
<el-table-column
label="性别"
width="80"
align="center"
>
<template #default="scope">
<el-tag :type="scope.row.gender === 1 ? 'primary' : 'danger'" size="small">
{{ scope.row.gender === 1 ? '男' : '女' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="age"
label="年龄"
width="80"
align="center"
/>
<el-table-column
prop="organization"
label="所属单位"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="contactPhone"
label="联系电话"
width="120"
/>
<el-table-column
prop="orderNum"
label="出场顺序"
width="100"
align="center"
/>
<el-table-column
label="操作"
width="220"
fixed="right"
align="center"
>
<template #default="scope">
<el-button
type="primary"
link
size="small"
icon="el-icon-view"
@click="handleView(scope.row)"
>
查看
</el-button>
<el-button
type="warning"
link
size="small"
icon="el-icon-edit"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
link
size="small"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="pagination.total > 0"
@size-change="handleSizeChange"
@current-change="handlePageChange"
:current-page="pagination.current"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.size"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 20px; text-align: right"
/>
</el-card>
</div>
<!-- 表单视图 -->
<div v-else class="form-view">
<el-card shadow="hover" v-loading="loading">
<div class="page-header">
<el-button
icon="el-icon-arrow-left"
@click="backToList"
>
返回列表
</el-button>
<h2 class="page-title">{{ pageTitle }}</h2>
<div class="header-actions" v-if="currentView !== 'view'">
<el-button @click="backToList">取消</el-button>
<el-button type="primary" @click="handleSave">
{{ currentView === 'create' ? '创建' : '保存' }}
</el-button>
</div>
<div class="header-actions" v-else>
<el-button type="primary" @click="switchToEdit">编辑</el-button>
</div>
</div>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
:disabled="currentView === 'view'"
class="participant-form"
>
<!-- 基本信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-user"></i>
基本信息
</div>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="选手姓名" prop="playerName">
<el-input
v-model="formData.playerName"
placeholder="请输入选手姓名"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="formData.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="formData.age"
:min="6"
:max="100"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="formData.idCard"
placeholder="请输入身份证号"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input
v-model="formData.contactPhone"
placeholder="请输入联系电话"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="所属单位" prop="organization">
<el-input
v-model="formData.organization"
placeholder="请输入所属单位"
/>
</el-form-item>
</div>
<!-- 赛事信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-trophy"></i>
赛事信息
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属赛事" prop="competitionId">
<el-select
v-model="formData.competitionId"
placeholder="请选择赛事"
style="width: 100%"
@change="handleCompetitionChange"
>
<el-option
v-for="item in availableCompetitionOptions"
: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="projectId">
<el-select
v-model="formData.projectId"
placeholder="请选择参赛项目"
style="width: 100%"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.projectName"
: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="category">
<el-input
v-model="formData.category"
placeholder="例如:成年男子组"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出场顺序" prop="orderNum">
<el-input-number
v-model="formData.orderNum"
:min="1"
:max="9999"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 其他信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-document"></i>
其他信息
</div>
<el-form-item label="选手简介" prop="introduction">
<el-input
v-model="formData.introduction"
type="textarea"
:rows="4"
placeholder="请输入选手简介"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</div>
</el-form>
</el-card>
</div>
</div>
</template>
<script>
import { getCompetitionList } from '@/api/martial/competition'
import { getInfoPublishList } from '@/api/martial/infoPublish'
import { getProjectsByCompetition } from '@/api/martial/project'
import {
getParticipantList,
getParticipantDetail,
addParticipant,
updateParticipant,
removeParticipant
} from '@/api/martial/participant'
export default {
name: 'ParticipantManagement',
data() {
return {
loading: false,
currentView: 'list', // list, create, edit, view
participantId: null,
searchForm: {
keyword: '',
competitionId: null
},
pagination: {
current: 1,
size: 10,
total: 0
},
competitionOptions: [], // 已发布的可报名赛事列表(用于新建)
allCompetitionOptions: [], // 所有赛事列表(用于搜索和编辑)
projectOptions: [], // 项目列表
participantList: [],
formData: {
orderId: null,
competitionId: null,
competitionName: '',
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectId: null,
category: '',
orderNum: 1,
introduction: '',
remark: '',
attachments: []
},
formRules: {
playerName: [
{ required: true, message: '请输入选手姓名', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' }
],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
competitionId: [
{ required: true, message: '请选择赛事', trigger: 'change' }
],
projectId: [
{ required: true, message: '请选择参赛项目', trigger: 'change' }
]
}
};
},
computed: {
pageTitle() {
const titleMap = {
create: '添加参赛选手',
edit: '编辑参赛选手',
view: '查看参赛选手'
};
return titleMap[this.currentView] || '参赛选手信息';
},
displayList() {
return this.participantList;
},
// 根据不同模式返回不同的赛事选项
availableCompetitionOptions() {
// 编辑和查看模式:显示所有赛事(因为可能编辑已过报名期的选手)
if (this.currentView === 'edit' || this.currentView === 'view') {
return this.allCompetitionOptions;
}
// 新建模式:只显示可报名的赛事
return this.competitionOptions;
}
},
watch: {
'$route.query': {
handler(query) {
this.initPage();
},
immediate: true
}
},
mounted() {
this.loadAvailableCompetitions();
this.loadAllCompetitions();
},
methods: {
initPage() {
const { mode, id } = this.$route.query;
this.currentView = mode || 'list';
// 不使用 parseInt保持 ID 为字符串避免精度丢失
this.participantId = id || null;
if (this.currentView === 'list') {
this.loadParticipantList();
} else if (this.currentView !== 'list' && this.participantId) {
this.loadParticipantData();
} else if (this.currentView === 'create') {
this.resetFormData();
}
},
// 加载可报名的赛事(从已发布的信息中获取)
loadAvailableCompetitions() {
getInfoPublishList(1, 100, { isPublished: 1 })
.then(res => {
console.log('已发布信息列表返回数据:', res);
const responseData = res.data?.data;
if (responseData && responseData.records) {
const publishedCompetitionIds = new Set(
responseData.records
.filter(item => item.competitionId)
.map(item => item.competitionId)
);
console.log('已发布的赛事ID列表:', Array.from(publishedCompetitionIds));
if (publishedCompetitionIds.size > 0) {
this.loadPublishedCompetitions(Array.from(publishedCompetitionIds));
} else {
// 如果没有发布信息,直接加载所有赛事作为可报名赛事
console.log('没有已发布信息,加载所有赛事');
this.loadPublishedCompetitions([]);
}
}
})
.catch(err => {
console.error('加载已发布信息列表失败', err);
// 出错时也加载所有赛事
this.loadPublishedCompetitions([]);
});
},
// 加载已发布的赛事详细信息,并过滤出可报名的赛事
loadPublishedCompetitions(competitionIds) {
getCompetitionList(1, 100, {})
.then(res => {
console.log('赛事列表返回数据:', res);
const responseData = res.data?.data;
if (responseData && responseData.records) {
const now = new Date();
this.competitionOptions = responseData.records
.filter(item => {
// 如果没有发布信息competitionIds为空数组则显示所有在报名期内的赛事
if (competitionIds.length > 0 && !competitionIds.includes(item.id)) {
return false;
}
// 检查报名时间
if (!item.registrationStartTime || !item.registrationEndTime) {
return false;
}
const regStart = new Date(item.registrationStartTime);
const regEnd = new Date(item.registrationEndTime);
return now >= regStart && now <= regEnd;
})
.map(item => ({
id: item.id,
competitionName: item.competitionName,
registrationStartTime: item.registrationStartTime,
registrationEndTime: item.registrationEndTime
}));
console.log('可报名的赛事列表:', this.competitionOptions);
if (this.competitionOptions.length === 0) {
console.log('当前没有可以报名的赛事(报名时间范围外)');
}
}
})
.catch(err => {
console.error('加载赛事列表失败', err);
this.$message.error('加载赛事列表失败');
});
},
// 加载所有赛事(用于搜索过滤)
loadAllCompetitions() {
getCompetitionList(1, 100, {})
.then(res => {
const responseData = res.data?.data;
if (responseData && responseData.records) {
this.allCompetitionOptions = responseData.records.map(item => ({
id: item.id,
competitionName: item.competitionName
}));
}
})
.catch(err => {
console.error('加载所有赛事失败', err);
});
},
loadParticipantList() {
this.loading = true;
const params = {};
if (this.searchForm.keyword) {
params.playerName = this.searchForm.keyword;
}
if (this.searchForm.competitionId) {
params.competitionId = this.searchForm.competitionId;
}
getParticipantList(null, this.pagination.current, this.pagination.size, params)
.then(res => {
console.log('参赛人员列表返回数据:', res);
const responseData = res.data?.data;
if (responseData && responseData.records) {
this.participantList = responseData.records;
this.pagination.total = responseData.total || 0;
}
})
.catch(err => {
console.error('加载参赛人员列表失败', err);
this.$message.error('加载参赛人员列表失败');
})
.finally(() => {
this.loading = false;
});
},
loadParticipantData() {
if (!this.participantId) return;
this.loading = true;
getParticipantDetail(this.participantId)
.then(res => {
const detailData = res.data?.data;
if (detailData) {
this.formData = { ...detailData };
// 将 attachments 字符串转换为数组(前端需要数组格式)
if (typeof this.formData.attachments === 'string') {
try {
this.formData.attachments = JSON.parse(this.formData.attachments);
} catch (e) {
console.warn('解析 attachments 失败,使用空数组', e);
this.formData.attachments = [];
}
} else if (!this.formData.attachments) {
this.formData.attachments = [];
}
// 加载该赛事的项目列表
if (detailData.competitionId) {
this.loadProjectsByCompetition(detailData.competitionId);
}
}
})
.catch(err => {
console.error('加载参赛人员详情失败', err);
this.$message.error('加载参赛人员详情失败');
})
.finally(() => {
this.loading = false;
});
},
handlePageChange(current) {
this.pagination.current = current;
this.loadParticipantList();
},
handleSizeChange(size) {
this.pagination.size = size;
this.pagination.current = 1;
this.loadParticipantList();
},
handleSearch() {
this.pagination.current = 1;
this.loadParticipantList();
},
handleReset() {
this.searchForm = {
keyword: '',
competitionId: null
};
this.pagination.current = 1;
this.loadParticipantList();
},
handleCreate() {
this.$router.push({
path: '/martial/participant/list',
query: { mode: 'create' }
});
},
handleView(row) {
this.$router.push({
path: '/martial/participant/list',
query: { mode: 'view', id: row.id }
});
},
handleEdit(row) {
this.$router.push({
path: '/martial/participant/list',
query: { mode: 'edit', id: row.id }
});
},
switchToEdit() {
this.$router.push({
path: '/martial/participant/list',
query: { mode: 'edit', id: this.participantId }
});
},
handleDelete(row) {
this.$confirm('确定要删除该选手吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.loading = true;
removeParticipant(row.id.toString())
.then(res => {
this.$message.success('删除成功');
this.loadParticipantList();
})
.catch(err => {
console.error('删除失败', err);
this.$message.error('删除失败');
})
.finally(() => {
this.loading = false;
});
}).catch(() => {});
},
handleCompetitionChange(competitionId) {
// 从可用的选项列表中查找赛事
const competition = this.availableCompetitionOptions.find(item => item.id === competitionId);
if (competition) {
this.formData.competitionName = competition.competitionName;
}
// 加载该赛事的项目列表
this.loadProjectsByCompetition(competitionId);
// 清空已选项目
this.formData.projectId = null;
},
handleProjectChange(projectId) {
const project = this.projectOptions.find(item => item.id === projectId);
if (project) {
// 自动填充组别信息
if (project.category && !this.formData.category) {
this.formData.category = project.category;
}
}
},
loadProjectsByCompetition(competitionId) {
if (!competitionId) {
this.projectOptions = [];
return;
}
console.log('加载赛事项目赛事ID:', competitionId);
getProjectsByCompetition(competitionId)
.then(res => {
console.log('项目列表返回数据:', res);
const responseData = res.data?.data;
// 兼容两种数据格式分页数据有records和直接数组
let projectList = [];
if (responseData) {
if (Array.isArray(responseData)) {
// 直接是数组
projectList = responseData;
console.log('返回的是直接数组,长度:', projectList.length);
} else if (responseData.records && Array.isArray(responseData.records)) {
// 分页数据
projectList = responseData.records;
console.log('返回的是分页数据,记录数:', projectList.length);
} else {
console.warn('未知的数据格式:', responseData);
}
}
if (projectList.length > 0) {
this.projectOptions = projectList.map(item => ({
id: item.id,
projectName: item.projectName,
projectCode: item.projectCode,
category: item.category
}));
console.log('可选项目列表:', this.projectOptions);
} else {
this.projectOptions = [];
console.log('该赛事没有项目数据');
this.$message.warning('该赛事还没有配置项目,请先添加项目');
}
})
.catch(err => {
console.error('加载项目列表失败', err);
this.$message.error('加载项目列表失败: ' + (err.message || '未知错误'));
this.projectOptions = [];
});
},
handleSave() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.loading = true;
// 确保有赛事名称
if (!this.formData.competitionName) {
const competition = this.availableCompetitionOptions.find(item => item.id === this.formData.competitionId);
if (competition) {
this.formData.competitionName = competition.competitionName;
}
}
const submitData = { ...this.formData };
console.log('=== 提交前的 formData ===', this.formData);
console.log('formData.orderId:', this.formData.orderId);
// 将 attachments 数组转换为 JSON 字符串(后端需要 String 类型)
if (Array.isArray(submitData.attachments)) {
submitData.attachments = JSON.stringify(submitData.attachments);
}
// 临时方案: 如果没有 orderId使用 competitionId 作为 orderId
// 警告: 这是临时解决方案,后续应修改数据库表结构或后端逻辑
if (!submitData.orderId && submitData.competitionId) {
submitData.orderId = submitData.competitionId;
console.warn('⚠️ 临时方案: 使用 competitionId 作为 orderId', submitData.competitionId);
}
console.log('=== 提交的数据 submitData ===', submitData);
console.log('submitData.orderId:', submitData.orderId);
if (this.currentView === 'create') {
// 新建
addParticipant(submitData)
.then(res => {
this.$message.success('添加成功');
this.backToList();
})
.catch(err => {
console.error('添加失败', err);
this.$message.error('添加失败');
})
.finally(() => {
this.loading = false;
});
} else if (this.currentView === 'edit') {
// 编辑
submitData.id = this.participantId;
updateParticipant(submitData)
.then(res => {
this.$message.success('保存成功');
this.backToList();
})
.catch(err => {
console.error('保存失败', err);
this.$message.error('保存失败');
})
.finally(() => {
this.loading = false;
});
}
} else {
this.$message.error('请完善必填信息');
}
});
},
backToList() {
this.$router.push({
path: '/martial/participant/list'
});
},
resetFormData() {
this.formData = {
orderId: null,
competitionId: null,
competitionName: '',
playerName: '',
gender: 1,
age: null,
contactPhone: '',
organization: '',
idCard: '',
projectId: null,
category: '',
orderNum: 1,
introduction: '',
remark: '',
attachments: []
};
}
}
};
</script>
<style lang="scss" scoped>
.participant-container {
padding: 20px;
background: #fff;
border-radius: 8px;
min-height: calc(100vh - 120px);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.search-form {
margin-bottom: 20px;
}
.data-table {
:deep(.el-table__header) {
th {
background-color: #fafafa;
color: #333;
font-weight: 600;
}
}
:deep(.el-button--text) {
padding: 0 8px;
}
}
.page-header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
.page-title {
flex: 1;
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
gap: 10px;
}
}
.participant-form {
max-width: 1200px;
}
.form-section {
margin-bottom: 30px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
font-size: 16px;
font-weight: 600;
color: #333;
i {
color: #dc2626;
font-size: 18px;
}
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
</style>

View File

@@ -28,17 +28,12 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="分组类别"> <el-form-item label="分组类别">
<el-select <el-input
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
@@ -47,15 +42,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.participantType" v-model="queryParams.type"
placeholder="请选择参赛类型" placeholder="请选择参赛类型"
clearable clearable
style="width: 150px" style="width: 150px"
@@ -133,66 +128,53 @@
min-width="180" min-width="180"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column label="所属赛事" min-width="150" show-overflow-tooltip>
prop="competitionName" <template #default="{ row }">
label="所属赛事" {{ getCompetitionName(row.competitionId) }}
min-width="150" </template>
show-overflow-tooltip </el-table-column>
/>
<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">男子</el-tag> <span>{{ row.category || '-' }}</span>
<el-tag v-else-if="row.category === 2" type="danger">女子</el-tag>
<el-tag v-else-if="row.category === 3" type="success">团体</el-tag>
<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 }">
<span v-if="row.eventType === 1">套路</span> <el-tag v-if="row.eventType === 1" type="primary" size="small">套路</el-tag>
<span v-else-if="row.eventType === 2">散打</span> <el-tag v-else-if="row.eventType === 2" type="danger" size="small">散打</el-tag>
<span v-else-if="row.eventType === 3">器械</span> <el-tag v-else-if="row.eventType === 3" type="success" size="small">器械</el-tag>
<span v-else-if="row.eventType === 4">对练</span> <el-tag v-else-if="row.eventType === 4" type="warning" size="small">对练</el-tag>
<span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="participantType" label="参赛类型" width="100" align="center"> <el-table-column prop="type" label="参赛类型" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.participantType === 1" type="success" size="small">单人</el-tag> <el-tag v-if="row.type === 1" type="success" size="small">单人</el-tag>
<el-tag v-else-if="row.participantType === 2" type="warning" size="small">集体</el-tag> <el-tag v-else-if="row.type === 2" type="warning" size="small">集体</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="registrationFee" prop="price"
label="报名费(元)" label="报名费(元)"
width="110" width="110"
align="center" align="center"
> >
<template #default="{ row }"> <template #default="{ row }">
<span style="color: #f56c6c">¥{{ row.registrationFee || 0 }}</span> <span style="color: #f56c6c">¥{{ row.price || 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 }">
<div v-if="row.registrationStartTime && row.registrationEndTime"> <span>{{ row.estimatedDuration || 5 }}分钟</span>
<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 }">
<span :style="{ color: row.currentCount >= row.maxParticipants ? '#f56c6c' : '#67c23a' }"> {{ row.maxParticipants || 0 }}
{{ 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="创建时间"
@@ -264,15 +246,6 @@
</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">
@@ -315,9 +288,9 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="参赛类型" prop="participantType"> <el-form-item label="参赛类型" prop="type">
<el-select <el-select
v-model="form.participantType" v-model="form.type"
placeholder="请选择参赛类型" placeholder="请选择参赛类型"
style="width: 100%" style="width: 100%"
> >
@@ -328,6 +301,17 @@
</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
@@ -342,33 +326,7 @@
</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="registrationStartTime"> <el-form-item label="单位容纳人数" prop="maxParticipants">
<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"
@@ -426,7 +384,7 @@
{{ detailData.projectName }} {{ detailData.projectName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="所属赛事"> <el-descriptions-item label="所属赛事">
{{ detailData.competitionName }} {{ getCompetitionName(detailData.competitionId) }}
</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>
@@ -441,24 +399,18 @@
<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.participantType === 1" type="success" size="small">单人</el-tag> <el-tag v-if="detailData.type === 1" type="success" size="small">单人</el-tag>
<el-tag v-else-if="detailData.participantType === 2" type="warning" size="small">集体</el-tag> <el-tag v-else-if="detailData.type === 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'
@@ -495,8 +447,7 @@ import {
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { import {
getProjectList, getProjectList,
addProject, submitProject,
updateProject,
removeProject, removeProject,
importProjects, importProjects,
exportProjects exportProjects
@@ -526,7 +477,7 @@ const queryParams = reactive({
projectName: '', projectName: '',
category: '', category: '',
eventType: '', eventType: '',
participantType: '' type: ''
}) })
// 表单数据 // 表单数据
@@ -537,10 +488,9 @@ const form = reactive({
projectName: '', projectName: '',
category: null, category: null,
eventType: null, eventType: null,
participantType: null, type: null,
estimatedDuration: 5,
registrationFee: 0, registrationFee: 0,
registrationStartTime: '',
registrationEndTime: '',
maxParticipants: 100, maxParticipants: 100,
sortOrder: 0, sortOrder: 0,
rules: '', rules: '',
@@ -562,10 +512,6 @@ 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' }
@@ -576,39 +522,26 @@ const rules = {
eventType: [ eventType: [
{ required: true, message: '请选择项目类型', trigger: 'change' } { required: true, message: '请选择项目类型', trigger: 'change' }
], ],
participantType: [ estimatedDuration: [
{ 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, { status: 1 }) const res = await getCompetitionList(1, 1000, {})
if (res.data && res.data.records) { if (res.data && res.data.data && res.data.data.records) {
competitionList.value = res.data.records competitionList.value = res.data.data.records
} }
} catch (error) { } catch (error) {
console.error('加载赛事列表失败:', error) console.error('加载赛事列表失败:', error)
@@ -619,14 +552,22 @@ 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,
queryParams params
) )
if (res.data) { if (res.data && res.data.data) {
tableData.value = res.data.records || [] tableData.value = res.data.data.records || []
total.value = res.data.total || 0 total.value = res.data.data.total || 0
} }
} catch (error) { } catch (error) {
ElMessage.error('获取数据失败') ElMessage.error('获取数据失败')
@@ -651,7 +592,7 @@ const handleReset = () => {
projectName: '', projectName: '',
category: '', category: '',
eventType: '', eventType: '',
participantType: '' type: ''
}) })
fetchData() fetchData()
} }
@@ -664,11 +605,15 @@ const handleAdd = () => {
} }
// 编辑 // 编辑
const handleEdit = (row) => { const handleEdit = async (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
} }
@@ -725,11 +670,16 @@ 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 updateProject(form) await submitProject(submitData)
ElMessage.success('修改成功') ElMessage.success('修改成功')
} else { } else {
await addProject(form) await submitProject(submitData)
ElMessage.success('新增成功') ElMessage.success('新增成功')
} }
dialogVisible.value = false dialogVisible.value = false
@@ -762,10 +712,9 @@ const resetForm = () => {
projectName: '', projectName: '',
category: null, category: null,
eventType: null, eventType: null,
participantType: null, type: null,
estimatedDuration: 5,
registrationFee: 0, registrationFee: 0,
registrationStartTime: '',
registrationEndTime: '',
maxParticipants: 100, maxParticipants: 100,
sortOrder: 0, sortOrder: 0,
rules: '', rules: '',
@@ -830,6 +779,13 @@ 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 '-'

View File

@@ -0,0 +1,931 @@
<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>

View File

@@ -33,8 +33,9 @@
<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>
@@ -63,8 +64,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' : 'primary'" size="small"> <el-tag :type="scope.row.refereeType === 1 ? 'danger' : (scope.row.refereeType === 3 ? 'warning' : 'primary')" size="small">
{{ scope.row.refereeType === 1 ? '主裁判' : '普通裁判' }} {{ scope.row.refereeType === 1 ? '主裁判' : (scope.row.refereeType === 3 ? '总裁' : '裁判员') }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -137,7 +138,8 @@
<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>

View File

@@ -101,12 +101,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="category" label="类别" width="80"></el-table-column> <el-table-column prop="singleCount" 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="teamCount" label="集体项目" width="100" align="center"></el-table-column>
<el-table-column prop="dual" 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="team1101" label="1101" width="60" align="center"></el-table-column> <el-table-column prop="female" label="" 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>
@@ -125,10 +123,11 @@
<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="participantCategory" label="人/单位" width="100"></el-table-column> <el-table-column prop="projectType" 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="athleteCount" label="人数/集体" width="100" align="center"></el-table-column>
<el-table-column prop="singleTeamPeople" label="单位型号人数" width="120" align="center"></el-table-column> <el-table-column prop="groupCount" label="数" width="80" align="center"></el-table-column>
<el-table-column prop="estimatedDuration" label="剩下显时(公共)" width="150" align="center"></el-table-column> <el-table-column prop="estimatedDuration" label="时长(分)" width="100" align="center"></el-table-column>
<el-table-column prop="projectCode" label="项目编码" width="120" align="center"></el-table-column>
</el-table> </el-table>
</div> </div>
@@ -335,76 +334,85 @@ 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,
category: '', singleCount: 0,
individual: 0, teamCount: 0,
dual: 0, male: 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分组 // 预加载项目信息
const projectMap = new Map() await this.preloadProjectInfo(participants)
// 按项目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) {
if (!projectMap.has(projectId)) { projectAthleteCount.set(projectId, (projectAthleteCount.get(projectId) || 0) + 1)
projectMap.set(projectId, [])
}
projectMap.get(projectId).push(athlete)
} }
}) })
// 3. 从缓存中获取项目信息并统计(项目信息已经在 loadRegistrationStats 中预加载) // 从缓存中获取项目信息并构建统计数据
const projectStats = [] const projectStats = []
for (const [projectId, athleteList] of projectMap) { for (const [projectId, count] of projectAthleteCount) {
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 || '未知项目',
participantCategory: project.category || '', projectType: projectType === 1 ? '单人' : '集体',
teamCount: 1, // 简化处理设为1 athleteCount: count,
singleTeamPeople: athleteList.length, groupCount: projectType === 2 ? count : '-', // 集体项目显示组数,单人显示-
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0 estimatedDuration: project.estimatedDuration || project.estimated_duration || 0,
}) projectCode: project.projectCode || project.project_code || ''
} else {
// 如果缓存中没有理论上<E8AEBA><E4B88A><EFBFBD>应该发生添加基本信息
projectStats.push({
projectName: `项目ID:${projectId}`,
participantCategory: '',
teamCount: 1,
singleTeamPeople: athleteList.length,
estimatedDuration: 0
}) })
} }
} }
@@ -417,7 +425,7 @@ export default {
} }
}, },
// 加载金额统计(该赛事所有单位的报名金额) // 加载金额统计(该赛事所有单位的报名金额)
async loadAmountStats() { async loadAmountStats() {
try { try {
// 使用缓存的参赛者列表 // 使用缓存的参赛者列表

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -94,7 +94,8 @@
<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 class="total-score">{{ formatScore(scope.row.totalScore) }}</span> <span v-if="scope.row.scoreStatus === 2" class="total-score">{{ formatScore(scope.row.chiefJudgeScore) }}</span>
<span v-else class="pending-score">待确认</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -153,7 +154,17 @@
<div class="total-score-display"> <div class="total-score-display">
<span class="label">总分</span> <span class="label">总分</span>
<span class="value">{{ formatScore(currentDetail.totalScore) }}</span> <template v-if="currentDetail.scoreStatus === 2">
<span class="value">{{ formatScore(currentDetail.chiefJudgeScore) }}</span>
<div class="calculation-note">(主裁判已确认)</div>
</template>
<template v-else>
<span class="value pending">待确认最终得分</span>
<div class="calculation-note">
裁判员评分: {{ formatScore(currentDetail.totalScore) }}
<span v-if="currentDetail.judgeScores.length > 2">(去掉最高最低分后平均)</span>
</div>
</template>
</div> </div>
</div> </div>
</div> </div>
@@ -182,96 +193,6 @@ export default {
projectOptions: [], projectOptions: [],
venueOptions: [], venueOptions: [],
scoreList: [], scoreList: [],
allTableData: [
{
id: 1,
projectName: '男子组陈氏太极拳',
venueName: '第一场地',
playerName: '张三',
teamName: '少林寺武术大学院',
idCard: '123456789000000000',
playerNo: '123-4567898275',
judgeScores: [8.906, 8.905, 8.908, 8.907, 8.906],
totalScore: 8.907
},
{
id: 2,
projectName: '女子组长拳',
venueName: '第一场地',
playerName: '李四',
teamName: '武当武术学院',
idCard: '123456789000000001',
playerNo: '123-4567898276',
judgeScores: [9.125, 9.130, 9.128, 9.126, 9.129],
totalScore: 9.128
},
{
id: 3,
projectName: '男子组陈氏太极拳',
venueName: '第二场地',
playerName: '王五',
teamName: '峨眉武术协会',
idCard: '123456789000000002',
playerNo: '123-4567898277',
judgeScores: [8.550, 8.548, 8.552, 8.551, 8.549],
totalScore: 8.550
},
{
id: 4,
projectName: '女子组双剑(含长穗双剑)',
venueName: '第一场地',
playerName: '赵六',
teamName: '昆仑武术馆',
idCard: '123456789000000003',
playerNo: '123-4567898278',
judgeScores: [9.245, 9.248, 9.246, 9.247, 9.249],
totalScore: 9.247
},
{
id: 5,
projectName: '男子组杨氏太极拳',
venueName: '第三场地',
playerName: '孙七',
teamName: '华山武术学校',
idCard: '123456789000000004',
playerNo: '123-4567898279',
judgeScores: [8.785, 8.788, 8.786, 8.787, 8.785],
totalScore: 8.786
},
{
id: 6,
projectName: '女子组刀术',
venueName: '第二场地',
playerName: '周八',
teamName: '少林寺武术大学院',
idCard: '123456789000000005',
playerNo: '123-4567898280',
judgeScores: [8.925, 8.928, 8.926, 8.927, 8.925],
totalScore: 8.926
},
{
id: 7,
projectName: '男子组棍术',
venueName: '第四场地',
playerName: '吴九',
teamName: '武当武术学院',
idCard: '123456789000000006',
playerNo: '123-4567898281',
judgeScores: [9.015, 9.018, 9.016, 9.017, 9.015],
totalScore: 9.016
},
{
id: 8,
projectName: '女子组枪术',
venueName: '第三场地',
playerName: '郑十',
teamName: '峨眉武术协会',
idCard: '123456789000000007',
playerNo: '123-4567898282',
judgeScores: [8.665, 8.668, 8.666, 8.667, 8.665],
totalScore: 8.666
}
],
tableData: [], tableData: [],
pagination: { pagination: {
current: 1, current: 1,
@@ -345,25 +266,22 @@ export default {
try { try {
const res = await getScoreList(this.pagination.current, this.pagination.size, params) const res = await getScoreList(this.pagination.current, this.pagination.size, params)
console.log('评分列表返回数据:', res) console.log('评分列表返回数据:', res)
console.log('===== 调试:后端返回的数据结构 =====')
const responseData = res.data?.data
if (responseData && responseData.records && responseData.records.length > 0) {
console.log('第一条评分记录:', responseData.records[0])
console.log('记录字段:', Object.keys(responseData.records[0]))
console.log('是否包含 projectName:', 'projectName' in responseData.records[0])
console.log('是否包含 venueName:', 'venueName' in responseData.records[0])
console.log('是否包含 playerName:', 'playerName' in responseData.records[0])
console.log('projectId 值:', responseData.records[0].projectId)
console.log('venueId 值:', responseData.records[0].venueId)
console.log('athleteId 值:', responseData.records[0].athleteId)
}
console.log('======================================')
const responseData = res.data?.data
if (responseData && responseData.records) { if (responseData && responseData.records) {
this.scoreList = responseData.records // 过滤掉 projectId 为 null 的无效记录
const validScores = responseData.records.filter(score => {
if (!score.projectId) {
console.warn('发现无效评分记录projectId为空:', score)
return false
}
return true
})
this.scoreList = validScores
// 补充关联数据(项目名称、场地名称、选手名称) // 补充关联数据(项目名称、场地名称、选手名称)
await this.enrichScoreData(responseData.records) await this.enrichScoreData(validScores)
// 按选手分组评分数据 // 按选手分组评分数据
this.processScoreData(this.scoreList) this.processScoreData(this.scoreList)
@@ -466,6 +384,12 @@ export default {
const athleteMap = new Map() const athleteMap = new Map()
scores.forEach(score => { scores.forEach(score => {
// 确保 projectId 存在
if (!score.projectId) {
console.warn('跳过无效评分记录:', score)
return
}
const key = `${score.athleteId}-${score.projectId}` const key = `${score.athleteId}-${score.projectId}`
if (!athleteMap.has(key)) { if (!athleteMap.has(key)) {
athleteMap.set(key, { athleteMap.set(key, {
@@ -480,7 +404,9 @@ export default {
playerNo: score.playerNo || '', playerNo: score.playerNo || '',
judgeScores: [], judgeScores: [],
scoreDetails: [], scoreDetails: [],
totalScore: 0 totalScore: 0,
chiefJudgeScore: score.chiefJudgeScore,
scoreStatus: score.scoreStatus || 0
}) })
} }
@@ -495,11 +421,10 @@ export default {
}) })
}) })
// 计算总分(平均分) // 计算总分(去掉最高最低分后的平均分)
this.tableData = Array.from(athleteMap.values()).map(athlete => { this.tableData = Array.from(athleteMap.values()).map(athlete => {
if (athlete.judgeScores.length > 0) { if (athlete.judgeScores.length > 0) {
const sum = athlete.judgeScores.reduce((a, b) => a + b, 0) athlete.totalScore = this.calculateFinalScore(athlete.judgeScores)
athlete.totalScore = sum / athlete.judgeScores.length
} }
return athlete return athlete
}) })
@@ -516,6 +441,34 @@ export default {
this.judgeColumns = Array(maxJudges).fill(null) this.judgeColumns = Array(maxJudges).fill(null)
}, },
/**
* 计算最终得分
* 规则:
* - 如果裁判数 <= 2直接取平均值
* - 如果裁判数 > 2去掉最高分和最低分后取平均值
*/
calculateFinalScore(scores) {
if (!scores || scores.length === 0) {
return 0
}
// 如果只有1-2个裁判直接取平均值
if (scores.length <= 2) {
const sum = scores.reduce((a, b) => a + b, 0)
return sum / scores.length
}
// 3个及以上裁判去掉最高分和最低分
const sortedScores = [...scores].sort((a, b) => a - b)
// 去掉第一个(最低分)和最后一个(最高分)
const validScores = sortedScores.slice(1, -1)
// 计算平均值
const sum = validScores.reduce((a, b) => a + b, 0)
return sum / validScores.length
},
// 查询 // 查询
handleSearch() { handleSearch() {
this.pagination.current = 1 this.pagination.current = 1
@@ -620,6 +573,16 @@ 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;
@@ -663,6 +626,12 @@ export default {
font-weight: 700; font-weight: 700;
color: #1b7c5e; color: #1b7c5e;
} }
.calculation-note {
margin-top: 8px;
font-size: 12px;
color: #999;
}
} }
} }
} }

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>

View File

@@ -14,7 +14,8 @@ export default ({ mode, command }) => {
__INTLIFY_PROD_DEVTOOLS__: false, __INTLIFY_PROD_DEVTOOLS__: false,
}, },
server: { server: {
port: 2888, port: 8083,
host: '0.0.0.0',
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8123', target: 'http://localhost:8123',