Compare commits
50 Commits
96f3b56eff
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b67a1e039c | ||
|
|
c7e78612bf | ||
|
|
49c1cd81c6 | ||
|
|
420bd29eff | ||
|
|
a1b26208a4 | ||
|
|
41c67e1ddf | ||
|
|
e5b028f084 | ||
|
|
6385acd43b | ||
|
|
c37b6d8f6f | ||
|
|
98c831eff0 | ||
|
|
586ad7e66e | ||
|
|
8656aa5abc | ||
|
|
c1f5acb644 | ||
|
|
ecd569337d | ||
|
|
be8b887a1c | ||
|
|
6a5b220f6e | ||
|
|
8f14a165e5 | ||
|
|
21fc12b18d | ||
|
|
21274e9639 | ||
|
|
564374250b | ||
|
|
77c2c51d8a | ||
|
|
578b94aa39 | ||
|
|
a9b82d7aae | ||
|
|
f412a9c759 | ||
|
|
0b9f107b2a | ||
|
|
5bbe374ebf | ||
|
|
39ff98e6c0 | ||
|
|
f1c2501afc | ||
|
|
657c4210a4 | ||
|
|
a98b18275f | ||
|
|
6267d87b18 | ||
|
|
67ffd4fc23 | ||
|
|
6befd3644a | ||
|
|
a6768c394a | ||
|
|
ac7587ef7e | ||
|
|
4f1d0b5888 | ||
|
|
cc6fabe576 | ||
| 04cd85cbe3 | |||
| c12fb79444 | |||
| 1744adcf92 | |||
|
|
7f8c5c630b | ||
|
|
694b955cef | ||
|
|
ea4650b912 | ||
|
|
e035647b51 | ||
|
|
352727b4fb | ||
|
|
5e75688e13 | ||
|
|
226d92f725 | ||
|
|
3d314fe84f | ||
| 179f7ea85d | |||
| d8730cc2c2 |
298
README.md
298
README.md
@@ -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)
|
||||
- **构建工具**: Vite 5
|
||||
- **UI 组件**: Element Plus
|
||||
- **表单/表格**: Avue
|
||||
- **HTTP 库**: Axios
|
||||
- **路由**: Vue Router 4
|
||||
- **UI 组件**: Element Plus + Avue
|
||||
- **状态管理**: Vuex 4
|
||||
- **样式**: Sass/SCSS
|
||||
- **路由**: Vue Router 4
|
||||
|
||||
## 📁 项目结构
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发环境
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
```
|
||||
|
||||
访问 http://localhost:8083
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
martial-web/
|
||||
├── src/
|
||||
│ ├── main.js # 应用入口
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── store/ # Vuex 状态管理
|
||||
│ ├── views/ # 页面组件
|
||||
│ ├── components/ # 通用组件
|
||||
│ ├── api/ # API 接口
|
||||
│ └── utils/ # 工具函数
|
||||
├── public/ # 静态资源
|
||||
├── .env.development # 开发环境配置
|
||||
├── .env.production # 生产环境配置
|
||||
├── vite.config.js # Vite 配置
|
||||
├── nginx.conf # Nginx 配置(生产环境容器使用)
|
||||
├── Dockerfile # 完整构建 Dockerfile
|
||||
├── Dockerfile.deploy # 部署 Dockerfile
|
||||
└── .drone.yml # Drone CI/CD 配置
|
||||
│ ├── views/martial/ # 武术业务页面
|
||||
│ │ ├── competition/ # 赛事管理
|
||||
│ │ ├── project/ # 项目管理
|
||||
│ │ ├── participant/ # 参赛选手
|
||||
│ │ ├── judgeInvite/ # 裁判邀请
|
||||
│ │ ├── score/ # 评分管理
|
||||
│ │ └── ...
|
||||
│ ├── api/ # API 接口
|
||||
│ ├── router/ # 路由配置
|
||||
│ └── store/ # 状态管理
|
||||
├── .env.development # 开发环境配置
|
||||
├── .env.production # 生产环境配置
|
||||
└── vite.config.js # Vite 配置
|
||||
```
|
||||
|
||||
## 🚀 本地开发
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js >= 18
|
||||
- npm >= 9
|
||||
|
||||
### 安装依赖
|
||||
## Docker 部署
|
||||
|
||||
```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
|
||||
---
|
||||
|
||||
### 生产构建
|
||||
|
||||
```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
|
||||
**最后更新**: 2024-12-29
|
||||
|
||||
181
doc/裁判邀请功能使用说明.md
Normal file
181
doc/裁判邀请功能使用说明.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# 裁判邀请功能使用说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
裁判邀请模块用于为武术赛事邀请裁判,通过生成邀请码的方式让裁判登录系统并回复邀请。
|
||||
|
||||
## 完整操作流程
|
||||
|
||||
### 1. 准备工作
|
||||
|
||||
#### 1.1 创建赛事
|
||||
- 进入"赛事管理"模块
|
||||
- 创建新的赛事
|
||||
- 确保赛事状态为"进行中"
|
||||
|
||||
#### 1.2 添加裁判
|
||||
- 进入"评委管理"模块
|
||||
- 点击"新增评委"按钮
|
||||
- 填写裁判基本信息:
|
||||
- 姓名、性别、手机号、身份证号
|
||||
- 裁判类型(主裁判/普通裁判)
|
||||
- 等级/职称
|
||||
- 擅长项目
|
||||
- 保存裁判信息
|
||||
|
||||
### 2. 生成邀请码
|
||||
|
||||
#### 方式一:从评委库导入(推荐)
|
||||
|
||||
这是为新裁判生成邀请码的主要方式。
|
||||
|
||||
**操作步骤:**
|
||||
1. 进入"裁判邀请"页面
|
||||
2. 选择赛事(页面顶部下拉框)
|
||||
3. 点击"从评委库导入"按钮
|
||||
4. 在弹出的对话框中:
|
||||
- 可以搜索裁判(按姓名、手机号、类型)
|
||||
- 勾选需要邀请的裁判(支持多选)
|
||||
- 查看已选择的裁判数量
|
||||
5. 点击"确定导入"按钮
|
||||
6. 系统自动为选中的裁判批量生成邀请码
|
||||
7. 生成成功后,邀请列表会自动刷新
|
||||
|
||||
**特点:**
|
||||
- ✅ 支持批量操作
|
||||
- ✅ 可以搜索和筛选裁判
|
||||
- ✅ 自动生成邀请码
|
||||
- ✅ 适合首次邀请裁判
|
||||
|
||||
#### 方式二:批量生成邀请码
|
||||
|
||||
用于为已有邀请记录但未生成邀请码的裁判批量生成。
|
||||
|
||||
**操作步骤:**
|
||||
1. 在邀请列表中勾选需要生成邀请码的记录
|
||||
2. 点击"批量生成邀请码"按钮
|
||||
3. 确认操作
|
||||
4. 系统为选中的裁判生成邀请码
|
||||
|
||||
**注意:**
|
||||
- ⚠️ 只能为已有邀请记录的裁判生成
|
||||
- ⚠️ 如果是新裁判,请使用"从评委库导入"
|
||||
|
||||
#### 方式三:单个生成邀请码
|
||||
|
||||
用于为单个裁判生成或重新生成邀请码。
|
||||
|
||||
**操作步骤:**
|
||||
1. 在邀请列表中找到目标裁判
|
||||
2. 如果未生成邀请码:点击"生成邀请码"按钮
|
||||
3. 如果已有邀请码:点击邀请码旁边的刷新图标"重新生成"
|
||||
4. 邀请码会自动复制到剪贴板
|
||||
|
||||
### 3. 发送邀请
|
||||
|
||||
生成邀请码后,需要将邀请码发送给裁判:
|
||||
|
||||
**发送方式:**
|
||||
- 📧 邮件:将邀请码通过邮件发送
|
||||
- 📱 短信:将邀请码通过短信发送
|
||||
- 💬 微信/其他:通过即时通讯工具发送
|
||||
|
||||
**邀请码使用:**
|
||||
- 裁判收到邀请码后,访问系统登录页面
|
||||
- 输入邀请码进行登录
|
||||
- 查看赛事信息并回复邀请(接受/拒绝)
|
||||
|
||||
### 4. 管理邀请
|
||||
|
||||
#### 4.1 查看邀请状态
|
||||
|
||||
邀请状态说明:
|
||||
- 🟡 **待回复**:已发送邀请,裁判尚未回复
|
||||
- 🟢 **已接受**:裁判已接受邀请
|
||||
- 🔴 **已拒绝**:裁判已拒绝邀请
|
||||
- ⚪ **已取消**:管理员已取消邀请
|
||||
|
||||
#### 4.2 邀请操作
|
||||
|
||||
**对于"待回复"状态的邀请:**
|
||||
- **重发**:重新发送邀请通知
|
||||
- **提醒**:发送提醒消息催促裁判回复
|
||||
- **取消**:取消邀请(需填写取消原因)
|
||||
|
||||
**对于"已接受"状态的邀请:**
|
||||
- **确认**:确认裁判参与(可进行后续的场地、项目分配)
|
||||
|
||||
**所有邀请:**
|
||||
- **查看**:查看邀请详细信息
|
||||
- **复制邀请码**:点击邀请码即可复制
|
||||
|
||||
### 5. 统计信息
|
||||
|
||||
页面顶部显示四个统计卡片:
|
||||
- 📊 **总邀请数**:已发送的邀请总数
|
||||
- ⏰ **待回复**:等待裁判回复的邀请数
|
||||
- ✅ **已接受**:裁判已接受的邀请数
|
||||
- ❌ **已拒绝**:裁判已拒绝的邀请数
|
||||
|
||||
### 6. 导出数据
|
||||
|
||||
点击"导出数据"按钮可以导出当前筛选条件下的邀请列表为Excel文件。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么"批量生成邀请码"按钮是灰色的?
|
||||
**A:** 可能的原因:
|
||||
1. 未选择赛事
|
||||
2. 赛事列表正在加载
|
||||
3. 未勾选任何邀请记录
|
||||
|
||||
### Q2: 如何为新裁判生成邀请码?
|
||||
**A:** 使用"从评委库导入"功能:
|
||||
1. 先在"评委管理"中添加裁判
|
||||
2. 在"裁判邀请"页面点击"从评委库导入"
|
||||
3. 选择裁判并确认导入
|
||||
|
||||
### Q3: 邀请码可以重复使用吗?
|
||||
**A:** 不可以。每个邀请码只能使用一次。如果需要重新邀请,请使用"重新生成"功能。
|
||||
|
||||
### Q4: 邀请码有效期是多久?
|
||||
**A:** 默认有效期为30天。过期后需要重新生成。
|
||||
|
||||
### Q5: 如何知道裁判是否收到邀请?
|
||||
**A:**
|
||||
- 查看邀请状态,如果裁判已登录并回复,状态会更新
|
||||
- 可以使用"提醒"功能发送提醒消息
|
||||
- 建议通过电话或其他方式确认裁判是否收到
|
||||
|
||||
## 技术说明
|
||||
|
||||
### 邀请码生成规则
|
||||
- 每个赛事+裁判组合生成唯一邀请码
|
||||
- 邀请码包含角色信息(主裁判/普通裁判)
|
||||
- 可以预分配场地和项目
|
||||
|
||||
### 数据关联
|
||||
```
|
||||
赛事 (Competition)
|
||||
↓
|
||||
邀请记录 (JudgeInvite)
|
||||
↓
|
||||
裁判 (Judge)
|
||||
```
|
||||
|
||||
### API接口
|
||||
- 生成邀请码:`POST /api/blade-martial/judgeInvite/generate`
|
||||
- 批量生成:`POST /api/blade-martial/judgeInvite/generate/batch`
|
||||
- 重新生成:`PUT /api/blade-martial/judgeInvite/regenerate/{inviteId}`
|
||||
- 邀请列表:`GET /api/blade-martial/judgeInvite/list`
|
||||
- 邀请统计:`GET /api/blade-martial/judgeInvite/statistics`
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2025-12-13
|
||||
- ✨ 新增"从评委库导入"功能
|
||||
- ✨ 支持裁判搜索和筛选
|
||||
- ✨ 优化邀请码生成流程
|
||||
- 🐛 修复按钮禁用逻辑问题
|
||||
- 🐛 修复赛事选择初始化问题
|
||||
- 💄 优化用户界面和交互体验
|
||||
260
doc/赛事列表加载问题排查指南.md
Normal file
260
doc/赛事列表加载问题排查指南.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# 赛事列表加载问题排查指南
|
||||
|
||||
## 问题现象
|
||||
|
||||
- 赛事下拉框显示"无数据"
|
||||
- 所有按钮(发送邀请、批量生成邀请码等)无法点击
|
||||
- 页面显示"暂无数据"
|
||||
|
||||
## 排查步骤
|
||||
|
||||
### 1. 检查浏览器控制台
|
||||
|
||||
打开浏览器开发者工具(F12),查看Console标签页:
|
||||
|
||||
**查看日志输出:**
|
||||
```
|
||||
赛事列表API返回: {...}
|
||||
解析后的赛事列表: [...]
|
||||
```
|
||||
|
||||
**可能的错误信息:**
|
||||
- `加载赛事列表失败: Network Error` - 网络连接问题
|
||||
- `加载赛事列表失败: 404` - API路径错误
|
||||
- `加载赛事列表失败: 500` - 后端服务错误
|
||||
|
||||
### 2. 检查Network请求
|
||||
|
||||
在开发者工具的Network标签页中:
|
||||
|
||||
1. 刷新页面
|
||||
2. 找到 `/api/martial/competition/list` 请求
|
||||
3. 查看请求状态:
|
||||
- **200**: 请求成功,检查返回数据
|
||||
- **404**: API路径不存在
|
||||
- **500**: 后端服务错误
|
||||
- **Failed**: 网络连接失败
|
||||
|
||||
4. 查看Response数据结构:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"records": [...], // 赛事列表
|
||||
"total": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 检查后端服务
|
||||
|
||||
#### 3.1 确认后端服务是否启动
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
netstat -ano | findstr 8888
|
||||
```
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
netstat -tuln | grep 8888
|
||||
```
|
||||
|
||||
如果没有输出,说明后端服务未启动。
|
||||
|
||||
#### 3.2 启动后端服务
|
||||
|
||||
进入后端项目目录:
|
||||
```bash
|
||||
cd martial-master
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
或者使用IDE(IDEA/Eclipse)启动。
|
||||
|
||||
### 4. 检查数据库数据
|
||||
|
||||
#### 4.1 连接数据库
|
||||
|
||||
使用数据库客户端(Navicat、DBeaver等)连接:
|
||||
- Host: 127.0.0.1
|
||||
- Port: 3306
|
||||
- Database: martial_db
|
||||
- Username: root
|
||||
- Password: 123456
|
||||
|
||||
#### 4.2 执行检查SQL
|
||||
|
||||
运行项目根目录下的 `check_data.sql` 文件:
|
||||
|
||||
```sql
|
||||
-- 检查赛事数据
|
||||
SELECT * FROM martial_competition LIMIT 10;
|
||||
|
||||
-- 检查赛事总数
|
||||
SELECT COUNT(*) FROM martial_competition;
|
||||
```
|
||||
|
||||
**如果赛事表为空:**
|
||||
需要先创建赛事数据。
|
||||
|
||||
### 5. 创建测试数据
|
||||
|
||||
#### 5.1 创建赛事
|
||||
|
||||
进入"赛事管理"页面,点击"新增赛事",填写:
|
||||
- 赛事名称:测试赛事
|
||||
- 赛事编码:TEST001
|
||||
- 主办单位:测试单位
|
||||
- 地区:北京
|
||||
- 详细地点:测试地点
|
||||
- 报名时间:选择日期范围
|
||||
- 比赛时间:选择日期范围
|
||||
|
||||
保存后,赛事列表应该就能显示了。
|
||||
|
||||
#### 5.2 创建裁判
|
||||
|
||||
进入"评委管理"页面,点击"新增评委",填写:
|
||||
- 姓名:张三
|
||||
- 性别:男
|
||||
- 手机号:13800138000
|
||||
- 身份证号:110101199001011234
|
||||
- 裁判类型:普通裁判
|
||||
- 等级/职称:一级
|
||||
|
||||
### 6. 检查API配置
|
||||
|
||||
#### 6.1 检查前端API配置
|
||||
|
||||
查看 `martial-web/src/axios/index.js` 或类似文件:
|
||||
|
||||
```javascript
|
||||
const baseURL = process.env.VUE_APP_API_BASE_URL || '/api'
|
||||
```
|
||||
|
||||
确认API基础路径配置正确。
|
||||
|
||||
#### 6.2 检查代理配置
|
||||
|
||||
查看 `martial-web/vite.config.js` 或 `vue.config.js`:
|
||||
|
||||
```javascript
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8888',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
确认代理配置指向正确的后端地址。
|
||||
|
||||
### 7. 常见问题解决
|
||||
|
||||
#### 问题1: CORS跨域错误
|
||||
|
||||
**错误信息:**
|
||||
```
|
||||
Access to XMLHttpRequest at 'http://localhost:8888/api/...' from origin 'http://localhost:5173' has been blocked by CORS policy
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
1. 检查后端CORS配置
|
||||
2. 使用代理配置
|
||||
3. 确保前后端端口配置正确
|
||||
|
||||
#### 问题2: 404 Not Found
|
||||
|
||||
**可能原因:**
|
||||
1. API路径错误
|
||||
2. 后端Controller路径配置错误
|
||||
3. 后端服务未启动
|
||||
|
||||
**解决方案:**
|
||||
1. 检查API路径:`/api/martial/competition/list`
|
||||
2. 检查后端Controller注解:`@RequestMapping("/martial/competition")`
|
||||
3. 启动后端服务
|
||||
|
||||
#### 问题3: 数据结构不匹配
|
||||
|
||||
**现象:**
|
||||
API返回数据,但前端解析失败。
|
||||
|
||||
**解决方案:**
|
||||
查看控制台日志,确认数据结构:
|
||||
```javascript
|
||||
console.log('赛事列表API返回:', res)
|
||||
console.log('解析后的赛事列表:', records)
|
||||
```
|
||||
|
||||
如果数据结构不对,修改前端解析逻辑。
|
||||
|
||||
### 8. 快速测试
|
||||
|
||||
#### 8.1 使用Postman测试API
|
||||
|
||||
**请求:**
|
||||
```
|
||||
GET http://localhost:8888/api/martial/competition/list?current=1&size=10
|
||||
```
|
||||
|
||||
**期望返回:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": 1,
|
||||
"competitionName": "测试赛事",
|
||||
"status": 1,
|
||||
...
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 8.2 使用curl测试
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8888/api/martial/competition/list?current=1&size=10"
|
||||
```
|
||||
|
||||
### 9. 临时解决方案
|
||||
|
||||
如果急需使用,可以临时修改代码,手动添加测试数据:
|
||||
|
||||
```javascript
|
||||
// 在 loadCompetitionList 函数中添加
|
||||
competitionList.value = [
|
||||
{
|
||||
id: 1,
|
||||
competitionName: '测试赛事',
|
||||
status: 1
|
||||
}
|
||||
]
|
||||
queryParams.competitionId = 1
|
||||
```
|
||||
|
||||
**注意:** 这只是临时方案,不要提交到代码库。
|
||||
|
||||
### 10. 联系支持
|
||||
|
||||
如果以上步骤都无法解决问题,请提供以下信息:
|
||||
|
||||
1. 浏览器控制台完整错误日志
|
||||
2. Network请求的详细信息(Request/Response)
|
||||
3. 后端服务日志
|
||||
4. 数据库查询结果
|
||||
|
||||
## 修改记录
|
||||
|
||||
### 2025-12-13
|
||||
- ✨ 添加详细的日志输出
|
||||
- ✨ 优化数据解析逻辑,支持多种返回格式
|
||||
- ✨ 移除status=1的限制,查询所有赛事
|
||||
- 📝 创建排查指南文档
|
||||
46
package-lock.json
generated
46
package-lock.json
generated
@@ -851,7 +851,6 @@
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -874,7 +873,6 @@
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
|
||||
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@transloadit/prettier-bytes": "0.0.7",
|
||||
"@uppy/store-default": "^2.1.1",
|
||||
@@ -903,7 +901,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
|
||||
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@uppy/companion-client": "^2.2.2",
|
||||
"@uppy/utils": "^4.1.2",
|
||||
@@ -1058,7 +1055,6 @@
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
|
||||
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-url": "^1.2.4"
|
||||
},
|
||||
@@ -1089,7 +1085,6 @@
|
||||
"version": "1.1.19",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
|
||||
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/event-emitter": "^0.3.3",
|
||||
"event-emitter": "^0.3.5",
|
||||
@@ -1119,7 +1114,6 @@
|
||||
"version": "5.1.23",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
|
||||
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@uppy/core": "^2.1.1",
|
||||
"@uppy/xhr-upload": "^2.0.3",
|
||||
@@ -1476,7 +1470,6 @@
|
||||
"version": "11.13.1",
|
||||
"resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-11.13.1.tgz",
|
||||
"integrity": "sha512-6kO0rBN6aBIQiMELfv1oX2Ohes/brlIPuOVZUYAioeWM0EyuazhAXgHeq8iKFt29daU9NGRr4n78esGx8QjtjQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bpmn-io/diagram-js-ui": "^0.2.2",
|
||||
"clsx": "^1.2.1",
|
||||
@@ -1514,7 +1507,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
|
||||
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ssr-window": "^3.0.0-alpha.1"
|
||||
}
|
||||
@@ -1540,7 +1532,6 @@
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz",
|
||||
"integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.1",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
@@ -1866,8 +1857,7 @@
|
||||
"node_modules/is-hotkey": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
|
||||
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
|
||||
"peer": true
|
||||
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
@@ -1942,14 +1932,12 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"peer": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"peer": true
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -1964,44 +1952,37 @@
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"peer": true
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||
"peer": true
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||
},
|
||||
"node_modules/lodash.foreach": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
|
||||
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"node_modules/lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
|
||||
},
|
||||
"node_modules/lodash.toarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
|
||||
"peer": true
|
||||
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw=="
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.10",
|
||||
@@ -2116,7 +2097,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -2308,7 +2288,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
|
||||
"integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
@@ -2367,7 +2346,6 @@
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz",
|
||||
"integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
@@ -2403,7 +2381,6 @@
|
||||
"version": "0.72.8",
|
||||
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
|
||||
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"immer": "^9.0.6",
|
||||
"is-plain-object": "^5.0.0",
|
||||
@@ -2425,7 +2402,6 @@
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz",
|
||||
"integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
}
|
||||
@@ -2623,7 +2599,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
|
||||
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.1",
|
||||
"postcss": "^8.4.38",
|
||||
@@ -2791,7 +2766,6 @@
|
||||
"version": "3.4.27",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz",
|
||||
"integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.4.27",
|
||||
"@vue/compiler-sfc": "3.4.27",
|
||||
|
||||
@@ -183,3 +183,26 @@ export const saveDispatch = (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'
|
||||
})
|
||||
}
|
||||
|
||||
136
src/api/martial/attachment.js
Normal file
136
src/api/martial/attachment.js
Normal 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: '图片直播'
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export const getDeductionDetail = (id) => {
|
||||
*/
|
||||
export const addDeduction = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/deductionItem/save',
|
||||
url: '/api/blade-martial/deductionItem/submit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -57,7 +57,7 @@ export const addDeduction = (data) => {
|
||||
*/
|
||||
export const updateDeduction = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/deductionItem/update',
|
||||
url: '/api/blade-martial/deductionItem/submit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ export const getDeductionDetail = (id) => {
|
||||
*/
|
||||
export const addDeduction = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/deductionItem/save',
|
||||
url: '/api/blade-martial/deductionItem/submit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -57,7 +57,7 @@ export const addDeduction = (data) => {
|
||||
*/
|
||||
export const updateDeduction = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/deductionItem/update',
|
||||
url: '/api/blade-martial/deductionItem/submit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ import request from '@/axios';
|
||||
*/
|
||||
export const getJudgeInviteList = (current, size, params) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/list',
|
||||
url: '/api/martial/judgeInvite/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
current,
|
||||
@@ -30,7 +30,7 @@ export const getJudgeInviteList = (current, size, params) => {
|
||||
*/
|
||||
export const getJudgeInviteDetail = (id) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/detail',
|
||||
url: '/api/martial/judgeInvite/detail',
|
||||
method: 'get',
|
||||
params: { id }
|
||||
})
|
||||
@@ -48,7 +48,7 @@ export const getJudgeInviteDetail = (id) => {
|
||||
*/
|
||||
export const sendInvite = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/send',
|
||||
url: '/api/martial/judgeInvite/send',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -60,7 +60,7 @@ export const sendInvite = (data) => {
|
||||
*/
|
||||
export const updateInvite = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/update',
|
||||
url: '/api/martial/judgeInvite/submit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -72,7 +72,7 @@ export const updateInvite = (data) => {
|
||||
*/
|
||||
export const removeInvite = (ids) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/remove',
|
||||
url: '/api/martial/judgeInvite/remove',
|
||||
method: 'post',
|
||||
params: { ids }
|
||||
})
|
||||
@@ -87,7 +87,7 @@ export const removeInvite = (ids) => {
|
||||
*/
|
||||
export const batchSendInvites = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/batch-send',
|
||||
url: '/api/martial/judgeInvite/batch-send',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -99,7 +99,7 @@ export const batchSendInvites = (data) => {
|
||||
*/
|
||||
export const resendInvite = (id) => {
|
||||
return request({
|
||||
url: `/api/blade-martial/judgeInvite/resend/${id}`,
|
||||
url: `/api/martial/judgeInvite/resend/${id}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export const resendInvite = (id) => {
|
||||
*/
|
||||
export const replyInvite = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/reply',
|
||||
url: '/api/martial/judgeInvite/reply',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -127,7 +127,7 @@ export const replyInvite = (data) => {
|
||||
*/
|
||||
export const cancelInvite = (id, reason) => {
|
||||
return request({
|
||||
url: `/api/blade-martial/judgeInvite/cancel/${id}`,
|
||||
url: `/api/martial/judgeInvite/cancel/${id}`,
|
||||
method: 'post',
|
||||
params: { reason }
|
||||
})
|
||||
@@ -139,7 +139,7 @@ export const cancelInvite = (id, reason) => {
|
||||
*/
|
||||
export const confirmInvite = (id) => {
|
||||
return request({
|
||||
url: `/api/blade-martial/judgeInvite/confirm/${id}`,
|
||||
url: `/api/martial/judgeInvite/confirm/${id}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@@ -150,7 +150,7 @@ export const confirmInvite = (id) => {
|
||||
*/
|
||||
export const getInviteStatistics = (competitionId) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/statistics',
|
||||
url: '/api/martial/judgeInvite/statistics',
|
||||
method: 'get',
|
||||
params: { competitionId }
|
||||
})
|
||||
@@ -162,7 +162,7 @@ export const getInviteStatistics = (competitionId) => {
|
||||
*/
|
||||
export const getAcceptedJudges = (competitionId) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/accepted-judges',
|
||||
url: '/api/martial/judgeInvite/accepted-judges',
|
||||
method: 'get',
|
||||
params: { competitionId }
|
||||
})
|
||||
@@ -175,7 +175,7 @@ export const getAcceptedJudges = (competitionId) => {
|
||||
*/
|
||||
export const importFromJudgePool = (competitionId, judgeIds) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/import/pool',
|
||||
url: '/api/martial/judgeInvite/import/pool',
|
||||
method: 'post',
|
||||
params: { competitionId, judgeIds }
|
||||
})
|
||||
@@ -187,7 +187,7 @@ export const importFromJudgePool = (competitionId, judgeIds) => {
|
||||
*/
|
||||
export const exportInvites = (params) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/export',
|
||||
url: '/api/martial/judgeInvite/export',
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob'
|
||||
@@ -201,7 +201,7 @@ export const exportInvites = (params) => {
|
||||
*/
|
||||
export const sendReminder = (id, message) => {
|
||||
return request({
|
||||
url: `/api/blade-martial/judgeInvite/reminder/${id}`,
|
||||
url: `/api/martial/judgeInvite/reminder/${id}`,
|
||||
method: 'post',
|
||||
params: { message }
|
||||
})
|
||||
@@ -219,7 +219,7 @@ export const sendReminder = (id, message) => {
|
||||
*/
|
||||
export const generateInviteCode = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/generate',
|
||||
url: '/api/martial/judgeInvite/generate',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -235,7 +235,7 @@ export const generateInviteCode = (data) => {
|
||||
*/
|
||||
export const batchGenerateInviteCode = (data) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/generate/batch',
|
||||
url: '/api/martial/judgeInvite/generate/batch',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -247,7 +247,7 @@ export const batchGenerateInviteCode = (data) => {
|
||||
*/
|
||||
export const regenerateInviteCode = (inviteId) => {
|
||||
return request({
|
||||
url: `/api/blade-martial/judgeInvite/regenerate/${inviteId}`,
|
||||
url: `/api/martial/judgeInvite/regenerate/${inviteId}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
@@ -259,7 +259,7 @@ export const regenerateInviteCode = (inviteId) => {
|
||||
*/
|
||||
export const getInviteByJudge = (competitionId, judgeId) => {
|
||||
return request({
|
||||
url: '/api/blade-martial/judgeInvite/byJudge',
|
||||
url: '/api/martial/judgeInvite/byJudge',
|
||||
method: 'get',
|
||||
params: { competitionId, judgeId }
|
||||
})
|
||||
|
||||
@@ -141,3 +141,16 @@ export const getOrderAmountStats = (orderId) => {
|
||||
params: { orderId }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取单位统计
|
||||
* @param {Number} competitionId - 赛事ID
|
||||
*/
|
||||
export const getOrganizationStats = (competitionId) => {
|
||||
return request({
|
||||
url: '/api/martial/registrationOrder/organization-stats',
|
||||
method: 'get',
|
||||
params: { competitionId }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import request from '@/axios';
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {Number} params.competitionId - 赛事ID
|
||||
* @param {String} params.projectName - 项目名称(可选)
|
||||
* @param {String} params.category - 分组类别(可选)
|
||||
* @param {Number} params.category - 分组类别(可选)
|
||||
* @param {Number} params.eventType - 项目类型(可选)
|
||||
* @param {Number} params.participantType - 参赛类型(可选,1-单人,2-集体)
|
||||
*/
|
||||
export const getProjectList = (current, size, params) => {
|
||||
return request({
|
||||
@@ -65,17 +67,16 @@ export const updateProject = (data) => {
|
||||
* @param {Number} data.competitionId - 赛事ID
|
||||
* @param {String} data.projectName - 项目名称
|
||||
* @param {String} data.projectCode - 项目编码
|
||||
* @param {String} data.category - 组别(男子组/女子组)
|
||||
* @param {Number} data.type - 类型(1-个人,2-双人,3-集体)
|
||||
* @param {Number} data.minParticipants - 最少参赛人数
|
||||
* @param {Number} data.maxParticipants - 最多参赛人数
|
||||
* @param {Number} data.minAge - 最小年龄
|
||||
* @param {Number} data.maxAge - 最大年龄
|
||||
* @param {Number} data.genderLimit - 性别限制(0-不限,1-仅男,2-仅女)
|
||||
* @param {Number} data.estimatedDuration - 预估时长(分钟)
|
||||
* @param {Number} data.price - 报名费用
|
||||
* @param {Number} data.difficultyCoefficient - 难度系数
|
||||
* @param {String} data.description - 项目描述
|
||||
* @param {Number} data.category - 分组类别(1-男子,2-女子,3-团体,4-混合)
|
||||
* @param {Number} data.eventType - 项目类型(1-套路,2-散打,3-器械,4-对练)
|
||||
* @param {Number} data.participantType - 参赛类型(1-单人,2-集体)
|
||||
* @param {Number} data.registrationFee - 报名费用
|
||||
* @param {String} data.registrationStartTime - 报名开始时间
|
||||
* @param {String} data.registrationEndTime - 报名结束时间
|
||||
* @param {Number} data.maxParticipants - 最大参赛人数
|
||||
* @param {Number} data.sortOrder - 排序序号
|
||||
* @param {String} data.rules - 比赛规则
|
||||
* @param {String} data.remark - 备注
|
||||
*/
|
||||
export const submitProject = (data) => {
|
||||
return request({
|
||||
|
||||
@@ -9,7 +9,7 @@ import request from '@/axios';
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {String} params.name - 裁判姓名
|
||||
* @param {String} params.phone - 手机号
|
||||
* @param {Number} params.refereeType - 裁判类型(1-裁判长,2-普通裁判)
|
||||
* @param {Number} params.refereeType - 裁判类型(1-主裁判,2-裁判员)
|
||||
*/
|
||||
export const getRefereeList = (current, size, params) => {
|
||||
return request({
|
||||
@@ -43,7 +43,7 @@ export const getRefereeDetail = (id) => {
|
||||
* @param {Number} data.gender - 性别(1-男,2-女)
|
||||
* @param {String} data.phone - 手机号
|
||||
* @param {String} data.idCard - 身份证号
|
||||
* @param {Number} data.refereeType - 裁判类型(1-裁判长,2-普通裁判)
|
||||
* @param {Number} data.refereeType - 裁判类型(1-主裁判,2-裁判员)
|
||||
* @param {String} data.level - 等级/职称
|
||||
* @param {String} data.specialty - 擅长项目
|
||||
* @param {String} data.photoUrl - 照片URL
|
||||
|
||||
@@ -194,3 +194,27 @@ export const exportSchedulePlans = (params) => {
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新参赛者签到状态
|
||||
* @param {Number} participantId - 参赛者ID
|
||||
* @param {String} status - 状态:未签到/已签到/异常
|
||||
*/
|
||||
export const updateCheckInStatus = (participantId, status) => {
|
||||
return request({
|
||||
url: '/api/martial/schedule/update-check-in-status',
|
||||
method: 'post',
|
||||
data: { participantId, status }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取赛程配置
|
||||
* @returns {Promise} 返回赛程配置信息
|
||||
*/
|
||||
export const getScheduleConfig = () => {
|
||||
return request({
|
||||
url: '/api/martial/schedule/config',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import request from '@/axios';
|
||||
|
||||
export const getList = (current, size, params) => {
|
||||
return request({
|
||||
url: '/blade-system/menu/list',
|
||||
url: '/api/blade-system/menu/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
@@ -14,7 +14,7 @@ export const getList = (current, size, params) => {
|
||||
|
||||
export const getLazyList = (parentId, params) => {
|
||||
return request({
|
||||
url: '/blade-system/menu/lazy-list',
|
||||
url: '/api/blade-system/menu/lazy-list',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
@@ -25,7 +25,7 @@ export const getLazyList = (parentId, params) => {
|
||||
|
||||
export const getLazyMenuList = (parentId, params) => {
|
||||
return request({
|
||||
url: '/blade-system/menu/lazy-menu-list',
|
||||
url: '/api/blade-system/menu/lazy-menu-list',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
@@ -36,7 +36,7 @@ export const getLazyMenuList = (parentId, params) => {
|
||||
|
||||
export const getMenuList = (current, size, params) => {
|
||||
return request({
|
||||
url: '/blade-system/menu/menu-list',
|
||||
url: '/api/blade-system/menu/menu-list',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
@@ -48,7 +48,7 @@ export const getMenuList = (current, size, params) => {
|
||||
|
||||
export const getMenuTree = tenantId => {
|
||||
return request({
|
||||
url: '/blade-system/menu/tree',
|
||||
url: '/api/blade-system/menu/tree',
|
||||
method: 'get',
|
||||
params: {
|
||||
tenantId,
|
||||
@@ -58,7 +58,7 @@ export const getMenuTree = tenantId => {
|
||||
|
||||
export const remove = ids => {
|
||||
return request({
|
||||
url: '/blade-system/menu/remove',
|
||||
url: '/api/blade-system/menu/remove',
|
||||
method: 'post',
|
||||
params: {
|
||||
ids,
|
||||
@@ -68,7 +68,7 @@ export const remove = ids => {
|
||||
|
||||
export const add = row => {
|
||||
return request({
|
||||
url: '/blade-system/menu/submit',
|
||||
url: '/api/blade-system/menu/submit',
|
||||
method: 'post',
|
||||
data: row,
|
||||
});
|
||||
@@ -76,7 +76,7 @@ export const add = row => {
|
||||
|
||||
export const update = row => {
|
||||
return request({
|
||||
url: '/blade-system/menu/submit',
|
||||
url: '/api/blade-system/menu/submit',
|
||||
method: 'post',
|
||||
data: row,
|
||||
});
|
||||
@@ -84,7 +84,7 @@ export const update = row => {
|
||||
|
||||
export const getMenu = id => {
|
||||
return request({
|
||||
url: '/blade-system/menu/detail',
|
||||
url: '/api/blade-system/menu/detail',
|
||||
method: 'get',
|
||||
params: {
|
||||
id,
|
||||
@@ -94,13 +94,13 @@ export const getMenu = id => {
|
||||
|
||||
export const getTopMenu = () =>
|
||||
request({
|
||||
url: '/blade-system/menu/top-menu',
|
||||
url: '/api/blade-system/menu/top-menu',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
export const getRoutes = topMenuId =>
|
||||
request({
|
||||
url: '/blade-system/menu/routes',
|
||||
url: '/api/blade-system/menu/routes',
|
||||
method: 'get',
|
||||
params: {
|
||||
topMenuId,
|
||||
|
||||
@@ -4,7 +4,7 @@ import website from '@/config/website';
|
||||
|
||||
export const loginByUsername = (tenantId, deptId, roleId, username, password, type, key, code) =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/token',
|
||||
url: '/api/blade-auth/oauth/token',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Tenant-Id': tenantId,
|
||||
@@ -25,7 +25,7 @@ export const loginByUsername = (tenantId, deptId, roleId, username, password, ty
|
||||
|
||||
export const loginBySocial = (tenantId, source, code, state) =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/token',
|
||||
url: '/api/blade-auth/oauth/token',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Tenant-Id': tenantId,
|
||||
@@ -42,7 +42,7 @@ export const loginBySocial = (tenantId, source, code, state) =>
|
||||
|
||||
export const loginBySso = (state, code) =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/token',
|
||||
url: '/api/blade-auth/oauth/token',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Tenant-Id': state,
|
||||
@@ -58,7 +58,7 @@ export const loginBySso = (state, code) =>
|
||||
|
||||
export const refreshToken = (refresh_token, tenantId, deptId, roleId) =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/token',
|
||||
url: '/api/blade-auth/oauth/token',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Tenant-Id': tenantId,
|
||||
@@ -75,7 +75,7 @@ export const refreshToken = (refresh_token, tenantId, deptId, roleId) =>
|
||||
|
||||
export const registerUser = (tenantId, name, account, password, phone, email) =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/token',
|
||||
url: '/api/blade-auth/oauth/token',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Tenant-Id': tenantId,
|
||||
@@ -94,7 +94,7 @@ export const registerUser = (tenantId, name, account, password, phone, email) =>
|
||||
|
||||
export const registerGuest = (form, oauthId) =>
|
||||
request({
|
||||
url: '/blade-system/user/register-guest',
|
||||
url: '/api/blade-system/user/register-guest',
|
||||
method: 'post',
|
||||
params: {
|
||||
tenantId: form.tenantId,
|
||||
@@ -107,40 +107,40 @@ export const registerGuest = (form, oauthId) =>
|
||||
|
||||
export const getButtons = () =>
|
||||
request({
|
||||
url: '/blade-system/menu/buttons',
|
||||
url: '/api/blade-system/menu/buttons',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
export const getCaptcha = () =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/captcha',
|
||||
url: '/api/blade-auth/oauth/captcha',
|
||||
method: 'get',
|
||||
authorization: false,
|
||||
});
|
||||
|
||||
export const logout = () =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/logout',
|
||||
url: '/api/blade-auth/oauth/logout',
|
||||
method: 'get',
|
||||
authorization: false,
|
||||
});
|
||||
|
||||
export const getUserInfo = () =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/user-info',
|
||||
url: '/api/blade-auth/oauth/user-info',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
export const sendLogs = list =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/logout',
|
||||
url: '/api/blade-auth/oauth/logout',
|
||||
method: 'post',
|
||||
data: list,
|
||||
});
|
||||
|
||||
export const clearCache = () =>
|
||||
request({
|
||||
url: '/blade-auth/oauth/clear-cache',
|
||||
url: '/api/blade-auth/oauth/clear-cache',
|
||||
method: 'get',
|
||||
authorization: false,
|
||||
});
|
||||
|
||||
@@ -98,6 +98,10 @@ axios.interceptors.request.use(
|
||||
axios.interceptors.response.use(
|
||||
res => {
|
||||
NProgress.done();
|
||||
// 如果是 blob 类型响应(文件下载),直接返回
|
||||
if (res.config.responseType === 'blob') {
|
||||
return res;
|
||||
}
|
||||
const status = res.data.code || res.status;
|
||||
const statusWhiteList = website.statusWhiteList || [];
|
||||
const message = res.data.msg || res.data.error_description || '系统错误';
|
||||
|
||||
@@ -48,7 +48,7 @@ export default [
|
||||
redirect: '/martial/order/list',
|
||||
children: [
|
||||
{
|
||||
path: 'competition/list',
|
||||
path: 'competition/index',
|
||||
name: '赛事管理',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
@@ -65,7 +65,7 @@ export default [
|
||||
},
|
||||
{
|
||||
path: 'order/list',
|
||||
name: '订单管理',
|
||||
name: '赛事管理',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 活动日程 -->
|
||||
<div class="form-section">
|
||||
<!-- <div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="el-icon-date"></i>
|
||||
活动日程
|
||||
@@ -290,7 +290,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<div class="form-section">
|
||||
@@ -361,23 +361,6 @@
|
||||
</template>
|
||||
</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
|
||||
label="项目说明"
|
||||
|
||||
@@ -370,8 +370,134 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 活动日程 -->
|
||||
<!-- 附件管理 -->
|
||||
<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">支持 PDF、Word、Excel、图片等格式,单个文件不超过 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">
|
||||
<i class="el-icon-date"></i>
|
||||
活动日程
|
||||
@@ -512,7 +638,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<div class="form-section">
|
||||
@@ -583,21 +709,24 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
||||
<el-table-column
|
||||
label="参赛人数限制"
|
||||
label="报名费用(元)"
|
||||
width="130"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-input-number
|
||||
v-if="currentView !== 'view'"
|
||||
v-model="scope.row.maxParticipants"
|
||||
:min="1"
|
||||
:max="9999"
|
||||
v-model="scope.row.price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="10"
|
||||
size="small"
|
||||
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>
|
||||
</el-table-column>
|
||||
|
||||
@@ -781,6 +910,22 @@
|
||||
</el-form>
|
||||
</el-card>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -806,6 +951,15 @@ import {
|
||||
removeVenue,
|
||||
getVenuesByCompetition
|
||||
} 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 {
|
||||
name: 'CompetitionManagement',
|
||||
@@ -820,6 +974,40 @@ export default {
|
||||
size: 10,
|
||||
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: {
|
||||
competitionName: '',
|
||||
competitionCode: '', // 比赛编码
|
||||
@@ -840,7 +1028,15 @@ export default {
|
||||
awards: '',
|
||||
schedule: [],
|
||||
projects: [],
|
||||
venues: []
|
||||
venues: [],
|
||||
attachments: {
|
||||
info: [],
|
||||
rules: [],
|
||||
schedule: [],
|
||||
results: [],
|
||||
medals: [],
|
||||
photos: []
|
||||
}
|
||||
},
|
||||
formRules: {
|
||||
competitionName: [
|
||||
@@ -988,10 +1184,11 @@ export default {
|
||||
try {
|
||||
this.formData = this.formatBackendData(detailData);
|
||||
console.log('格式化后的表单数据:', this.formData);
|
||||
// 加载关联数据:活动日程、项目列表、场地配置
|
||||
// 加载关联数据:活动日程、项目列表、场地配置、附件
|
||||
this.loadActivitySchedules();
|
||||
this.loadProjects();
|
||||
this.loadVenues();
|
||||
this.loadAttachments();
|
||||
} catch (error) {
|
||||
console.error('格式化数据时出错:', error);
|
||||
this.$message.error('数据格式化失败: ' + error.message);
|
||||
@@ -1138,7 +1335,8 @@ export default {
|
||||
venueCode: item.venueCode,
|
||||
capacity: item.capacity,
|
||||
location: item.location,
|
||||
remark: item.facilities || ''
|
||||
remark: item.facilities || '',
|
||||
venueType: item.venueType || 'indoor'
|
||||
}));
|
||||
console.log('✅ 加载的场地列表:', this.formData.venues);
|
||||
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) {
|
||||
const statusMap = {
|
||||
1: '未开始',
|
||||
@@ -1176,7 +1581,7 @@ export default {
|
||||
|
||||
handleCreate() {
|
||||
this.$router.push({
|
||||
path: '/martial/competition/list',
|
||||
path: '/martial/competition/index',
|
||||
query: { mode: 'create' }
|
||||
});
|
||||
},
|
||||
@@ -1184,21 +1589,21 @@ export default {
|
||||
handleView(row) {
|
||||
console.log(row)
|
||||
this.$router.push({
|
||||
path: '/martial/competition/list',
|
||||
path: '/martial/competition/index',
|
||||
query: { mode: 'view', id: row.id }
|
||||
});
|
||||
},
|
||||
|
||||
handleEdit(row) {
|
||||
this.$router.push({
|
||||
path: '/martial/competition/list',
|
||||
path: '/martial/competition/index',
|
||||
query: { mode: 'edit', id: row.id }
|
||||
});
|
||||
},
|
||||
|
||||
switchToEdit() {
|
||||
this.$router.push({
|
||||
path: '/martial/competition/list',
|
||||
path: '/martial/competition/index',
|
||||
query: { mode: 'edit', id: this.competitionId }
|
||||
});
|
||||
},
|
||||
@@ -1247,6 +1652,7 @@ export default {
|
||||
projectCode: '',
|
||||
category: '',
|
||||
maxParticipants: null,
|
||||
price: 0,
|
||||
description: ''
|
||||
});
|
||||
},
|
||||
@@ -1311,6 +1717,9 @@ export default {
|
||||
savePromises.push(this.saveVenues(savedCompetitionId));
|
||||
}
|
||||
|
||||
// 4. 保存附件
|
||||
savePromises.push(this.saveAttachments(savedCompetitionId));
|
||||
|
||||
// 等待所有保存操作完成
|
||||
if (savePromises.length > 0) {
|
||||
Promise.all(savePromises)
|
||||
@@ -1455,7 +1864,8 @@ export default {
|
||||
venueCode: venue.venueCode,
|
||||
capacity: venue.capacity || 100,
|
||||
location: venue.location || '',
|
||||
facilities: venue.remark || ''
|
||||
facilities: venue.remark || '',
|
||||
venueType: venue.venueType || 'indoor'
|
||||
};
|
||||
|
||||
// 如果有 id,说明是编辑已有的场地
|
||||
@@ -1472,7 +1882,7 @@ export default {
|
||||
|
||||
backToList() {
|
||||
this.$router.push({
|
||||
path: '/martial/competition/list'
|
||||
path: '/martial/competition/index'
|
||||
});
|
||||
// 路由跳转后,在 initPage 中会自动加载列表
|
||||
},
|
||||
@@ -1498,7 +1908,15 @@ export default {
|
||||
awards: '',
|
||||
schedule: [],
|
||||
projects: [],
|
||||
venues: []
|
||||
venues: [],
|
||||
attachments: {
|
||||
info: [],
|
||||
rules: [],
|
||||
schedule: [],
|
||||
results: [],
|
||||
medals: [],
|
||||
photos: []
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1789,4 +2207,55 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 活动日程 -->
|
||||
<div class="form-section">
|
||||
<!-- <div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="el-icon-date"></i>
|
||||
活动日程
|
||||
@@ -386,7 +386,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div> -->
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
|
||||
@@ -3,19 +3,47 @@
|
||||
<!-- 搜索区域 -->
|
||||
<el-card shadow="never" class="search-card">
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="选择赛事" required>
|
||||
<el-select
|
||||
v-model="queryParams.competitionId"
|
||||
placeholder="请选择赛事"
|
||||
clearable
|
||||
filterable
|
||||
:loading="competitionLoading"
|
||||
:disabled="competitionLoading"
|
||||
style="width: 250px"
|
||||
@change="handleCompetitionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<div style="padding: 20px; text-align: center; color: #909399;">
|
||||
<p>暂无赛事数据</p>
|
||||
<p style="font-size: 12px; margin-top: 5px;">
|
||||
请先在"赛事管理"中创建赛事
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in competitionList"
|
||||
:key="item.id"
|
||||
:label="item.competitionName"
|
||||
:value="String(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属项目">
|
||||
<el-select
|
||||
v-model="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="!queryParams.competitionId"
|
||||
style="width: 250px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in projectList"
|
||||
:key="item.id"
|
||||
:label="item.projectName"
|
||||
:value="item.id"
|
||||
:value="String(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -40,21 +68,31 @@
|
||||
<el-card shadow="never" class="toolbar-card">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
:disabled="!queryParams.competitionId"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增扣分项
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
:disabled="!selection.length"
|
||||
:disabled="!selection.length || !queryParams.competitionId"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
<el-button type="success" :icon="DocumentCopy" @click="handleClone">
|
||||
<!-- <el-button type="success" :icon="DocumentCopy" @click="handleClone">
|
||||
克隆扣分项
|
||||
</el-button>
|
||||
<el-button type="warning" :icon="Download" @click="handleExport">
|
||||
</el-button> -->
|
||||
<el-button
|
||||
type="warning"
|
||||
:icon="Download"
|
||||
:disabled="!queryParams.competitionId"
|
||||
@click="handleExport"
|
||||
>
|
||||
导出模板
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -65,7 +103,12 @@
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button circle :icon="Refresh" @click="fetchData" />
|
||||
<el-button
|
||||
circle
|
||||
:icon="Refresh"
|
||||
:disabled="!queryParams.competitionId"
|
||||
@click="fetchData"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,14 +146,14 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="deductionPoints"
|
||||
prop="deductionPoint"
|
||||
label="扣分值(分)"
|
||||
width="120"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag type="danger" effect="dark">
|
||||
-{{ row.deductionPoints }}
|
||||
-{{ row.deductionPoint }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -135,9 +178,6 @@
|
||||
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="success" :icon="DocumentCopy" @click="handleCloneSingle(row)">
|
||||
克隆
|
||||
</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
@@ -173,6 +213,22 @@
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="所属赛事" prop="competitionId">
|
||||
<el-select
|
||||
v-model="form.competitionId"
|
||||
placeholder="请选择赛事"
|
||||
filterable
|
||||
disabled
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in competitionList"
|
||||
:key="item.id"
|
||||
:label="item.competitionName"
|
||||
:value="String(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属项目" prop="projectId">
|
||||
<el-select
|
||||
v-model="form.projectId"
|
||||
@@ -184,7 +240,7 @@
|
||||
v-for="item in projectList"
|
||||
:key="item.id"
|
||||
:label="item.projectName"
|
||||
:value="item.id"
|
||||
:value="String(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -195,9 +251,9 @@
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="扣分值(分)" prop="deductionPoints">
|
||||
<el-form-item label="扣分值(分)" prop="deductionPoint">
|
||||
<el-input-number
|
||||
v-model="form.deductionPoints"
|
||||
v-model="form.deductionPoint"
|
||||
:min="0.1"
|
||||
:max="10"
|
||||
:precision="1"
|
||||
@@ -261,7 +317,7 @@
|
||||
v-for="item in projectList.filter(p => p.id !== cloneForm.sourceProjectId)"
|
||||
:key="item.id"
|
||||
:label="item.projectName"
|
||||
:value="item.id"
|
||||
:value="String(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -295,7 +351,6 @@ import {
|
||||
Delete,
|
||||
Edit,
|
||||
Download,
|
||||
DocumentCopy,
|
||||
InfoFilled,
|
||||
Rank
|
||||
} from '@element-plus/icons-vue'
|
||||
@@ -310,6 +365,7 @@ import {
|
||||
import { getProjectList } from '@/api/martial/project'
|
||||
import dayjs from 'dayjs'
|
||||
import Sortable from 'sortablejs'
|
||||
import { getCompetitionList } from '@/api/martial/competition'
|
||||
|
||||
// 数据定义
|
||||
const loading = ref(false)
|
||||
@@ -324,21 +380,25 @@ const cloneDialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref(null)
|
||||
const cloneFormRef = ref(null)
|
||||
const competitionList = ref([])
|
||||
const competitionLoading = ref(false)
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
projectId: '',
|
||||
itemName: ''
|
||||
competitionId: null,
|
||||
projectId: null,
|
||||
itemName: '',
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: null,
|
||||
projectId: '',
|
||||
itemName: '',
|
||||
deductionPoints: 0.5,
|
||||
competitionId: null,
|
||||
projectId: null,
|
||||
itemName: '',
|
||||
deductionPoint: 0.5,
|
||||
sortOrder: 0,
|
||||
description: ''
|
||||
})
|
||||
@@ -353,6 +413,9 @@ const cloneForm = reactive({
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
competitionId: [
|
||||
{ required: true, message: '请选择赛事', trigger: 'change' }
|
||||
],
|
||||
projectId: [
|
||||
{ required: true, message: '请选择所属项目', trigger: 'change' }
|
||||
],
|
||||
@@ -360,7 +423,7 @@ const rules = {
|
||||
{ required: true, message: '请输入扣分项名称', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
deductionPoints: [
|
||||
deductionPoint: [
|
||||
{ required: true, message: '请输入扣分值', trigger: 'blur' }
|
||||
],
|
||||
sortOrder: [
|
||||
@@ -375,31 +438,90 @@ const cloneRules = {
|
||||
]
|
||||
}
|
||||
|
||||
// 加载项目列表
|
||||
const loadProjectList = async () => {
|
||||
// 加载赛事列表
|
||||
const loadCompetitionList = async () => {
|
||||
competitionLoading.value = true
|
||||
try {
|
||||
const res = await getProjectList(1, 1000, {})
|
||||
if (res.data && res.data.records) {
|
||||
projectList.value = res.data.records
|
||||
const resCompe = await getCompetitionList(1, 1000, {})
|
||||
const recordsCompe = resCompe.data?.data?.records || []
|
||||
competitionList.value = recordsCompe
|
||||
|
||||
if (recordsCompe.length === 0) {
|
||||
console.warn('赛事列表为空,请先在"赛事管理"中创建赛事')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载赛事列表失败:', error)
|
||||
ElMessage.error('加载赛事列表失败')
|
||||
} finally {
|
||||
competitionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载项目列表(根据赛事ID)
|
||||
const loadProjectList = async (competitionId) => {
|
||||
if (!competitionId) {
|
||||
projectList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getProjectList(1, 1000, { competitionId })
|
||||
// 根据axios响应拦截器的处理,数据在 res.data.data.records 中
|
||||
const records = res.data?.data?.records || []
|
||||
projectList.value = records
|
||||
|
||||
if (records.length === 0) {
|
||||
console.warn('该赛事下暂无项目,请先在"轮编管理"中创建项目')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载项目列表失败:', error)
|
||||
ElMessage.error('加载项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 赛事选择变化
|
||||
const handleCompetitionChange = (competitionId) => {
|
||||
// 清空项目选择和表格数据
|
||||
queryParams.projectId = null
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
|
||||
// 加载该赛事下的项目
|
||||
if (competitionId) {
|
||||
loadProjectList(competitionId)
|
||||
// 自动查询数据
|
||||
fetchData()
|
||||
} else {
|
||||
projectList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
const fetchData = async () => {
|
||||
// 必须选择赛事才能查询
|
||||
if (!queryParams.competitionId) {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
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(
|
||||
queryParams.current,
|
||||
queryParams.size,
|
||||
queryParams
|
||||
params
|
||||
)
|
||||
if (res.data) {
|
||||
tableData.value = res.data.records || []
|
||||
total.value = res.data.total || 0
|
||||
}
|
||||
// 根据axios响应拦截器的处理,数据在 res.data.data 中
|
||||
const data = res.data?.data || {}
|
||||
tableData.value = data.records || []
|
||||
total.value = data.total || 0
|
||||
} catch (error) {
|
||||
ElMessage.error('获取数据失败')
|
||||
console.error(error)
|
||||
@@ -416,28 +538,48 @@ const handleSearch = () => {
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
const competitionId = queryParams.competitionId
|
||||
Object.assign(queryParams, {
|
||||
current: 1,
|
||||
size: 10,
|
||||
projectId: '',
|
||||
itemName: ''
|
||||
competitionId: competitionId,
|
||||
projectId: null,
|
||||
itemName: '',
|
||||
})
|
||||
fetchData()
|
||||
if (competitionId) {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
if (!queryParams.competitionId) {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
dialogTitle.value = '新增扣分项'
|
||||
resetForm()
|
||||
form.competitionId = queryParams.competitionId
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
const handleEdit = async (row) => {
|
||||
dialogTitle.value = '编辑扣分项'
|
||||
Object.keys(form).forEach((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
|
||||
}
|
||||
|
||||
@@ -480,29 +622,6 @@ const handleBatchDelete = () => {
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 克隆单个扣分项
|
||||
const handleCloneSingle = (row) => {
|
||||
cloneForm.sourceProjectId = row.projectId
|
||||
cloneForm.sourceProjectName = row.projectName
|
||||
cloneForm.targetProjectId = ''
|
||||
cloneForm.itemCount = 1
|
||||
cloneDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 克隆扣分项(批量)
|
||||
const handleClone = () => {
|
||||
if (!queryParams.projectId) {
|
||||
ElMessage.warning('请先选择源项目(通过搜索筛选)')
|
||||
return
|
||||
}
|
||||
|
||||
const sourceProject = projectList.value.find(p => p.id === queryParams.projectId)
|
||||
cloneForm.sourceProjectId = queryParams.projectId
|
||||
cloneForm.sourceProjectName = sourceProject?.projectName || ''
|
||||
cloneForm.targetProjectId = ''
|
||||
cloneForm.itemCount = total.value
|
||||
cloneDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交克隆
|
||||
const handleCloneSubmit = async () => {
|
||||
@@ -568,9 +687,10 @@ const handleDialogClose = () => {
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: null,
|
||||
projectId: '',
|
||||
competitionId: null,
|
||||
projectId: null,
|
||||
itemName: '',
|
||||
deductionPoints: 0.5,
|
||||
deductionPoint: 0.5,
|
||||
sortOrder: 0,
|
||||
description: ''
|
||||
})
|
||||
@@ -639,8 +759,7 @@ const formatDate = (date) => {
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadProjectList()
|
||||
fetchData()
|
||||
loadCompetitionList()
|
||||
initSortable()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -9,9 +9,19 @@
|
||||
placeholder="请选择赛事"
|
||||
clearable
|
||||
filterable
|
||||
:loading="competitionLoading"
|
||||
:disabled="competitionLoading"
|
||||
style="width: 250px"
|
||||
@change="handleCompetitionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<div style="padding: 20px; text-align: center; color: #909399;">
|
||||
<p>暂无赛事数据</p>
|
||||
<p style="font-size: 12px; margin-top: 5px;">
|
||||
请先在"赛事管理"中创建赛事
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in competitionList"
|
||||
:key="item.id"
|
||||
@@ -41,19 +51,6 @@
|
||||
<el-option label="三级" value="三级" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="邀请状态">
|
||||
<el-select
|
||||
v-model="queryParams.inviteStatus"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="待回复" :value="0" />
|
||||
<el-option label="已接受" :value="1" />
|
||||
<el-option label="已拒绝" :value="2" />
|
||||
<el-option label="已取消" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">
|
||||
搜索
|
||||
@@ -63,82 +60,30 @@
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)">
|
||||
<el-icon :size="30"><User /></el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ statistics.totalInvites || 0 }}</div>
|
||||
<div class="stats-label">总邀请数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon" style="background: linear-gradient(135deg, #fccb90 0%, #d57eeb 100%)">
|
||||
<el-icon :size="30"><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ statistics.pendingCount || 0 }}</div>
|
||||
<div class="stats-label">待回复</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon" style="background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)">
|
||||
<el-icon :size="30"><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ statistics.acceptedCount || 0 }}</div>
|
||||
<div class="stats-label">已接受</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon" style="background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)">
|
||||
<el-icon :size="30"><CircleClose /></el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ statistics.rejectedCount || 0 }}</div>
|
||||
<div class="stats-label">已拒绝</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<el-card shadow="never" class="toolbar-card">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" :icon="Plus" @click="handleSendInvite">
|
||||
发送邀请
|
||||
</el-button>
|
||||
<el-button type="success" :icon="DocumentCopy" @click="handleBatchGenerateCode">
|
||||
批量生成邀请码
|
||||
</el-button>
|
||||
<el-button type="warning" :icon="FolderOpened" @click="handleImportFromPool">
|
||||
<el-button
|
||||
type="warning"
|
||||
:icon="FolderOpened"
|
||||
:disabled="competitionLoading || queryParams.competitionId === null || queryParams.competitionId === ''"
|
||||
:loading="competitionLoading"
|
||||
@click="handleImportFromPool"
|
||||
>
|
||||
从评委库导入
|
||||
</el-button>
|
||||
<el-button :icon="Download" @click="handleExport">
|
||||
<el-button
|
||||
:icon="Download"
|
||||
:disabled="competitionLoading || queryParams.competitionId === null || queryParams.competitionId === ''"
|
||||
@click="handleExport"
|
||||
>
|
||||
导出数据
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button circle :icon="Refresh" @click="fetchData" />
|
||||
<el-button circle :icon="Refresh" :disabled="competitionLoading" @click="fetchData" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,15 +99,15 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column prop="judgeName" label="评委姓名" width="120" />
|
||||
<el-table-column prop="judgeLevel" label="评委等级" width="100" align="center">
|
||||
<el-table-column prop="judgeName" label="评委姓名" />
|
||||
<el-table-column prop="judgeLevel" label="评委等级" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getLevelType(row.judgeLevel)" size="small">
|
||||
{{ row.judgeLevel }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="inviteCode" label="邀请码" width="200" align="center">
|
||||
<el-table-column prop="inviteCode" label="邀请码" align="center">
|
||||
<template #default="{ row }">
|
||||
<!-- 已有邀请码:显示邀请码 + 重新生成按钮 -->
|
||||
<div v-if="row.inviteCode" style="display: flex; align-items: center; justify-content: center; gap: 8px;">
|
||||
@@ -198,66 +143,27 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contactPhone" label="联系电话" width="130" />
|
||||
<el-table-column prop="contactEmail" label="联系邮箱" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="inviteStatus" label="邀请状态" width="100" align="center">
|
||||
<el-table-column prop="venueName" label="负责场地" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.inviteStatus)" size="small">
|
||||
{{ getStatusText(row.inviteStatus) }}
|
||||
<span>{{ row.venueName || "全部场地" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="refereeType" label="裁判类型" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.refereeType === 1 ? 'danger' : (row.refereeType === 3 ? 'warning' : 'primary')" size="small">
|
||||
{{ row.refereeType === 1 ? '主裁判' : (row.refereeType === 3 ? '总裁' : '裁判员') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="inviteTime" label="邀请时间" width="160" align="center">
|
||||
<el-table-column prop="contactPhone" label="联系电话" />
|
||||
<el-table-column prop="contactEmail" label="擅长项目" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="150" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.inviteTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="replyTime" label="回复时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.replyTime ? formatDateTime(row.replyTime) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="replyNote" label="回复备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.inviteStatus === 0"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Refresh"
|
||||
@click="handleResend(row)"
|
||||
>
|
||||
重发
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.inviteStatus === 0"
|
||||
link
|
||||
type="warning"
|
||||
:icon="Bell"
|
||||
@click="handleReminder(row)"
|
||||
>
|
||||
提醒
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.inviteStatus === 0"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Close"
|
||||
@click="handleCancel(row)"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button link type="primary" :icon="View" @click="handleView(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.inviteStatus === 1"
|
||||
link
|
||||
type="success"
|
||||
:icon="CircleCheck"
|
||||
@click="handleConfirm(row)"
|
||||
>
|
||||
确认
|
||||
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -276,6 +182,129 @@
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 裁判选择对话框 -->
|
||||
<el-dialog
|
||||
v-model="judgeDialogVisible"
|
||||
title="从评委库导入"
|
||||
width="900px"
|
||||
: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-item label="姓名">
|
||||
<el-input
|
||||
v-model="judgeQueryParams.name"
|
||||
placeholder="请输入姓名"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input
|
||||
v-model="judgeQueryParams.phone"
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="裁判类型">
|
||||
<el-select
|
||||
v-model="judgeQueryParams.refereeType"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="主裁判" :value="1" />
|
||||
<el-option label="裁判员" :value="2" />
|
||||
<el-option label="总裁" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleJudgeSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="handleJudgeReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 裁判列表 -->
|
||||
<el-table
|
||||
v-loading="judgeLoading"
|
||||
:data="judgeList"
|
||||
stripe
|
||||
border
|
||||
max-height="400"
|
||||
@selection-change="handleJudgeSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="gender" label="性别" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.gender === 1 ? '男' : '女' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column prop="refereeType" label="裁判类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.refereeType === 1 ? 'danger' : (row.refereeType === 3 ? 'warning' : 'primary')" size="small">
|
||||
{{ row.refereeType === 1 ? '主裁判' : (row.refereeType === 3 ? '总裁' : '裁判员') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="level" label="等级/职称" width="120" />
|
||||
<el-table-column prop="specialty" label="擅长项目" min-width="150" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="judge-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="judgeQueryParams.current"
|
||||
v-model:page-size="judgeQueryParams.size"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="judgeTotal"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="loadJudgeList"
|
||||
@current-change="loadJudgeList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<span style="margin-right: auto; color: #909399;">
|
||||
已选择 {{ selectedJudges.length }} 位裁判
|
||||
</span>
|
||||
<el-button @click="judgeDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="selectedJudges.length === 0"
|
||||
@click="handleConfirmImport"
|
||||
>
|
||||
确定导入
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -285,36 +314,24 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Search,
|
||||
Refresh,
|
||||
Plus,
|
||||
Delete,
|
||||
Download,
|
||||
DocumentCopy,
|
||||
FolderOpened,
|
||||
Upload,
|
||||
View,
|
||||
Close,
|
||||
Bell,
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
User,
|
||||
Clock
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
getJudgeInviteList,
|
||||
sendInvite,
|
||||
batchSendInvites,
|
||||
resendInvite,
|
||||
cancelInvite,
|
||||
confirmInvite,
|
||||
getInviteStatistics,
|
||||
importFromJudgePool,
|
||||
exportInvites,
|
||||
sendReminder,
|
||||
generateInviteCode,
|
||||
batchGenerateInviteCode,
|
||||
regenerateInviteCode
|
||||
regenerateInviteCode,
|
||||
removeInvite
|
||||
} from '@/api/martial/judgeInvite'
|
||||
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 dayjs from 'dayjs'
|
||||
|
||||
// 数据状态
|
||||
@@ -323,6 +340,32 @@ const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const selection = ref([])
|
||||
const competitionList = ref([])
|
||||
const competitionLoading = 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 judgeList = ref([])
|
||||
const judgeTotal = ref(0)
|
||||
const selectedJudges = ref([])
|
||||
const judgeQueryParams = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
name: '',
|
||||
phone: '',
|
||||
refereeType: null
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const statistics = ref({
|
||||
@@ -336,37 +379,74 @@ const statistics = ref({
|
||||
const queryParams = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
competitionId: '',
|
||||
competitionId: null,
|
||||
judgeName: '',
|
||||
judgeLevel: '',
|
||||
inviteStatus: ''
|
||||
inviteStatus: '',
|
||||
venueId: null
|
||||
})
|
||||
|
||||
// 加载赛事列表
|
||||
const loadCompetitionList = async () => {
|
||||
competitionLoading.value = true
|
||||
try {
|
||||
const res = await getCompetitionList(1, 1000, { status: 1 })
|
||||
if (res.data && res.data.records) {
|
||||
competitionList.value = res.data.records
|
||||
if (competitionList.value.length > 0 && !queryParams.competitionId) {
|
||||
// 查询所有赛事
|
||||
const res = await getCompetitionList(1, 1000, {})
|
||||
|
||||
// 根据axios响应拦截器的处理,数据结构为: res.data.data.records
|
||||
// res.data = 后端返回的整个JSON对象 { code, success, data, msg }
|
||||
// res.data.data = 后端返回的data字段 { records, total, size, current, pages }
|
||||
// res.data.data.records = 实际的赛事列表数组
|
||||
const records = res.data?.data?.records || []
|
||||
|
||||
competitionList.value = records
|
||||
|
||||
if (competitionList.value.length > 0) {
|
||||
// 如果没有选中赛事,自动选择第一个
|
||||
if (queryParams.competitionId === null || queryParams.competitionId === '') {
|
||||
queryParams.competitionId = competitionList.value[0].id
|
||||
handleCompetitionChange(queryParams.competitionId)
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning('暂无可用赛事,请先创建赛事')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载赛事列表失败:', error)
|
||||
ElMessage.error(`加载赛事列表失败: ${error.response?.data?.msg || error.message || '请检查网络连接'}`)
|
||||
} finally {
|
||||
competitionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 赛事切换
|
||||
const handleCompetitionChange = (competitionId) => {
|
||||
const handleCompetitionChange = async (competitionId) => {
|
||||
// 重置场地筛选
|
||||
queryParams.venueId = null
|
||||
// 加载该赛事的场地列表用于筛选
|
||||
await loadFilterVenueList()
|
||||
fetchData()
|
||||
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 () => {
|
||||
if (!queryParams.competitionId) return
|
||||
if (queryParams.competitionId === null || queryParams.competitionId === '') return
|
||||
|
||||
try {
|
||||
const res = await getInviteStatistics(queryParams.competitionId)
|
||||
@@ -380,7 +460,7 @@ const loadStatistics = async () => {
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
if (!queryParams.competitionId) return
|
||||
if (queryParams.competitionId === null || queryParams.competitionId === '') return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -389,10 +469,10 @@ const fetchData = async () => {
|
||||
queryParams.size,
|
||||
queryParams
|
||||
)
|
||||
if (res.data) {
|
||||
tableData.value = res.data.records || []
|
||||
total.value = res.data.total || 0
|
||||
}
|
||||
// 根据axios响应拦截器的处理,数据在 res.data.data 中
|
||||
const data = res.data?.data || {}
|
||||
tableData.value = data.records || []
|
||||
total.value = data.total || 0
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败')
|
||||
console.error(error)
|
||||
@@ -414,101 +494,185 @@ const handleReset = () => {
|
||||
size: 10,
|
||||
judgeName: '',
|
||||
judgeLevel: '',
|
||||
inviteStatus: ''
|
||||
inviteStatus: '',
|
||||
venueId: null
|
||||
})
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 发送邀请
|
||||
const handleSendInvite = async () => {
|
||||
if (!queryParams.competitionId) {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
// TODO: 打开发送邀请对话框
|
||||
ElMessage.info('请先生成邀请码,然后通过邮件/短信发送给评委')
|
||||
}
|
||||
|
||||
// 从评委库导入
|
||||
const handleImportFromPool = async () => {
|
||||
if (!queryParams.competitionId) {
|
||||
if (competitionLoading.value) {
|
||||
ElMessage.warning('赛事列表加载中,请稍候...')
|
||||
return
|
||||
}
|
||||
if (competitionList.value.length === 0) {
|
||||
ElMessage.warning('暂无可用赛事,请先创建赛事')
|
||||
return
|
||||
}
|
||||
if (queryParams.competitionId === null || queryParams.competitionId === '') {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
|
||||
// 重置导入表单
|
||||
importForm.venueId = null
|
||||
importForm.projectIds = []
|
||||
|
||||
// 加载场地和项目列表
|
||||
await loadVenueAndProjectList()
|
||||
|
||||
// 打开裁判选择对话框
|
||||
judgeDialogVisible.value = true
|
||||
selectedJudges.value = []
|
||||
loadJudgeList()
|
||||
}
|
||||
|
||||
// 加载场地和项目列表
|
||||
const loadVenueAndProjectList = async () => {
|
||||
try {
|
||||
// TODO: 打开评委选择对话框
|
||||
ElMessage.info('请先在评委管理中添加评委,然后在此处生成邀请码')
|
||||
// 并行加载场地和项目
|
||||
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) {
|
||||
ElMessage.error('导入失败')
|
||||
console.error('加载场地/项目列表失败:', error)
|
||||
ElMessage.error('加载场地/项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重发
|
||||
const handleResend = async (row) => {
|
||||
// 加载裁判列表
|
||||
const loadJudgeList = async () => {
|
||||
judgeLoading.value = true
|
||||
try {
|
||||
await resendInvite(row.id)
|
||||
ElMessage.success('重发成功')
|
||||
fetchData()
|
||||
const params = {}
|
||||
if (judgeQueryParams.name) {
|
||||
params.name = judgeQueryParams.name
|
||||
}
|
||||
if (judgeQueryParams.phone) {
|
||||
params.phone = judgeQueryParams.phone
|
||||
}
|
||||
if (judgeQueryParams.refereeType !== null && judgeQueryParams.refereeType !== '') {
|
||||
params.refereeType = judgeQueryParams.refereeType
|
||||
}
|
||||
|
||||
const res = await getRefereeList(
|
||||
judgeQueryParams.current,
|
||||
judgeQueryParams.size,
|
||||
params
|
||||
)
|
||||
|
||||
if (res.data && res.data.data && res.data.data.records) {
|
||||
judgeList.value = res.data.data.records
|
||||
judgeTotal.value = res.data.data.total || 0
|
||||
} else if (res.data && res.data.records) {
|
||||
judgeList.value = res.data.records
|
||||
judgeTotal.value = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('重发失败')
|
||||
console.error('加载裁判列表失败:', error)
|
||||
ElMessage.error('加载裁判列表失败')
|
||||
} finally {
|
||||
judgeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提醒
|
||||
const handleReminder = async (row) => {
|
||||
try {
|
||||
await sendReminder(row.id, '请尽快回复邀请')
|
||||
ElMessage.success('提醒发送成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('提醒发送失败')
|
||||
}
|
||||
// 裁判搜索
|
||||
const handleJudgeSearch = () => {
|
||||
judgeQueryParams.current = 1
|
||||
loadJudgeList()
|
||||
}
|
||||
|
||||
// 取消邀请
|
||||
const handleCancel = async (row) => {
|
||||
// 裁判搜索重置
|
||||
const handleJudgeReset = () => {
|
||||
judgeQueryParams.name = ''
|
||||
judgeQueryParams.phone = ''
|
||||
judgeQueryParams.refereeType = null
|
||||
judgeQueryParams.current = 1
|
||||
loadJudgeList()
|
||||
}
|
||||
|
||||
// 裁判选择改变
|
||||
const handleJudgeSelectionChange = (val) => {
|
||||
selectedJudges.value = val
|
||||
}
|
||||
|
||||
// 确认导入裁判
|
||||
const handleConfirmImport = async () => {
|
||||
if (selectedJudges.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一位裁判')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证场地和项目
|
||||
if (!importForm.venueId) {
|
||||
ElMessage.warning('请选择分配的场地')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const { value: reason } = await ElMessageBox.prompt('请输入取消原因', '取消邀请', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '请输入取消原因',
|
||||
inputValidator: (value) => {
|
||||
if (!value || value.trim() === '') {
|
||||
return '请输入取消原因'
|
||||
}
|
||||
return true
|
||||
await ElMessageBox.confirm(
|
||||
`确定为选中的 ${selectedJudges.value.length} 位裁判生成邀请码吗?`,
|
||||
'确认导入',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
const judgeIds = selectedJudges.value.map(item => item.id)
|
||||
|
||||
const res = await batchGenerateInviteCode({
|
||||
competitionId: queryParams.competitionId,
|
||||
judgeIds: judgeIds,
|
||||
role: 'judge',
|
||||
venueId: importForm.venueId,
|
||||
// projects不传,裁判默认负责整个场地
|
||||
expireDays: 30
|
||||
})
|
||||
|
||||
await cancelInvite(row.id, reason)
|
||||
ElMessage.success('取消成功')
|
||||
fetchData()
|
||||
loadStatistics()
|
||||
// 根据axios响应拦截器的处理,数据在 res.data.data 中
|
||||
const invites = res.data?.data || []
|
||||
|
||||
if (Array.isArray(invites) && invites.length > 0) {
|
||||
ElMessage.success(`成功为 ${invites.length} 位裁判生成邀请码`)
|
||||
judgeDialogVisible.value = false
|
||||
await fetchData()
|
||||
await loadStatistics()
|
||||
} else if (Array.isArray(invites) && invites.length === 0) {
|
||||
ElMessage.warning('所有裁判都已有有效邀请码,无需重复生成')
|
||||
} else {
|
||||
ElMessage.error(res.data?.msg || '生成邀请码失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('取消失败')
|
||||
console.error('导入裁判失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || error.message || '导入裁判失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认
|
||||
const handleConfirm = async (row) => {
|
||||
ElMessageBox.confirm('确认接受该评委的邀请回复?', '提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await confirmInvite(row.id)
|
||||
ElMessage.success('确认成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
ElMessage.error('确认失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 查看
|
||||
const handleView = async (row) => {
|
||||
try {
|
||||
@@ -547,19 +711,27 @@ const handleView = async (row) => {
|
||||
|
||||
/**
|
||||
* 生成单个邀请码
|
||||
* 注意:此功能仅用于已有邀请记录但未生成邀请码的情况
|
||||
* 如果需要为新裁判生成邀请码,请使用"从评委库导入"功能
|
||||
*/
|
||||
const handleGenerateCode = async (row) => {
|
||||
if (!queryParams.competitionId) {
|
||||
if (queryParams.competitionId === null || queryParams.competitionId === '') {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有judgeId
|
||||
if (!row.judgeId) {
|
||||
ElMessage.error('数据异常:缺少裁判ID,请使用"从评委库导入"功能')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await generateInviteCode({
|
||||
competitionId: queryParams.competitionId,
|
||||
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,
|
||||
projects: row.projects ? JSON.stringify(row.projects) : null,
|
||||
expireDays: 30
|
||||
@@ -622,52 +794,31 @@ const handleRegenerateCode = async (row) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成邀请码
|
||||
* 删除邀请记录
|
||||
*/
|
||||
const handleBatchGenerateCode = async () => {
|
||||
if (!queryParams.competitionId) {
|
||||
ElMessage.warning('请先选择赛事')
|
||||
return
|
||||
}
|
||||
|
||||
if (selection.value.length === 0) {
|
||||
ElMessage.warning('请先选择评委')
|
||||
return
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定为选中的 ${selection.value.length} 位评委批量生成邀请码吗?`,
|
||||
'批量生成邀请码',
|
||||
`确定要删除评委"${row.judgeName}"的邀请记录吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
const judgeIds = selection.value.map(item => item.judgeId)
|
||||
await removeInvite(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
const res = await batchGenerateInviteCode({
|
||||
competitionId: queryParams.competitionId,
|
||||
judgeIds: judgeIds,
|
||||
role: 'judge',
|
||||
expireDays: 30
|
||||
})
|
||||
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
ElMessage.success(`成功生成 ${res.data.length} 个邀请码`)
|
||||
// 刷新列表
|
||||
await fetchData()
|
||||
await loadStatistics()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '批量生成失败')
|
||||
}
|
||||
// 刷新列表和统计数据
|
||||
await fetchData()
|
||||
await loadStatistics()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('批量生成邀请码失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || error.message || '批量生成邀请码失败')
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || error.message || '删除失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -776,8 +927,12 @@ const fallbackCopyToClipboard = (text, label) => {
|
||||
}
|
||||
|
||||
// 挂载
|
||||
onMounted(() => {
|
||||
loadCompetitionList()
|
||||
onMounted(async () => {
|
||||
await loadCompetitionList()
|
||||
// 加载筛选用的场地列表
|
||||
if (queryParams.competitionId) {
|
||||
await loadFilterVenueList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -854,4 +1009,25 @@ onMounted(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// 裁判选择对话框样式
|
||||
.judge-search-form {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.judge-pagination {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
<div class="martial-order-container">
|
||||
<el-card shadow="hover">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">订单管理</h2>
|
||||
<h2 class="page-title">赛事管理</h2>
|
||||
</div>
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item>
|
||||
@@ -60,8 +60,8 @@
|
||||
</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 :type="getStatusType(calculateStatus(scope.row))" size="small">
|
||||
{{ getStatusText(calculateStatus(scope.row)) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -73,7 +73,6 @@
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleDispatch(scope.row)"
|
||||
:disabled="!isScheduleCompleted(scope.row.id)"
|
||||
:title="isScheduleCompleted(scope.row.id) ? '进入调度' : '请先完成编排'"
|
||||
>
|
||||
调度
|
||||
@@ -182,13 +181,13 @@ export default {
|
||||
try {
|
||||
const res = await getScheduleResult(competition.id)
|
||||
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 {
|
||||
this.$set(this.scheduleStatusMap, competition.id, false)
|
||||
this.scheduleStatusMap[competition.id] = false
|
||||
}
|
||||
} catch (err) {
|
||||
// 如果获取失败,默认为未完成
|
||||
this.$set(this.scheduleStatusMap, competition.id, false)
|
||||
this.scheduleStatusMap[competition.id] = false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -253,6 +252,19 @@ export default {
|
||||
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) {
|
||||
const statusMap = {
|
||||
1: 'info', // 未开始
|
||||
|
||||
355
src/views/martial/order/index.vue.bak
Normal file
355
src/views/martial/order/index.vue.bak
Normal 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>
|
||||
@@ -94,11 +94,14 @@
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="age"
|
||||
label="年龄"
|
||||
width="80"
|
||||
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
|
||||
prop="organization"
|
||||
|
||||
990
src/views/martial/participant/index.vue.bak
Normal file
990
src/views/martial/participant/index.vue.bak
Normal 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>
|
||||
@@ -28,17 +28,12 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组类别">
|
||||
<el-select
|
||||
<el-input
|
||||
v-model="queryParams.category"
|
||||
placeholder="请选择分组类别"
|
||||
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
|
||||
@@ -47,10 +42,21 @@
|
||||
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-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>
|
||||
@@ -122,60 +128,53 @@
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="competitionName"
|
||||
label="所属赛事"
|
||||
min-width="150"
|
||||
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 }">
|
||||
<el-tag v-if="row.category === 1" type="primary">男子</el-tag>
|
||||
<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>
|
||||
<span>{{ row.category || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="eventType" label="项目类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.eventType === 1">套路</span>
|
||||
<span v-else-if="row.eventType === 2">散打</span>
|
||||
<span v-else-if="row.eventType === 3">器械</span>
|
||||
<span v-else-if="row.eventType === 4">对练</span>
|
||||
<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="registrationFee"
|
||||
prop="price"
|
||||
label="报名费(元)"
|
||||
width="110"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span style="color: #f56c6c">¥{{ row.registrationFee || 0 }}</span>
|
||||
<span style="color: #f56c6c">¥{{ row.price || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报名时间" width="180" align="center">
|
||||
|
||||
<el-table-column prop="estimatedDuration" label="预计时长" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.registrationStartTime && row.registrationEndTime">
|
||||
<div style="font-size: 12px">
|
||||
{{ formatDate(row.registrationStartTime) }}
|
||||
</div>
|
||||
<div style="font-size: 12px">至</div>
|
||||
<div style="font-size: 12px">
|
||||
{{ formatDate(row.registrationEndTime) }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else style="color: #909399">未设置</span>
|
||||
<span>{{ row.estimatedDuration || 5 }}分钟</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报名人数" width="120" align="center">
|
||||
<el-table-column label="单位容纳人数" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.currentCount >= row.maxParticipants ? '#f56c6c' : '#67c23a' }">
|
||||
{{ row.currentCount || 0 }}
|
||||
</span>
|
||||
/ {{ row.maxParticipants || 0 }}
|
||||
{{ row.maxParticipants || 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
@@ -247,15 +246,6 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</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 :gutter="20">
|
||||
<el-col :span="12">
|
||||
@@ -297,6 +287,31 @@
|
||||
</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
|
||||
@@ -311,33 +326,7 @@
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报名开始时间" prop="registrationStartTime">
|
||||
<el-date-picker
|
||||
v-model="form.registrationStartTime"
|
||||
type="datetime"
|
||||
placeholder="选择开始时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报名结束时间" prop="registrationEndTime">
|
||||
<el-date-picker
|
||||
v-model="form.registrationEndTime"
|
||||
type="datetime"
|
||||
placeholder="选择结束时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大参赛人数" prop="maxParticipants">
|
||||
<el-form-item label="单位容纳人数" prop="maxParticipants">
|
||||
<el-input-number
|
||||
v-model="form.maxParticipants"
|
||||
:min="1"
|
||||
@@ -395,7 +384,7 @@
|
||||
{{ detailData.projectName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="所属赛事">
|
||||
{{ detailData.competitionName }}
|
||||
{{ getCompetitionName(detailData.competitionId) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分组类别">
|
||||
<el-tag v-if="detailData.category === 1" type="primary">男子</el-tag>
|
||||
@@ -409,21 +398,19 @@
|
||||
<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="报名开始时间">
|
||||
{{ formatDate(detailData.registrationStartTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报名结束时间">
|
||||
{{ formatDate(detailData.registrationEndTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最大参赛人数">
|
||||
<el-descriptions-item label="单位容纳人数">
|
||||
{{ detailData.maxParticipants }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前报名人数">
|
||||
<el-descriptions-item label="已报名人数">
|
||||
<span :style="{
|
||||
color: detailData.currentCount >= detailData.maxParticipants ? '#f56c6c' : '#67c23a',
|
||||
fontWeight: 'bold'
|
||||
@@ -460,8 +447,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
getProjectList,
|
||||
addProject,
|
||||
updateProject,
|
||||
submitProject,
|
||||
removeProject,
|
||||
importProjects,
|
||||
exportProjects
|
||||
@@ -490,7 +476,8 @@ const queryParams = reactive({
|
||||
competitionId: '',
|
||||
projectName: '',
|
||||
category: '',
|
||||
eventType: ''
|
||||
eventType: '',
|
||||
type: ''
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
@@ -501,9 +488,9 @@ const form = reactive({
|
||||
projectName: '',
|
||||
category: null,
|
||||
eventType: null,
|
||||
type: null,
|
||||
estimatedDuration: 5,
|
||||
registrationFee: 0,
|
||||
registrationStartTime: '',
|
||||
registrationEndTime: '',
|
||||
maxParticipants: 100,
|
||||
sortOrder: 0,
|
||||
rules: '',
|
||||
@@ -525,10 +512,6 @@ 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' }
|
||||
@@ -539,36 +522,26 @@ const rules = {
|
||||
eventType: [
|
||||
{ required: true, message: '请选择项目类型', trigger: 'change' }
|
||||
],
|
||||
estimatedDuration: [
|
||||
{ required: true, message: '请输入预计时长', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择参赛类型', trigger: 'change' }
|
||||
],
|
||||
registrationFee: [
|
||||
{ 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: [
|
||||
{ required: true, message: '请输入最大参赛人数', trigger: 'blur' }
|
||||
{ required: true, message: '请输入单位容纳人数', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 加载赛事列表
|
||||
const loadCompetitionList = async () => {
|
||||
try {
|
||||
const res = await getCompetitionList(1, 1000, { status: 1 })
|
||||
if (res.data && res.data.records) {
|
||||
competitionList.value = res.data.records
|
||||
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)
|
||||
@@ -579,14 +552,22 @@ const loadCompetitionList = async () => {
|
||||
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,
|
||||
queryParams
|
||||
params
|
||||
)
|
||||
if (res.data) {
|
||||
tableData.value = res.data.records || []
|
||||
total.value = res.data.total || 0
|
||||
if (res.data && res.data.data) {
|
||||
tableData.value = res.data.data.records || []
|
||||
total.value = res.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取数据失败')
|
||||
@@ -610,7 +591,8 @@ const handleReset = () => {
|
||||
competitionId: '',
|
||||
projectName: '',
|
||||
category: '',
|
||||
eventType: ''
|
||||
eventType: '',
|
||||
type: ''
|
||||
})
|
||||
fetchData()
|
||||
}
|
||||
@@ -623,11 +605,15 @@ const handleAdd = () => {
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
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
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -684,11 +670,16 @@ const handleSubmit = async () => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
// 构建提交数据,确保字段名与后端一致
|
||||
const submitData = {
|
||||
...form,
|
||||
price: form.registrationFee // 后端使用 price 字段
|
||||
}
|
||||
if (form.id) {
|
||||
await updateProject(form)
|
||||
await submitProject(submitData)
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await addProject(form)
|
||||
await submitProject(submitData)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
@@ -721,9 +712,9 @@ const resetForm = () => {
|
||||
projectName: '',
|
||||
category: null,
|
||||
eventType: null,
|
||||
type: null,
|
||||
estimatedDuration: 5,
|
||||
registrationFee: 0,
|
||||
registrationStartTime: '',
|
||||
registrationEndTime: '',
|
||||
maxParticipants: 100,
|
||||
sortOrder: 0,
|
||||
rules: '',
|
||||
@@ -788,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) => {
|
||||
if (!date) return '-'
|
||||
|
||||
931
src/views/martial/project/index.vue.backup
Normal file
931
src/views/martial/project/index.vue.backup
Normal 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>
|
||||
@@ -33,8 +33,9 @@
|
||||
<el-form-item>
|
||||
<el-select v-model="searchForm.refereeType" placeholder="裁判类型" clearable size="small" style="width: 150px">
|
||||
<el-option label="全部" :value="null"></el-option>
|
||||
<el-option label="裁判长" :value="1"></el-option>
|
||||
<el-option label="普通裁判" :value="2"></el-option>
|
||||
<el-option label="主裁判" :value="1"></el-option>
|
||||
<el-option label="裁判员" :value="2"></el-option>
|
||||
<el-option label="总裁" :value="3"></el-option>
|
||||
</el-select>
|
||||
</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="refereeType" label="裁判类型" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.refereeType === 1 ? 'danger' : 'primary'" size="small">
|
||||
{{ scope.row.refereeType === 1 ? '主裁判' : '普通裁判' }}
|
||||
<el-tag :type="scope.row.refereeType === 1 ? 'danger' : (scope.row.refereeType === 3 ? 'warning' : 'primary')" size="small">
|
||||
{{ scope.row.refereeType === 1 ? '主裁判' : (scope.row.refereeType === 3 ? '总裁' : '裁判员') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -137,13 +138,24 @@
|
||||
<el-form-item label="裁判类型" prop="refereeType">
|
||||
<el-select v-model="formData.refereeType" placeholder="请选择裁判类型" style="width: 100%">
|
||||
<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-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="等级/职称" prop="level">
|
||||
<el-input v-model="formData.level" placeholder="请输入等级/职称" clearable></el-input>
|
||||
<el-select
|
||||
v-model="formData.level"
|
||||
placeholder="请选择等级"
|
||||
clearable
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="国家级" value="国家级" />
|
||||
<el-option label="一级" value="一级" />
|
||||
<el-option label="二级" value="二级" />
|
||||
<el-option label="三级" value="三级" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -101,12 +101,10 @@
|
||||
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="category" label="类别" width="80"></el-table-column>
|
||||
<el-table-column prop="individual" label="2队" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="dual" label="2组" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="team1101" label="1101" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="workers" label="工作人员" width="90" align="center"></el-table-column>
|
||||
<el-table-column prop="female" label="女性组别" width="90" align="center"></el-table-column>
|
||||
<el-table-column prop="singleCount" label="单人项目" width="100" align="center"></el-table-column>
|
||||
<el-table-column prop="teamCount" label="集体项目" width="100" align="center"></el-table-column>
|
||||
<el-table-column prop="male" label="男" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="female" label="女" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="total" label="合计" width="60" align="center">
|
||||
<template #default="scope">
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="participantCategory" label="参人/单位" width="100"></el-table-column>
|
||||
<el-table-column prop="teamCount" label="队伍" width="80" align="center"></el-table-column>
|
||||
<el-table-column prop="singleTeamPeople" label="单位型号人数" width="120" align="center"></el-table-column>
|
||||
<el-table-column prop="estimatedDuration" label="剩下显时(公共)" width="150" align="center"></el-table-column>
|
||||
<el-table-column prop="projectType" label="单人/集体" width="100" align="center"></el-table-column>
|
||||
<el-table-column prop="athleteCount" label="人数/集体" width="100" 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="100" align="center"></el-table-column>
|
||||
<el-table-column prop="projectCode" label="项目编码" width="120" align="center"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -335,76 +334,85 @@ export default {
|
||||
const participants = await this.getParticipants()
|
||||
console.log('参赛人员列表返回:', participants)
|
||||
|
||||
// 预加载项目信息
|
||||
await this.preloadProjectInfo(participants)
|
||||
|
||||
// 按单位分组统计
|
||||
const unitMap = new Map()
|
||||
participants.forEach(p => {
|
||||
// 兼容驼峰和下划线命名
|
||||
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)) {
|
||||
unitMap.set(unit, {
|
||||
schoolUnit: unit,
|
||||
category: '',
|
||||
individual: 0,
|
||||
dual: 0,
|
||||
team1101: 0,
|
||||
workers: 0,
|
||||
singleCount: 0,
|
||||
teamCount: 0,
|
||||
male: 0,
|
||||
female: 0,
|
||||
total: 0
|
||||
})
|
||||
}
|
||||
const stat = unitMap.get(unit)
|
||||
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())
|
||||
} catch (err) {
|
||||
console.error('加载参赛人员统计失败', err)
|
||||
this.$message.warning('加载参赛人员统计失败')
|
||||
// 使用空数组作为默认值
|
||||
this.participantsData = []
|
||||
}
|
||||
},
|
||||
|
||||
// 加载项目时间统计(该赛事的所有项目及参赛人数)
|
||||
// 加载项目时间统计(该赛事的所有项目及参赛人数)
|
||||
async loadProjectTimeStats() {
|
||||
try {
|
||||
// 使用缓存的参赛者列表
|
||||
const participants = await this.getParticipants()
|
||||
|
||||
// 2. 按项目ID分组
|
||||
const projectMap = new Map()
|
||||
// 预加载项目信息
|
||||
await this.preloadProjectInfo(participants)
|
||||
|
||||
// 按项目ID分组统计人数
|
||||
const projectAthleteCount = new Map()
|
||||
participants.forEach(athlete => {
|
||||
// 兼容驼峰和下划线命名
|
||||
const projectId = athlete.projectId || athlete.project_id
|
||||
if (projectId) {
|
||||
if (!projectMap.has(projectId)) {
|
||||
projectMap.set(projectId, [])
|
||||
}
|
||||
projectMap.get(projectId).push(athlete)
|
||||
projectAthleteCount.set(projectId, (projectAthleteCount.get(projectId) || 0) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
// 3. 从缓存中获取项目信息并统计(项目信息已经在 loadRegistrationStats 中预加载)
|
||||
// 从缓存中获取项目信息并构建统计数据
|
||||
const projectStats = []
|
||||
for (const [projectId, athleteList] of projectMap) {
|
||||
for (const [projectId, count] of projectAthleteCount) {
|
||||
const project = this.projectCache.get(projectId)
|
||||
if (project) {
|
||||
const projectType = project.type || 1 // 1=单人, 2=集体
|
||||
projectStats.push({
|
||||
projectName: project.projectName || project.project_name || '未知项目',
|
||||
participantCategory: project.category || '',
|
||||
teamCount: 1, // 简化处理,设为1
|
||||
singleTeamPeople: athleteList.length,
|
||||
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0
|
||||
})
|
||||
} else {
|
||||
// 如果缓存中没有(理论上<E8AEBA><E4B88A><EFBFBD>应该发生),添加基本信息
|
||||
projectStats.push({
|
||||
projectName: `项目ID:${projectId}`,
|
||||
participantCategory: '',
|
||||
teamCount: 1,
|
||||
singleTeamPeople: athleteList.length,
|
||||
estimatedDuration: 0
|
||||
projectType: projectType === 1 ? '单人' : '集体',
|
||||
athleteCount: count,
|
||||
groupCount: projectType === 2 ? count : '-', // 集体项目显示组数,单人显示-
|
||||
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0,
|
||||
projectCode: project.projectCode || project.project_code || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -417,7 +425,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// 加载金额统计(该赛事所有单位的报名金额)
|
||||
// 加载金额统计(该赛事所有单位的报名金额)
|
||||
async loadAmountStats() {
|
||||
try {
|
||||
// 使用缓存的参赛者列表
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1019
src/views/martial/schedule/index.vue.bak
Normal file
1019
src/views/martial/schedule/index.vue.bak
Normal file
File diff suppressed because it is too large
Load Diff
@@ -94,7 +94,8 @@
|
||||
|
||||
<el-table-column label="总裁判分数" width="120" align="center" fixed="right">
|
||||
<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>
|
||||
</el-table-column>
|
||||
|
||||
@@ -153,7 +154,17 @@
|
||||
|
||||
<div class="total-score-display">
|
||||
<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>
|
||||
@@ -182,96 +193,6 @@ export default {
|
||||
projectOptions: [],
|
||||
venueOptions: [],
|
||||
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: [],
|
||||
pagination: {
|
||||
current: 1,
|
||||
@@ -345,25 +266,22 @@ export default {
|
||||
try {
|
||||
const res = await getScoreList(this.pagination.current, this.pagination.size, params)
|
||||
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('======================================')
|
||||
|
||||
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)
|
||||
@@ -466,6 +384,12 @@ export default {
|
||||
const athleteMap = new Map()
|
||||
|
||||
scores.forEach(score => {
|
||||
// 确保 projectId 存在
|
||||
if (!score.projectId) {
|
||||
console.warn('跳过无效评分记录:', score)
|
||||
return
|
||||
}
|
||||
|
||||
const key = `${score.athleteId}-${score.projectId}`
|
||||
if (!athleteMap.has(key)) {
|
||||
athleteMap.set(key, {
|
||||
@@ -480,7 +404,9 @@ export default {
|
||||
playerNo: score.playerNo || '',
|
||||
judgeScores: [],
|
||||
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 => {
|
||||
if (athlete.judgeScores.length > 0) {
|
||||
const sum = athlete.judgeScores.reduce((a, b) => a + b, 0)
|
||||
athlete.totalScore = sum / athlete.judgeScores.length
|
||||
athlete.totalScore = this.calculateFinalScore(athlete.judgeScores)
|
||||
}
|
||||
return athlete
|
||||
})
|
||||
@@ -516,6 +441,34 @@ export default {
|
||||
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() {
|
||||
this.pagination.current = 1
|
||||
@@ -620,6 +573,16 @@ export default {
|
||||
}
|
||||
|
||||
.total-score {
|
||||
}
|
||||
|
||||
.pending-score {
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value.pending {
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
color: #1b7c5e;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
@@ -663,6 +626,12 @@ export default {
|
||||
font-weight: 700;
|
||||
color: #1b7c5e;
|
||||
}
|
||||
|
||||
.calculation-note {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<i class="el-icon-s-order"></i>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<h3>订单管理</h3>
|
||||
<h3>赛事管理</h3>
|
||||
<p>查看和管理订单</p>
|
||||
</div>
|
||||
<i class="card-arrow el-icon-arrow-right"></i>
|
||||
|
||||
@@ -14,7 +14,8 @@ export default ({ mode, command }) => {
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
server: {
|
||||
port: 2888,
|
||||
port: 8083,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8123',
|
||||
|
||||
Reference in New Issue
Block a user