Compare commits
40 Commits
7f8c5c630b
...
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 |
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)
|
- **框架**: Vue 3.4 (Composition API)
|
||||||
- **构建工具**: Vite 5
|
- **构建工具**: Vite 5
|
||||||
- **UI 组件**: Element Plus
|
- **UI 组件**: Element Plus + Avue
|
||||||
- **表单/表格**: Avue
|
|
||||||
- **HTTP 库**: Axios
|
|
||||||
- **路由**: Vue Router 4
|
|
||||||
- **状态管理**: Vuex 4
|
- **状态管理**: Vuex 4
|
||||||
- **样式**: Sass/SCSS
|
- **路由**: Vue Router 4
|
||||||
|
|
||||||
## 📁 项目结构
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 开发环境
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 http://localhost:8083
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
martial-web/
|
martial-web/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── main.js # 应用入口
|
│ ├── views/martial/ # 武术业务页面
|
||||||
│ ├── App.vue # 根组件
|
│ │ ├── competition/ # 赛事管理
|
||||||
│ ├── router/ # 路由配置
|
│ │ ├── project/ # 项目管理
|
||||||
│ ├── store/ # Vuex 状态管理
|
│ │ ├── participant/ # 参赛选手
|
||||||
│ ├── views/ # 页面组件
|
│ │ ├── judgeInvite/ # 裁判邀请
|
||||||
│ ├── components/ # 通用组件
|
│ │ ├── score/ # 评分管理
|
||||||
│ ├── api/ # API 接口
|
│ │ └── ...
|
||||||
│ └── utils/ # 工具函数
|
│ ├── api/ # API 接口
|
||||||
├── public/ # 静态资源
|
│ ├── router/ # 路由配置
|
||||||
├── .env.development # 开发环境配置
|
│ └── store/ # 状态管理
|
||||||
├── .env.production # 生产环境配置
|
├── .env.development # 开发环境配置
|
||||||
├── vite.config.js # Vite 配置
|
├── .env.production # 生产环境配置
|
||||||
├── nginx.conf # Nginx 配置(生产环境容器使用)
|
└── vite.config.js # Vite 配置
|
||||||
├── Dockerfile # 完整构建 Dockerfile
|
|
||||||
├── Dockerfile.deploy # 部署 Dockerfile
|
|
||||||
└── .drone.yml # Drone CI/CD 配置
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 本地开发
|
## Docker 部署
|
||||||
|
|
||||||
### 环境要求
|
|
||||||
|
|
||||||
- Node.js >= 18
|
|
||||||
- npm >= 9
|
|
||||||
|
|
||||||
### 安装依赖
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
docker build -t martial-web .
|
||||||
|
docker run -d -p 8083:80 martial-web
|
||||||
```
|
```
|
||||||
|
|
||||||
### 开发运行
|
## 相关仓库
|
||||||
|
|
||||||
```bash
|
| 仓库 | 说明 |
|
||||||
npm run dev
|
|------|------|
|
||||||
```
|
| [martial-master](https://git.waypeak.work/martial/martial-master) | 后端 API |
|
||||||
|
| [martial-mini](https://git.waypeak.work/martial/martial-mini) | 用户端小程序 |
|
||||||
|
| [martial-admin-mini](https://git.waypeak.work/martial/martial-admin-mini) | 裁判端小程序 |
|
||||||
|
|
||||||
访问 http://localhost:5173
|
---
|
||||||
|
|
||||||
### 生产构建
|
**最后更新**: 2024-12-29
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
构建产物在 `dist/` 目录
|
|
||||||
|
|
||||||
### 预览生产构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐳 Docker 部署
|
|
||||||
|
|
||||||
### 方法一:完整构建(开发/测试)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t martial/frontend:latest -f Dockerfile .
|
|
||||||
docker run -d -p 80:80 martial/frontend:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方法二:部署已构建产物(生产推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 先本地构建
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# 使用 Dockerfile.deploy 轻量化部署
|
|
||||||
docker build -t martial/frontend:latest -f Dockerfile.deploy .
|
|
||||||
|
|
||||||
# 运行容器
|
|
||||||
docker run -d \
|
|
||||||
--name martial-frontend \
|
|
||||||
--restart always \
|
|
||||||
-p 5173:80 \
|
|
||||||
--network martial_martial-network \
|
|
||||||
-e TZ=Asia/Shanghai \
|
|
||||||
martial/frontend:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 CI/CD 自动部署
|
|
||||||
|
|
||||||
项目使用 Drone CI/CD 进行自动化部署。
|
|
||||||
|
|
||||||
### 部署流程
|
|
||||||
|
|
||||||
当推送到 `main` 分支时,自动触发:
|
|
||||||
|
|
||||||
1. **安装依赖** - `npm install`
|
|
||||||
2. **构建项目** - `npm run build`
|
|
||||||
3. **传输文件** - SCP 上传 dist、Dockerfile.deploy、nginx.conf 到服务器
|
|
||||||
4. **构建镜像** - 在服务器上执行 `docker build -f Dockerfile.deploy`
|
|
||||||
5. **部署容器** - 停止旧容器,启动新容器
|
|
||||||
6. **健康检查** - 验证服务可访问
|
|
||||||
|
|
||||||
### 查看构建状态
|
|
||||||
|
|
||||||
访问 https://martial-ci.johnsion.club
|
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
|
||||||
|
|
||||||
### 环境变量
|
|
||||||
|
|
||||||
**开发环境** (`.env.development`):
|
|
||||||
```env
|
|
||||||
VITE_APP_API=/api # API 前缀(开发时走 Vite 代理)
|
|
||||||
```
|
|
||||||
|
|
||||||
**生产环境** (`.env.production`):
|
|
||||||
```env
|
|
||||||
VITE_APP_API= # 留空(因为 BladeX 端点路径已完整)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nginx 配置
|
|
||||||
|
|
||||||
生产环境 nginx 配置(`nginx.conf`):
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# 前端路由(Vue Router history 模式)
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API 代理到后端(通过宿主机网关地址)
|
|
||||||
location /blade-auth/ {
|
|
||||||
proxy_pass http://172.21.0.1:8123/blade-auth/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /blade-system/ {
|
|
||||||
proxy_pass http://172.21.0.1:8123/blade-system/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://172.21.0.1:8123/api/;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Vite 配置
|
|
||||||
|
|
||||||
开发环境代理配置(`vite.config.js`):
|
|
||||||
|
|
||||||
```js
|
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:8123',
|
|
||||||
changeOrigin: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 常见问题
|
|
||||||
|
|
||||||
### 问题1: "No endpoint POST /api/blade-auth/oauth/token"
|
|
||||||
|
|
||||||
**原因**: 错误配置 `VITE_APP_API` 为 `/api` 前缀,导致请求 `/api/blade-auth/oauth/token`
|
|
||||||
|
|
||||||
**解决**: 检查 `.env.production` 中 `VITE_APP_API=`(留空)
|
|
||||||
|
|
||||||
### 问题2: 容器启动报 "host not found in upstream martial-backend"
|
|
||||||
|
|
||||||
**原因**: nginx.conf 中使用 Docker 服务名 `martial-backend`,但容器无法解析
|
|
||||||
|
|
||||||
**解决**: 使用宿主机网关地址 `172.21.0.1:8123` 代替
|
|
||||||
|
|
||||||
### 问题3: 端口 80 占用
|
|
||||||
|
|
||||||
**原因**: 宿主机 Caddy 已占用 80 端口
|
|
||||||
|
|
||||||
**解决**: 前端容器使用其他端口(如 5173),由 Caddy 反向代理
|
|
||||||
|
|
||||||
## 📝 开发规范
|
|
||||||
|
|
||||||
### 代码风格
|
|
||||||
|
|
||||||
- 使用 ESLint + Prettier
|
|
||||||
- 遵循 Vue 3 组合式 API 最佳实践
|
|
||||||
- 组件命名使用PascalCase
|
|
||||||
- 文件命名使用kebab-case
|
|
||||||
|
|
||||||
### Git 提交规范
|
|
||||||
|
|
||||||
```
|
|
||||||
feat: 新功能
|
|
||||||
fix: 修复 Bug
|
|
||||||
docs: 文档更新
|
|
||||||
style: 代码格式调整
|
|
||||||
refactor: 重构
|
|
||||||
perf: 性能优化
|
|
||||||
test: 测试相关
|
|
||||||
chore: 构建/工具配置
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏗️ 生产架构
|
|
||||||
|
|
||||||
### 部署拓扑
|
|
||||||
|
|
||||||
```
|
|
||||||
互联网
|
|
||||||
↓
|
|
||||||
Cloudflare CDN
|
|
||||||
↓
|
|
||||||
Caddy (80/443端口,自动 HTTPS)
|
|
||||||
↓
|
|
||||||
martial.johnsion.club → localhost:5173 (前端 Nginx 容器)
|
|
||||||
├── 静态文件 → 直接返回 Vue 应用
|
|
||||||
└── /blade-auth, /blade-system, /api → 代理到后端 172.21.0.1:8123
|
|
||||||
```
|
|
||||||
|
|
||||||
### 网络架构
|
|
||||||
|
|
||||||
```
|
|
||||||
Docker Network: martial_martial-network (bridge)
|
|
||||||
├── martial-frontend (172.21.0.x) - 端口映射 5173:80
|
|
||||||
├── martial-mysql (172.21.0.x) - 端口映射 33066:3306
|
|
||||||
└── martial-redis (172.21.0.x) - 端口映射 63379:6379
|
|
||||||
|
|
||||||
宿主机:
|
|
||||||
├── Caddy (80/443) - 反向代理服务
|
|
||||||
├── Java 后端 (8123) - martial-master 应用
|
|
||||||
└── Drone CI/CD (8080) - 自动化部署
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔐 安全考虑
|
|
||||||
|
|
||||||
- 生产环境启用 HTTPS 证书(Caddy 自动签发)
|
|
||||||
- API 接口通过 Nginx 代理,隔离后端
|
|
||||||
- 敏感配置通过 Drone Secrets 管理
|
|
||||||
- 容器间网络隔离,仅暴露必要端口
|
|
||||||
|
|
||||||
## 📚 相关链接
|
|
||||||
|
|
||||||
- [后端仓库](https://git.waypeak.work/martial/martial-master)
|
|
||||||
- [BladeX 框架](https://bladex.cn)
|
|
||||||
- [Vue 3 文档](https://cn.vuejs.org/)
|
|
||||||
- [Element Plus](https://element-plus.org/)
|
|
||||||
- [Vite 文档](https://vitejs.dev/)
|
|
||||||
|
|
||||||
## 👥 贡献者
|
|
||||||
|
|
||||||
- **开发者**: JohnSion
|
|
||||||
- **AI 助手**: Claude Code
|
|
||||||
|
|||||||
@@ -196,3 +196,13 @@ export const exportSchedule = (competitionId) => {
|
|||||||
responseType: 'blob'
|
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: '图片直播'
|
||||||
|
}
|
||||||
@@ -141,3 +141,16 @@ export const getOrderAmountStats = (orderId) => {
|
|||||||
params: { orderId }
|
params: { orderId }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单位统计
|
||||||
|
* @param {Number} competitionId - 赛事ID
|
||||||
|
*/
|
||||||
|
export const getOrganizationStats = (competitionId) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/martial/registrationOrder/organization-stats',
|
||||||
|
method: 'get',
|
||||||
|
params: { competitionId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import request from '@/axios';
|
|||||||
* @param {Object} params - 查询参数
|
* @param {Object} params - 查询参数
|
||||||
* @param {String} params.name - 裁判姓名
|
* @param {String} params.name - 裁判姓名
|
||||||
* @param {String} params.phone - 手机号
|
* @param {String} params.phone - 手机号
|
||||||
* @param {Number} params.refereeType - 裁判类型(1-裁判长,2-普通裁判)
|
* @param {Number} params.refereeType - 裁判类型(1-主裁判,2-裁判员)
|
||||||
*/
|
*/
|
||||||
export const getRefereeList = (current, size, params) => {
|
export const getRefereeList = (current, size, params) => {
|
||||||
return request({
|
return request({
|
||||||
@@ -43,7 +43,7 @@ export const getRefereeDetail = (id) => {
|
|||||||
* @param {Number} data.gender - 性别(1-男,2-女)
|
* @param {Number} data.gender - 性别(1-男,2-女)
|
||||||
* @param {String} data.phone - 手机号
|
* @param {String} data.phone - 手机号
|
||||||
* @param {String} data.idCard - 身份证号
|
* @param {String} data.idCard - 身份证号
|
||||||
* @param {Number} data.refereeType - 裁判类型(1-裁判长,2-普通裁判)
|
* @param {Number} data.refereeType - 裁判类型(1-主裁判,2-裁判员)
|
||||||
* @param {String} data.level - 等级/职称
|
* @param {String} data.level - 等级/职称
|
||||||
* @param {String} data.specialty - 擅长项目
|
* @param {String} data.specialty - 擅长项目
|
||||||
* @param {String} data.photoUrl - 照片URL
|
* @param {String} data.photoUrl - 照片URL
|
||||||
|
|||||||
@@ -194,3 +194,27 @@ export const exportSchedulePlans = (params) => {
|
|||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新参赛者签到状态
|
||||||
|
* @param {Number} participantId - 参赛者ID
|
||||||
|
* @param {String} status - 状态:未签到/已签到/异常
|
||||||
|
*/
|
||||||
|
export const updateCheckInStatus = (participantId, status) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/martial/schedule/update-check-in-status',
|
||||||
|
method: 'post',
|
||||||
|
data: { participantId, status }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取赛程配置
|
||||||
|
* @returns {Promise} 返回赛程配置信息
|
||||||
|
*/
|
||||||
|
export const getScheduleConfig = () => {
|
||||||
|
return request({
|
||||||
|
url: '/api/martial/schedule/config',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default [
|
|||||||
redirect: '/martial/order/list',
|
redirect: '/martial/order/list',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'competition/list',
|
path: 'competition/index',
|
||||||
name: '赛事管理',
|
name: '赛事管理',
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
@@ -65,7 +65,7 @@ export default [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'order/list',
|
path: 'order/list',
|
||||||
name: '订单管理',
|
name: '赛事管理',
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -188,7 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 活动日程 -->
|
<!-- 活动日程 -->
|
||||||
<div class="form-section">
|
<!-- <div class="form-section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<i class="el-icon-date"></i>
|
<i class="el-icon-date"></i>
|
||||||
活动日程
|
活动日程
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- 项目列表 -->
|
<!-- 项目列表 -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
@@ -361,23 +361,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
label="参赛人数限制"
|
|
||||||
width="130"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
|
||||||
<el-input-number
|
|
||||||
v-if="mode !== 'view'"
|
|
||||||
v-model="scope.row.maxParticipants"
|
|
||||||
:min="1"
|
|
||||||
:max="9999"
|
|
||||||
size="small"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
<span v-else>{{ scope.row.maxParticipants || '不限' }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="项目说明"
|
label="项目说明"
|
||||||
|
|||||||
@@ -370,8 +370,134 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 活动日程 -->
|
<!-- 附件管理 -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="el-icon-folder-opened"></i>
|
||||||
|
附件管理
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 附件类型选项卡 -->
|
||||||
|
<el-tabs v-model="activeAttachmentTab" type="card">
|
||||||
|
<el-tab-pane
|
||||||
|
v-for="tab in attachmentTabs"
|
||||||
|
:key="tab.type"
|
||||||
|
:label="tab.label"
|
||||||
|
:name="tab.type"
|
||||||
|
>
|
||||||
|
<div class="attachment-section">
|
||||||
|
<!-- 上传按钮 -->
|
||||||
|
<div class="attachment-upload" v-if="currentView !== 'view'">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-upload"
|
||||||
|
size="small"
|
||||||
|
@click="handleOpenAttachmentUpload(tab.type)"
|
||||||
|
>
|
||||||
|
上传{{ tab.label }}附件
|
||||||
|
</el-button>
|
||||||
|
<span class="upload-tip">支持 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">
|
<div class="section-title">
|
||||||
<i class="el-icon-date"></i>
|
<i class="el-icon-date"></i>
|
||||||
活动日程
|
活动日程
|
||||||
@@ -512,7 +638,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- 项目列表 -->
|
<!-- 项目列表 -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
@@ -583,21 +709,24 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="参赛人数限制"
|
label="报名费用(元)"
|
||||||
width="130"
|
width="130"
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-if="currentView !== 'view'"
|
v-if="currentView !== 'view'"
|
||||||
v-model="scope.row.maxParticipants"
|
v-model="scope.row.price"
|
||||||
:min="1"
|
:min="0"
|
||||||
:max="9999"
|
:precision="2"
|
||||||
|
:step="10"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
placeholder="0.00"
|
||||||
/>
|
/>
|
||||||
<span v-else>{{ scope.row.maxParticipants || '不限' }}</span>
|
<span v-else style="color: #f56c6c">¥{{ scope.row.price || 0 }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
@@ -781,6 +910,22 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 附件上传对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
title="上传附件"
|
||||||
|
v-model="attachmentUploadDialogVisible"
|
||||||
|
width="555px"
|
||||||
|
append-to-body
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<avue-form
|
||||||
|
ref="attachmentUploadForm"
|
||||||
|
:option="attachmentUploadOption"
|
||||||
|
v-model="attachmentUploadForm"
|
||||||
|
:upload-after="attachmentUploadAfter"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -806,6 +951,15 @@ import {
|
|||||||
removeVenue,
|
removeVenue,
|
||||||
getVenuesByCompetition
|
getVenuesByCompetition
|
||||||
} from '@/api/martial/venue'
|
} from '@/api/martial/venue'
|
||||||
|
import {
|
||||||
|
getAttachmentsByCompetition,
|
||||||
|
submitAttachment,
|
||||||
|
removeAttachment,
|
||||||
|
batchSubmitAttachments,
|
||||||
|
ATTACHMENT_TYPES,
|
||||||
|
ATTACHMENT_TYPE_LABELS
|
||||||
|
} from '@/api/martial/attachment'
|
||||||
|
import { getToken } from '@/utils/auth'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CompetitionManagement',
|
name: 'CompetitionManagement',
|
||||||
@@ -820,6 +974,40 @@ export default {
|
|||||||
size: 10,
|
size: 10,
|
||||||
total: 0
|
total: 0
|
||||||
},
|
},
|
||||||
|
// 附件相关数据
|
||||||
|
activeAttachmentTab: 'info',
|
||||||
|
attachmentTabs: [
|
||||||
|
{ type: 'info', label: '赛事发布' },
|
||||||
|
{ type: 'rules', label: '赛事规程' },
|
||||||
|
{ type: 'schedule', label: '活动日程' },
|
||||||
|
{ type: 'results', label: '成绩' },
|
||||||
|
{ type: 'medals', label: '奖牌榜' },
|
||||||
|
{ type: 'photos', label: '图片直播' }
|
||||||
|
],
|
||||||
|
attachmentUploadDialogVisible: false,
|
||||||
|
currentAttachmentType: '',
|
||||||
|
attachmentUploadForm: {},
|
||||||
|
attachmentUploadOption: {
|
||||||
|
submitBtn: false,
|
||||||
|
emptyBtn: false,
|
||||||
|
column: [
|
||||||
|
{
|
||||||
|
label: '附件上传',
|
||||||
|
prop: 'attachmentFile',
|
||||||
|
type: 'upload',
|
||||||
|
drag: true,
|
||||||
|
loadText: '文件上传中,请稍等',
|
||||||
|
span: 24,
|
||||||
|
accept: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.zip,.rar',
|
||||||
|
tip: '支持 PDF、Word、Excel、PPT、图片、压缩包等格式,单个文件不超过 50MB',
|
||||||
|
propsHttp: {
|
||||||
|
res: 'data',
|
||||||
|
},
|
||||||
|
|
||||||
|
action: '/blade-resource/oss/endpoint/put-file'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
formData: {
|
formData: {
|
||||||
competitionName: '',
|
competitionName: '',
|
||||||
competitionCode: '', // 比赛编码
|
competitionCode: '', // 比赛编码
|
||||||
@@ -840,7 +1028,15 @@ export default {
|
|||||||
awards: '',
|
awards: '',
|
||||||
schedule: [],
|
schedule: [],
|
||||||
projects: [],
|
projects: [],
|
||||||
venues: []
|
venues: [],
|
||||||
|
attachments: {
|
||||||
|
info: [],
|
||||||
|
rules: [],
|
||||||
|
schedule: [],
|
||||||
|
results: [],
|
||||||
|
medals: [],
|
||||||
|
photos: []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
formRules: {
|
formRules: {
|
||||||
competitionName: [
|
competitionName: [
|
||||||
@@ -988,10 +1184,11 @@ export default {
|
|||||||
try {
|
try {
|
||||||
this.formData = this.formatBackendData(detailData);
|
this.formData = this.formatBackendData(detailData);
|
||||||
console.log('格式化后的表单数据:', this.formData);
|
console.log('格式化后的表单数据:', this.formData);
|
||||||
// 加载关联数据:活动日程、项目列表、场地配置
|
// 加载关联数据:活动日程、项目列表、场地配置、附件
|
||||||
this.loadActivitySchedules();
|
this.loadActivitySchedules();
|
||||||
this.loadProjects();
|
this.loadProjects();
|
||||||
this.loadVenues();
|
this.loadVenues();
|
||||||
|
this.loadAttachments();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('格式化数据时出错:', error);
|
console.error('格式化数据时出错:', error);
|
||||||
this.$message.error('数据格式化失败: ' + error.message);
|
this.$message.error('数据格式化失败: ' + error.message);
|
||||||
@@ -1138,7 +1335,8 @@ export default {
|
|||||||
venueCode: item.venueCode,
|
venueCode: item.venueCode,
|
||||||
capacity: item.capacity,
|
capacity: item.capacity,
|
||||||
location: item.location,
|
location: item.location,
|
||||||
remark: item.facilities || ''
|
remark: item.facilities || '',
|
||||||
|
venueType: item.venueType || 'indoor'
|
||||||
}));
|
}));
|
||||||
console.log('✅ 加载的场地列表:', this.formData.venues);
|
console.log('✅ 加载的场地列表:', this.formData.venues);
|
||||||
console.log('✅ 场地数量:', this.formData.venues.length);
|
console.log('✅ 场地数量:', this.formData.venues.length);
|
||||||
@@ -1154,6 +1352,213 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 加载附件列表
|
||||||
|
loadAttachments() {
|
||||||
|
if (!this.competitionId) {
|
||||||
|
console.warn('loadAttachments: competitionId 为空,跳过加载');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始加载附件列表,competitionId:', this.competitionId);
|
||||||
|
getAttachmentsByCompetition(this.competitionId)
|
||||||
|
.then(res => {
|
||||||
|
console.log('附件列表返回数据:', res);
|
||||||
|
const responseData = res.data?.data;
|
||||||
|
|
||||||
|
// 初始化附件对象
|
||||||
|
const attachments = {
|
||||||
|
info: [],
|
||||||
|
rules: [],
|
||||||
|
schedule: [],
|
||||||
|
results: [],
|
||||||
|
medals: [],
|
||||||
|
photos: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (responseData && Array.isArray(responseData)) {
|
||||||
|
// 按类型分组
|
||||||
|
responseData.forEach(item => {
|
||||||
|
const type = item.attachmentType;
|
||||||
|
if (attachments[type]) {
|
||||||
|
attachments[type].push({
|
||||||
|
id: item.id,
|
||||||
|
fileName: item.fileName,
|
||||||
|
fileUrl: item.fileUrl,
|
||||||
|
fileSize: item.fileSize,
|
||||||
|
fileType: item.fileType,
|
||||||
|
orderNum: item.orderNum || 0,
|
||||||
|
createTime: item.createTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('✅ 加载的附件列表:', attachments);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 附件列表为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formData.attachments = attachments;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('❌ 加载附件列表失败:', err);
|
||||||
|
// 不显示错误消息,因为可能是新建赛事还没有附件
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 打开附件上传对话框
|
||||||
|
handleOpenAttachmentUpload(type) {
|
||||||
|
this.currentAttachmentType = type;
|
||||||
|
this.attachmentUploadForm = {};
|
||||||
|
this.attachmentUploadOption.column[0].headers = { "Blade-Auth": "bearer " + getToken() };
|
||||||
|
this.attachmentUploadDialogVisible = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 附件上传成功回调
|
||||||
|
attachmentUploadAfter(res, done, loading, column) {
|
||||||
|
console.log('附件上传响应:', res);
|
||||||
|
|
||||||
|
if (res && (res.link || res.url)) {
|
||||||
|
const fileUrl = res.link || res.url;
|
||||||
|
const fileName = res.originalName || res.name || this.getFileNameFromUrl(fileUrl);
|
||||||
|
const fileType = this.getFileExtension(fileName);
|
||||||
|
const fileSize = res.size || 0;
|
||||||
|
|
||||||
|
// 添加到对应类型的附件列表
|
||||||
|
if (!this.formData.attachments[this.currentAttachmentType]) {
|
||||||
|
this.formData.attachments[this.currentAttachmentType] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formData.attachments[this.currentAttachmentType].push({
|
||||||
|
fileName: fileName,
|
||||||
|
fileUrl: fileUrl,
|
||||||
|
fileSize: fileSize,
|
||||||
|
fileType: fileType,
|
||||||
|
orderNum: this.formData.attachments[this.currentAttachmentType].length,
|
||||||
|
isNew: true // 标记为新上传的附件
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$message.success('附件上传成功');
|
||||||
|
this.attachmentUploadDialogVisible = false;
|
||||||
|
} else {
|
||||||
|
this.$message.error('上传失败,未获取到文件地址');
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 预览附件
|
||||||
|
handlePreviewAttachment(attachment) {
|
||||||
|
if (!attachment.fileUrl) {
|
||||||
|
this.$message.warning('文件地址不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.open(attachment.fileUrl, '_blank');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除附件
|
||||||
|
handleDeleteAttachment(type, index) {
|
||||||
|
this.$confirm('确定要删除该附件吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
const attachment = this.formData.attachments[type][index];
|
||||||
|
|
||||||
|
// 如果是已保存的附件,需要调用后端删除
|
||||||
|
if (attachment.id) {
|
||||||
|
removeAttachment(attachment.id.toString())
|
||||||
|
.then(() => {
|
||||||
|
this.formData.attachments[type].splice(index, 1);
|
||||||
|
this.$message.success('删除成功');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('删除附件失败:', err);
|
||||||
|
this.$message.error('删除失败');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 新上传的附件直接从列表中移除
|
||||||
|
this.formData.attachments[type].splice(index, 1);
|
||||||
|
this.$message.success('删除成功');
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取文件图标
|
||||||
|
getFileIcon(fileType) {
|
||||||
|
const iconMap = {
|
||||||
|
'pdf': 'el-icon-document',
|
||||||
|
'doc': 'el-icon-document',
|
||||||
|
'docx': 'el-icon-document',
|
||||||
|
'xls': 'el-icon-document',
|
||||||
|
'xlsx': 'el-icon-document',
|
||||||
|
'ppt': 'el-icon-document',
|
||||||
|
'pptx': 'el-icon-document',
|
||||||
|
'jpg': 'el-icon-picture',
|
||||||
|
'jpeg': 'el-icon-picture',
|
||||||
|
'png': 'el-icon-picture',
|
||||||
|
'gif': 'el-icon-picture',
|
||||||
|
'zip': 'el-icon-folder',
|
||||||
|
'rar': 'el-icon-folder'
|
||||||
|
};
|
||||||
|
return iconMap[fileType?.toLowerCase()] || 'el-icon-document';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (!bytes || bytes === 0) return '0 B';
|
||||||
|
if (typeof bytes === 'string') return bytes;
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从URL获取文件名
|
||||||
|
getFileNameFromUrl(url) {
|
||||||
|
if (!url) return 'unknown';
|
||||||
|
const parts = url.split('/');
|
||||||
|
return parts[parts.length - 1] || 'unknown';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取文件扩展名
|
||||||
|
getFileExtension(fileName) {
|
||||||
|
if (!fileName) return '';
|
||||||
|
const parts = fileName.split('.');
|
||||||
|
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存附件
|
||||||
|
async saveAttachments(competitionId) {
|
||||||
|
const allAttachments = [];
|
||||||
|
|
||||||
|
// 收集所有需要保存的附件
|
||||||
|
for (const type of Object.keys(this.formData.attachments)) {
|
||||||
|
const attachments = this.formData.attachments[type] || [];
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
// 只保存新上传的附件或已修改的附件
|
||||||
|
if (attachment.isNew || attachment.isModified) {
|
||||||
|
allAttachments.push({
|
||||||
|
id: attachment.id || null,
|
||||||
|
competitionId: competitionId,
|
||||||
|
attachmentType: type,
|
||||||
|
fileName: attachment.fileName,
|
||||||
|
fileUrl: attachment.fileUrl,
|
||||||
|
fileSize: attachment.fileSize,
|
||||||
|
fileType: attachment.fileType,
|
||||||
|
orderNum: attachment.orderNum || 0,
|
||||||
|
status: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allAttachments.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('准备保存的附件:', allAttachments);
|
||||||
|
return batchSubmitAttachments(allAttachments);
|
||||||
|
},
|
||||||
|
|
||||||
getStatusText(status) {
|
getStatusText(status) {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
1: '未开始',
|
1: '未开始',
|
||||||
@@ -1176,7 +1581,7 @@ export default {
|
|||||||
|
|
||||||
handleCreate() {
|
handleCreate() {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/martial/competition/list',
|
path: '/martial/competition/index',
|
||||||
query: { mode: 'create' }
|
query: { mode: 'create' }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1184,21 +1589,21 @@ export default {
|
|||||||
handleView(row) {
|
handleView(row) {
|
||||||
console.log(row)
|
console.log(row)
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/martial/competition/list',
|
path: '/martial/competition/index',
|
||||||
query: { mode: 'view', id: row.id }
|
query: { mode: 'view', id: row.id }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEdit(row) {
|
handleEdit(row) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/martial/competition/list',
|
path: '/martial/competition/index',
|
||||||
query: { mode: 'edit', id: row.id }
|
query: { mode: 'edit', id: row.id }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
switchToEdit() {
|
switchToEdit() {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/martial/competition/list',
|
path: '/martial/competition/index',
|
||||||
query: { mode: 'edit', id: this.competitionId }
|
query: { mode: 'edit', id: this.competitionId }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1247,6 +1652,7 @@ export default {
|
|||||||
projectCode: '',
|
projectCode: '',
|
||||||
category: '',
|
category: '',
|
||||||
maxParticipants: null,
|
maxParticipants: null,
|
||||||
|
price: 0,
|
||||||
description: ''
|
description: ''
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1311,6 +1717,9 @@ export default {
|
|||||||
savePromises.push(this.saveVenues(savedCompetitionId));
|
savePromises.push(this.saveVenues(savedCompetitionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 保存附件
|
||||||
|
savePromises.push(this.saveAttachments(savedCompetitionId));
|
||||||
|
|
||||||
// 等待所有保存操作完成
|
// 等待所有保存操作完成
|
||||||
if (savePromises.length > 0) {
|
if (savePromises.length > 0) {
|
||||||
Promise.all(savePromises)
|
Promise.all(savePromises)
|
||||||
@@ -1455,7 +1864,8 @@ export default {
|
|||||||
venueCode: venue.venueCode,
|
venueCode: venue.venueCode,
|
||||||
capacity: venue.capacity || 100,
|
capacity: venue.capacity || 100,
|
||||||
location: venue.location || '',
|
location: venue.location || '',
|
||||||
facilities: venue.remark || ''
|
facilities: venue.remark || '',
|
||||||
|
venueType: venue.venueType || 'indoor'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果有 id,说明是编辑已有的场地
|
// 如果有 id,说明是编辑已有的场地
|
||||||
@@ -1472,7 +1882,7 @@ export default {
|
|||||||
|
|
||||||
backToList() {
|
backToList() {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/martial/competition/list'
|
path: '/martial/competition/index'
|
||||||
});
|
});
|
||||||
// 路由跳转后,在 initPage 中会自动加载列表
|
// 路由跳转后,在 initPage 中会自动加载列表
|
||||||
},
|
},
|
||||||
@@ -1498,7 +1908,15 @@ export default {
|
|||||||
awards: '',
|
awards: '',
|
||||||
schedule: [],
|
schedule: [],
|
||||||
projects: [],
|
projects: [],
|
||||||
venues: []
|
venues: [],
|
||||||
|
attachments: {
|
||||||
|
info: [],
|
||||||
|
rules: [],
|
||||||
|
schedule: [],
|
||||||
|
results: [],
|
||||||
|
medals: [],
|
||||||
|
photos: []
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1789,4 +2207,55 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 附件管理样式
|
||||||
|
.attachment-section {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-upload {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-attachment {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
color: #909399;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 活动日程 -->
|
<!-- 活动日程 -->
|
||||||
<div class="form-section">
|
<!-- <div class="form-section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<i class="el-icon-date"></i>
|
<i class="el-icon-date"></i>
|
||||||
活动日程
|
活动日程
|
||||||
@@ -386,7 +386,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div> -->
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
v-for="item in competitionList"
|
v-for="item in competitionList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.competitionName"
|
:label="item.competitionName"
|
||||||
:value="item.id"
|
:value="String(item.id)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
v-for="item in projectList"
|
v-for="item in projectList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.projectName"
|
:label="item.projectName"
|
||||||
:value="item.id"
|
:value="String(item.id)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -146,14 +146,14 @@
|
|||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="deductionPoints"
|
prop="deductionPoint"
|
||||||
label="扣分值(分)"
|
label="扣分值(分)"
|
||||||
width="120"
|
width="120"
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag type="danger" effect="dark">
|
<el-tag type="danger" effect="dark">
|
||||||
-{{ row.deductionPoints }}
|
-{{ row.deductionPoint }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
v-for="item in competitionList"
|
v-for="item in competitionList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.competitionName"
|
:label="item.competitionName"
|
||||||
:value="item.id"
|
:value="String(item.id)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
v-for="item in projectList"
|
v-for="item in projectList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.projectName"
|
:label="item.projectName"
|
||||||
:value="item.id"
|
:value="String(item.id)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -251,9 +251,9 @@
|
|||||||
maxlength="100"
|
maxlength="100"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="扣分值(分)" prop="deductionPoints">
|
<el-form-item label="扣分值(分)" prop="deductionPoint">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="form.deductionPoints"
|
v-model="form.deductionPoint"
|
||||||
:min="0.1"
|
:min="0.1"
|
||||||
:max="10"
|
:max="10"
|
||||||
:precision="1"
|
:precision="1"
|
||||||
@@ -317,7 +317,7 @@
|
|||||||
v-for="item in projectList.filter(p => p.id !== cloneForm.sourceProjectId)"
|
v-for="item in projectList.filter(p => p.id !== cloneForm.sourceProjectId)"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.projectName"
|
:label="item.projectName"
|
||||||
:value="item.id"
|
:value="String(item.id)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -389,6 +389,7 @@ const queryParams = reactive({
|
|||||||
size: 10,
|
size: 10,
|
||||||
competitionId: null,
|
competitionId: null,
|
||||||
projectId: null,
|
projectId: null,
|
||||||
|
itemName: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
@@ -396,7 +397,8 @@ const form = reactive({
|
|||||||
id: null,
|
id: null,
|
||||||
competitionId: null,
|
competitionId: null,
|
||||||
projectId: null,
|
projectId: null,
|
||||||
deductionPoints: 0.5,
|
itemName: '',
|
||||||
|
deductionPoint: 0.5,
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
description: ''
|
description: ''
|
||||||
})
|
})
|
||||||
@@ -421,7 +423,7 @@ const rules = {
|
|||||||
{ required: true, message: '请输入扣分项名称', trigger: 'blur' },
|
{ required: true, message: '请输入扣分项名称', trigger: 'blur' },
|
||||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
|
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
deductionPoints: [
|
deductionPoint: [
|
||||||
{ required: true, message: '请输入扣分值', trigger: 'blur' }
|
{ required: true, message: '请输入扣分值', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
sortOrder: [
|
sortOrder: [
|
||||||
@@ -504,10 +506,17 @@ const fetchData = async () => {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
// 过滤掉空字符串的参数
|
||||||
|
const params = {}
|
||||||
|
Object.keys(queryParams).forEach(key => {
|
||||||
|
if (queryParams[key] !== '' && queryParams[key] !== null && queryParams[key] !== undefined) {
|
||||||
|
params[key] = queryParams[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
const res = await getDeductionList(
|
const res = await getDeductionList(
|
||||||
queryParams.current,
|
queryParams.current,
|
||||||
queryParams.size,
|
queryParams.size,
|
||||||
queryParams
|
params
|
||||||
)
|
)
|
||||||
// 根据axios响应拦截器的处理,数据在 res.data.data 中
|
// 根据axios响应拦截器的处理,数据在 res.data.data 中
|
||||||
const data = res.data?.data || {}
|
const data = res.data?.data || {}
|
||||||
@@ -535,7 +544,7 @@ const handleReset = () => {
|
|||||||
size: 10,
|
size: 10,
|
||||||
competitionId: competitionId,
|
competitionId: competitionId,
|
||||||
projectId: null,
|
projectId: null,
|
||||||
itemName: ''
|
itemName: '',
|
||||||
})
|
})
|
||||||
if (competitionId) {
|
if (competitionId) {
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -555,11 +564,22 @@ const handleAdd = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
const handleEdit = (row) => {
|
const handleEdit = async (row) => {
|
||||||
dialogTitle.value = '编辑扣分项'
|
dialogTitle.value = '编辑扣分项'
|
||||||
Object.keys(form).forEach((key) => {
|
Object.keys(form).forEach((key) => {
|
||||||
form[key] = row[key]
|
form[key] = row[key]
|
||||||
})
|
})
|
||||||
|
// Convert competitionId to string for el-select matching
|
||||||
|
if (form.competitionId) {
|
||||||
|
form.competitionId = String(form.competitionId)
|
||||||
|
}
|
||||||
|
if (form.projectId) {
|
||||||
|
form.projectId = String(form.projectId)
|
||||||
|
}
|
||||||
|
// Load project list for the competition first
|
||||||
|
if (form.competitionId) {
|
||||||
|
await loadProjectList(form.competitionId)
|
||||||
|
}
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,7 +690,7 @@ const resetForm = () => {
|
|||||||
competitionId: null,
|
competitionId: null,
|
||||||
projectId: null,
|
projectId: null,
|
||||||
itemName: '',
|
itemName: '',
|
||||||
deductionPoints: 0.5,
|
deductionPoint: 0.5,
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
description: ''
|
description: ''
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -143,10 +143,15 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column prop="venueName" label="负责场地" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.venueName || "全部场地" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="refereeType" label="裁判类型" align="center">
|
<el-table-column prop="refereeType" label="裁判类型" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.refereeType === 1 ? 'danger' : 'primary'" size="small">
|
<el-tag :type="row.refereeType === 1 ? 'danger' : (row.refereeType === 3 ? 'warning' : 'primary')" size="small">
|
||||||
{{ row.refereeType === 1 ? '主裁判' : '普通裁判' }}
|
{{ row.refereeType === 1 ? '主裁判' : (row.refereeType === 3 ? '总裁' : '裁判员') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -185,6 +190,26 @@
|
|||||||
width="900px"
|
width="900px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
|
<!-- 场地和项目选择 -->
|
||||||
|
<el-form :inline="true" class="venue-project-form" style="margin-bottom: 15px; padding: 15px; background: #f5f7fa; border-radius: 8px;">
|
||||||
|
<el-form-item label="分配场地" required>
|
||||||
|
<el-select
|
||||||
|
v-model="importForm.venueId"
|
||||||
|
placeholder="请选择场地"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in venueList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.venueName"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<el-form :inline="true" :model="judgeQueryParams" class="judge-search-form">
|
<el-form :inline="true" :model="judgeQueryParams" class="judge-search-form">
|
||||||
<el-form-item label="姓名">
|
<el-form-item label="姓名">
|
||||||
@@ -211,7 +236,8 @@
|
|||||||
style="width: 130px"
|
style="width: 130px"
|
||||||
>
|
>
|
||||||
<el-option label="主裁判" :value="1" />
|
<el-option label="主裁判" :value="1" />
|
||||||
<el-option label="普通裁判" :value="2" />
|
<el-option label="裁判员" :value="2" />
|
||||||
|
<el-option label="总裁" :value="3" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -241,8 +267,8 @@
|
|||||||
<el-table-column prop="phone" label="手机号" width="130" />
|
<el-table-column prop="phone" label="手机号" width="130" />
|
||||||
<el-table-column prop="refereeType" label="裁判类型" width="100" align="center">
|
<el-table-column prop="refereeType" label="裁判类型" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.refereeType === 1 ? 'danger' : 'primary'" size="small">
|
<el-tag :type="row.refereeType === 1 ? 'danger' : (row.refereeType === 3 ? 'warning' : 'primary')" size="small">
|
||||||
{{ row.refereeType === 1 ? '主裁判' : '普通裁判' }}
|
{{ row.refereeType === 1 ? '主裁判' : (row.refereeType === 3 ? '总裁' : '裁判员') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -303,6 +329,8 @@ import {
|
|||||||
removeInvite
|
removeInvite
|
||||||
} from '@/api/martial/judgeInvite'
|
} from '@/api/martial/judgeInvite'
|
||||||
import { getCompetitionList } from '@/api/martial/competition'
|
import { getCompetitionList } from '@/api/martial/competition'
|
||||||
|
import { getVenuesByCompetition } from '@/api/martial/venue'
|
||||||
|
import { getProjectsByCompetition } from '@/api/martial/project'
|
||||||
import { getRefereeList } from '@/api/martial/referee'
|
import { getRefereeList } from '@/api/martial/referee'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
@@ -316,6 +344,17 @@ const competitionLoading = ref(false) // 赛事列表加载状态
|
|||||||
|
|
||||||
// 裁判选择对话框
|
// 裁判选择对话框
|
||||||
const judgeDialogVisible = ref(false)
|
const judgeDialogVisible = ref(false)
|
||||||
|
|
||||||
|
// 场地和项目列表
|
||||||
|
const venueList = ref([])
|
||||||
|
const filterVenueList = ref([])
|
||||||
|
const projectList = ref([])
|
||||||
|
|
||||||
|
// 导入表单
|
||||||
|
const importForm = reactive({
|
||||||
|
venueId: null,
|
||||||
|
projectIds: []
|
||||||
|
})
|
||||||
const judgeLoading = ref(false)
|
const judgeLoading = ref(false)
|
||||||
const judgeList = ref([])
|
const judgeList = ref([])
|
||||||
const judgeTotal = ref(0)
|
const judgeTotal = ref(0)
|
||||||
@@ -343,7 +382,8 @@ const queryParams = reactive({
|
|||||||
competitionId: null,
|
competitionId: null,
|
||||||
judgeName: '',
|
judgeName: '',
|
||||||
judgeLevel: '',
|
judgeLevel: '',
|
||||||
inviteStatus: ''
|
inviteStatus: '',
|
||||||
|
venueId: null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载赛事列表
|
// 加载赛事列表
|
||||||
@@ -379,11 +419,31 @@ const loadCompetitionList = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 赛事切换
|
// 赛事切换
|
||||||
const handleCompetitionChange = (competitionId) => {
|
const handleCompetitionChange = async (competitionId) => {
|
||||||
|
// 重置场地筛选
|
||||||
|
queryParams.venueId = null
|
||||||
|
// 加载该赛事的场地列表用于筛选
|
||||||
|
await loadFilterVenueList()
|
||||||
fetchData()
|
fetchData()
|
||||||
loadStatistics()
|
loadStatistics()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载筛选用的场地列表
|
||||||
|
const loadFilterVenueList = async () => {
|
||||||
|
if (!queryParams.competitionId) {
|
||||||
|
filterVenueList.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getVenuesByCompetition(queryParams.competitionId)
|
||||||
|
const venueData = res.data?.data || res.data || {}
|
||||||
|
filterVenueList.value = venueData.records || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载场地列表失败:', error)
|
||||||
|
filterVenueList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载统计数据
|
// 加载统计数据
|
||||||
const loadStatistics = async () => {
|
const loadStatistics = async () => {
|
||||||
if (queryParams.competitionId === null || queryParams.competitionId === '') return
|
if (queryParams.competitionId === null || queryParams.competitionId === '') return
|
||||||
@@ -434,7 +494,8 @@ const handleReset = () => {
|
|||||||
size: 10,
|
size: 10,
|
||||||
judgeName: '',
|
judgeName: '',
|
||||||
judgeLevel: '',
|
judgeLevel: '',
|
||||||
inviteStatus: ''
|
inviteStatus: '',
|
||||||
|
venueId: null
|
||||||
})
|
})
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
@@ -454,12 +515,48 @@ const handleImportFromPool = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置导入表单
|
||||||
|
importForm.venueId = null
|
||||||
|
importForm.projectIds = []
|
||||||
|
|
||||||
|
// 加载场地和项目列表
|
||||||
|
await loadVenueAndProjectList()
|
||||||
|
|
||||||
// 打开裁判选择对话框
|
// 打开裁判选择对话框
|
||||||
judgeDialogVisible.value = true
|
judgeDialogVisible.value = true
|
||||||
selectedJudges.value = []
|
selectedJudges.value = []
|
||||||
loadJudgeList()
|
loadJudgeList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载场地和项目列表
|
||||||
|
const loadVenueAndProjectList = async () => {
|
||||||
|
try {
|
||||||
|
// 并行加载场地和项目
|
||||||
|
const [venueRes, projectRes] = await Promise.all([
|
||||||
|
getVenuesByCompetition(queryParams.competitionId),
|
||||||
|
getProjectsByCompetition(queryParams.competitionId)
|
||||||
|
])
|
||||||
|
|
||||||
|
// 处理场地数据
|
||||||
|
const venueData = venueRes.data?.data || venueRes.data || {}
|
||||||
|
venueList.value = venueData.records || []
|
||||||
|
|
||||||
|
// 处理项目数据
|
||||||
|
const projectData = projectRes.data?.data || projectRes.data || {}
|
||||||
|
projectList.value = projectData.records || []
|
||||||
|
|
||||||
|
if (venueList.value.length === 0) {
|
||||||
|
ElMessage.warning('该赛事暂无场地,请先添加场地')
|
||||||
|
}
|
||||||
|
if (projectList.value.length === 0) {
|
||||||
|
ElMessage.warning('该赛事暂无项目,请先添加项目')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载场地/项目列表失败:', error)
|
||||||
|
ElMessage.error('加载场地/项目列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载裁判列表
|
// 加载裁判列表
|
||||||
const loadJudgeList = async () => {
|
const loadJudgeList = async () => {
|
||||||
judgeLoading.value = true
|
judgeLoading.value = true
|
||||||
@@ -523,6 +620,13 @@ const handleConfirmImport = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证场地和项目
|
||||||
|
if (!importForm.venueId) {
|
||||||
|
ElMessage.warning('请选择分配的场地')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`确定为选中的 ${selectedJudges.value.length} 位裁判生成邀请码吗?`,
|
`确定为选中的 ${selectedJudges.value.length} 位裁判生成邀请码吗?`,
|
||||||
@@ -541,6 +645,8 @@ const handleConfirmImport = async () => {
|
|||||||
competitionId: queryParams.competitionId,
|
competitionId: queryParams.competitionId,
|
||||||
judgeIds: judgeIds,
|
judgeIds: judgeIds,
|
||||||
role: 'judge',
|
role: 'judge',
|
||||||
|
venueId: importForm.venueId,
|
||||||
|
// projects不传,裁判默认负责整个场地
|
||||||
expireDays: 30
|
expireDays: 30
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -625,7 +731,7 @@ const handleGenerateCode = async (row) => {
|
|||||||
const res = await generateInviteCode({
|
const res = await generateInviteCode({
|
||||||
competitionId: queryParams.competitionId,
|
competitionId: queryParams.competitionId,
|
||||||
judgeId: row.judgeId,
|
judgeId: row.judgeId,
|
||||||
role: row.refereeType === 1 ? 'chief_judge' : 'judge', // 根据评委类型设置角色
|
role: row.refereeType === 1 ? 'chief_judge' : (row.refereeType === 3 ? 'general_judge' : 'judge'), // 根据评委类型设置角色
|
||||||
venueId: row.venueId || null,
|
venueId: row.venueId || null,
|
||||||
projects: row.projects ? JSON.stringify(row.projects) : null,
|
projects: row.projects ? JSON.stringify(row.projects) : null,
|
||||||
expireDays: 30
|
expireDays: 30
|
||||||
@@ -821,8 +927,12 @@ const fallbackCopyToClipboard = (text, label) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 挂载
|
// 挂载
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
loadCompetitionList()
|
await loadCompetitionList()
|
||||||
|
// 加载筛选用的场地列表
|
||||||
|
if (queryParams.competitionId) {
|
||||||
|
await loadFilterVenueList()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="martial-order-container">
|
<div class="martial-order-container">
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2 class="page-title">订单管理</h2>
|
<h2 class="page-title">赛事管理</h2>
|
||||||
</div>
|
</div>
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="getStatusType(scope.row.status)" size="small">
|
<el-tag :type="getStatusType(calculateStatus(scope.row))" size="small">
|
||||||
{{ getStatusText(scope.row.status) }}
|
{{ getStatusText(calculateStatus(scope.row)) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -252,6 +252,19 @@ export default {
|
|||||||
return `${start} ~ ${end}`
|
return `${start} ~ ${end}`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 根据时间计算赛事状态
|
||||||
|
calculateStatus(row) {
|
||||||
|
const now = new Date()
|
||||||
|
const regStart = row.registrationStartTime ? new Date(row.registrationStartTime) : null
|
||||||
|
const regEnd = row.registrationEndTime ? new Date(row.registrationEndTime) : null
|
||||||
|
const compStart = row.competitionStartTime ? new Date(row.competitionStartTime) : null
|
||||||
|
const compEnd = row.competitionEndTime ? new Date(row.competitionEndTime) : null
|
||||||
|
if (compEnd && now > compEnd) return 4
|
||||||
|
if (compStart && now >= compStart) return 3
|
||||||
|
if (regStart && regEnd && now >= regStart && now <= regEnd) return 2
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
|
||||||
getStatusType(status) {
|
getStatusType(status) {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
1: 'info', // 未开始
|
1: 'info', // 未开始
|
||||||
|
|||||||
@@ -28,17 +28,12 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分组类别">
|
<el-form-item label="分组类别">
|
||||||
<el-select
|
<el-input
|
||||||
v-model="queryParams.category"
|
v-model="queryParams.category"
|
||||||
placeholder="请选择分组类别"
|
placeholder="请输入分组类别"
|
||||||
clearable
|
clearable
|
||||||
style="width: 150px"
|
style="width: 150px"
|
||||||
>
|
/>
|
||||||
<el-option label="男子" value="1" />
|
|
||||||
<el-option label="女子" value="2" />
|
|
||||||
<el-option label="团体" value="3" />
|
|
||||||
<el-option label="混合" value="4" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="项目类型">
|
<el-form-item label="项目类型">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -47,15 +42,15 @@
|
|||||||
clearable
|
clearable
|
||||||
style="width: 150px"
|
style="width: 150px"
|
||||||
>
|
>
|
||||||
<el-option label="套路" value="1" />
|
<el-option label="套路" :value="1" />
|
||||||
<el-option label="散打" value="2" />
|
<el-option label="散打" :value="2" />
|
||||||
<el-option label="器械" value="3" />
|
<el-option label="器械" :value="3" />
|
||||||
<el-option label="对练" value="4" />
|
<el-option label="对练" :value="4" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="参赛类型">
|
<el-form-item label="参赛类型">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.participantType"
|
v-model="queryParams.type"
|
||||||
placeholder="请选择参赛类型"
|
placeholder="请选择参赛类型"
|
||||||
clearable
|
clearable
|
||||||
style="width: 150px"
|
style="width: 150px"
|
||||||
@@ -133,66 +128,53 @@
|
|||||||
min-width="180"
|
min-width="180"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
<el-table-column label="所属赛事" min-width="150" show-overflow-tooltip>
|
||||||
prop="competitionName"
|
<template #default="{ row }">
|
||||||
label="所属赛事"
|
{{ getCompetitionName(row.competitionId) }}
|
||||||
min-width="150"
|
</template>
|
||||||
show-overflow-tooltip
|
</el-table-column>
|
||||||
/>
|
|
||||||
<el-table-column prop="category" label="分组类别" width="100" align="center">
|
<el-table-column prop="category" label="分组类别" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.category === 1" type="primary">男子</el-tag>
|
<span>{{ row.category || '-' }}</span>
|
||||||
<el-tag v-else-if="row.category === 2" type="danger">女子</el-tag>
|
|
||||||
<el-tag v-else-if="row.category === 3" type="success">团体</el-tag>
|
|
||||||
<el-tag v-else-if="row.category === 4" type="warning">混合</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="eventType" label="项目类型" width="100" align="center">
|
<el-table-column prop="eventType" label="项目类型" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.eventType === 1">套路</span>
|
<el-tag v-if="row.eventType === 1" type="primary" size="small">套路</el-tag>
|
||||||
<span v-else-if="row.eventType === 2">散打</span>
|
<el-tag v-else-if="row.eventType === 2" type="danger" size="small">散打</el-tag>
|
||||||
<span v-else-if="row.eventType === 3">器械</span>
|
<el-tag v-else-if="row.eventType === 3" type="success" size="small">器械</el-tag>
|
||||||
<span v-else-if="row.eventType === 4">对练</span>
|
<el-tag v-else-if="row.eventType === 4" type="warning" size="small">对练</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="participantType" label="参赛类型" width="100" align="center">
|
<el-table-column prop="type" label="参赛类型" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.participantType === 1" type="success" size="small">单人</el-tag>
|
<el-tag v-if="row.type === 1" type="success" size="small">单人</el-tag>
|
||||||
<el-tag v-else-if="row.participantType === 2" type="warning" size="small">集体</el-tag>
|
<el-tag v-else-if="row.type === 2" type="warning" size="small">集体</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="registrationFee"
|
prop="price"
|
||||||
label="报名费(元)"
|
label="报名费(元)"
|
||||||
width="110"
|
width="110"
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span style="color: #f56c6c">¥{{ row.registrationFee || 0 }}</span>
|
<span style="color: #f56c6c">¥{{ row.price || 0 }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="报名时间" width="180" align="center">
|
|
||||||
|
<el-table-column prop="estimatedDuration" label="预计时长" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-if="row.registrationStartTime && row.registrationEndTime">
|
<span>{{ row.estimatedDuration || 5 }}分钟</span>
|
||||||
<div style="font-size: 12px">
|
|
||||||
{{ formatDate(row.registrationStartTime) }}
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 12px">至</div>
|
|
||||||
<div style="font-size: 12px">
|
|
||||||
{{ formatDate(row.registrationEndTime) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span v-else style="color: #909399">未设置</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="报名人数" width="120" align="center">
|
<el-table-column label="单位容纳人数" width="120" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :style="{ color: row.currentCount >= row.maxParticipants ? '#f56c6c' : '#67c23a' }">
|
{{ row.maxParticipants || 0 }}
|
||||||
{{ row.currentCount || 0 }}
|
|
||||||
</span>
|
|
||||||
/ {{ row.maxParticipants || 0 }}
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="createTime"
|
prop="createTime"
|
||||||
label="创建时间"
|
label="创建时间"
|
||||||
@@ -264,15 +246,6 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="项目编码" prop="projectCode">
|
|
||||||
<el-input
|
|
||||||
v-model="form.projectCode"
|
|
||||||
placeholder="请输入项目编码"
|
|
||||||
maxlength="50"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -315,9 +288,9 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="参赛类型" prop="participantType">
|
<el-form-item label="参赛类型" prop="type">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.participantType"
|
v-model="form.type"
|
||||||
placeholder="请选择参赛类型"
|
placeholder="请选择参赛类型"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
@@ -328,6 +301,17 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="预计时长(分钟)" prop="estimatedDuration">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.estimatedDuration"
|
||||||
|
:min="1"
|
||||||
|
:max="120"
|
||||||
|
placeholder="每人/队预计比赛时长"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="报名费(元)" prop="registrationFee">
|
<el-form-item label="报名费(元)" prop="registrationFee">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
@@ -342,33 +326,7 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="报名开始时间" prop="registrationStartTime">
|
<el-form-item label="单位容纳人数" prop="maxParticipants">
|
||||||
<el-date-picker
|
|
||||||
v-model="form.registrationStartTime"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="选择开始时间"
|
|
||||||
style="width: 100%"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="报名结束时间" prop="registrationEndTime">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="form.registrationEndTime"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="选择结束时间"
|
|
||||||
style="width: 100%"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="最大参赛人数" prop="maxParticipants">
|
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="form.maxParticipants"
|
v-model="form.maxParticipants"
|
||||||
:min="1"
|
:min="1"
|
||||||
@@ -426,7 +384,7 @@
|
|||||||
{{ detailData.projectName }}
|
{{ detailData.projectName }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="所属赛事">
|
<el-descriptions-item label="所属赛事">
|
||||||
{{ detailData.competitionName }}
|
{{ getCompetitionName(detailData.competitionId) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="分组类别">
|
<el-descriptions-item label="分组类别">
|
||||||
<el-tag v-if="detailData.category === 1" type="primary">男子</el-tag>
|
<el-tag v-if="detailData.category === 1" type="primary">男子</el-tag>
|
||||||
@@ -441,24 +399,18 @@
|
|||||||
<span v-else-if="detailData.eventType === 4">对练</span>
|
<span v-else-if="detailData.eventType === 4">对练</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="参赛类型">
|
<el-descriptions-item label="参赛类型">
|
||||||
<el-tag v-if="detailData.participantType === 1" type="success" size="small">单人</el-tag>
|
<el-tag v-if="detailData.type === 1" type="success" size="small">单人</el-tag>
|
||||||
<el-tag v-else-if="detailData.participantType === 2" type="warning" size="small">集体</el-tag>
|
<el-tag v-else-if="detailData.type === 2" type="warning" size="small">集体</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="报名费">
|
<el-descriptions-item label="报名费">
|
||||||
<span style="color: #f56c6c; font-weight: bold">
|
<span style="color: #f56c6c; font-weight: bold">
|
||||||
¥{{ detailData.registrationFee || 0 }}
|
¥{{ detailData.registrationFee || 0 }}
|
||||||
</span>
|
</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="报名开始时间">
|
<el-descriptions-item label="单位容纳人数">
|
||||||
{{ formatDate(detailData.registrationStartTime) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="报名结束时间">
|
|
||||||
{{ formatDate(detailData.registrationEndTime) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="最大参赛人数">
|
|
||||||
{{ detailData.maxParticipants }}
|
{{ detailData.maxParticipants }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="当前报名人数">
|
<el-descriptions-item label="已报名人数">
|
||||||
<span :style="{
|
<span :style="{
|
||||||
color: detailData.currentCount >= detailData.maxParticipants ? '#f56c6c' : '#67c23a',
|
color: detailData.currentCount >= detailData.maxParticipants ? '#f56c6c' : '#67c23a',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
@@ -495,8 +447,7 @@ import {
|
|||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getProjectList,
|
getProjectList,
|
||||||
addProject,
|
submitProject,
|
||||||
updateProject,
|
|
||||||
removeProject,
|
removeProject,
|
||||||
importProjects,
|
importProjects,
|
||||||
exportProjects
|
exportProjects
|
||||||
@@ -526,7 +477,7 @@ const queryParams = reactive({
|
|||||||
projectName: '',
|
projectName: '',
|
||||||
category: '',
|
category: '',
|
||||||
eventType: '',
|
eventType: '',
|
||||||
participantType: ''
|
type: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
@@ -537,10 +488,9 @@ const form = reactive({
|
|||||||
projectName: '',
|
projectName: '',
|
||||||
category: null,
|
category: null,
|
||||||
eventType: null,
|
eventType: null,
|
||||||
participantType: null,
|
type: null,
|
||||||
|
estimatedDuration: 5,
|
||||||
registrationFee: 0,
|
registrationFee: 0,
|
||||||
registrationStartTime: '',
|
|
||||||
registrationEndTime: '',
|
|
||||||
maxParticipants: 100,
|
maxParticipants: 100,
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
rules: '',
|
rules: '',
|
||||||
@@ -562,10 +512,6 @@ const rules = {
|
|||||||
competitionId: [
|
competitionId: [
|
||||||
{ required: true, message: '请选择所属赛事', trigger: 'change' }
|
{ required: true, message: '请选择所属赛事', trigger: 'change' }
|
||||||
],
|
],
|
||||||
projectCode: [
|
|
||||||
{ required: true, message: '请输入项目编码', trigger: 'blur' },
|
|
||||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
projectName: [
|
projectName: [
|
||||||
{ required: true, message: '请输入项目名称', trigger: 'blur' },
|
{ required: true, message: '请输入项目名称', trigger: 'blur' },
|
||||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
|
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
|
||||||
@@ -576,39 +522,26 @@ const rules = {
|
|||||||
eventType: [
|
eventType: [
|
||||||
{ required: true, message: '请选择项目类型', trigger: 'change' }
|
{ required: true, message: '请选择项目类型', trigger: 'change' }
|
||||||
],
|
],
|
||||||
participantType: [
|
estimatedDuration: [
|
||||||
|
{ required: true, message: '请输入预计时长', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
type: [
|
||||||
{ required: true, message: '请选择参赛类型', trigger: 'change' }
|
{ required: true, message: '请选择参赛类型', trigger: 'change' }
|
||||||
],
|
],
|
||||||
registrationFee: [
|
registrationFee: [
|
||||||
{ required: true, message: '请输入报名费', trigger: 'blur' }
|
{ required: true, message: '请输入报名费', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
registrationStartTime: [
|
|
||||||
{ required: true, message: '请选择报名开始时间', trigger: 'change' }
|
|
||||||
],
|
|
||||||
registrationEndTime: [
|
|
||||||
{ required: true, message: '请选择报名结束时间', trigger: 'change' },
|
|
||||||
{
|
|
||||||
validator: (rule, value, callback) => {
|
|
||||||
if (value && form.registrationStartTime && value <= form.registrationStartTime) {
|
|
||||||
callback(new Error('结束时间必须大于开始时间'))
|
|
||||||
} else {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trigger: 'change'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
maxParticipants: [
|
maxParticipants: [
|
||||||
{ required: true, message: '请输入最大参赛人数', trigger: 'blur' }
|
{ required: true, message: '请输入单位容纳人数', trigger: 'blur' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载赛事列表
|
// 加载赛事列表
|
||||||
const loadCompetitionList = async () => {
|
const loadCompetitionList = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getCompetitionList(1, 1000, { status: 1 })
|
const res = await getCompetitionList(1, 1000, {})
|
||||||
if (res.data && res.data.records) {
|
if (res.data && res.data.data && res.data.data.records) {
|
||||||
competitionList.value = res.data.records
|
competitionList.value = res.data.data.records
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载赛事列表失败:', error)
|
console.error('加载赛事列表失败:', error)
|
||||||
@@ -619,14 +552,22 @@ const loadCompetitionList = async () => {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
// Only pass parameters that backend supports
|
||||||
|
const params = {
|
||||||
|
competitionId: queryParams.competitionId || undefined,
|
||||||
|
projectName: queryParams.projectName || undefined,
|
||||||
|
category: queryParams.category || undefined,
|
||||||
|
eventType: queryParams.eventType || undefined,
|
||||||
|
type: queryParams.type || undefined
|
||||||
|
}
|
||||||
const res = await getProjectList(
|
const res = await getProjectList(
|
||||||
queryParams.current,
|
queryParams.current,
|
||||||
queryParams.size,
|
queryParams.size,
|
||||||
queryParams
|
params
|
||||||
)
|
)
|
||||||
if (res.data) {
|
if (res.data && res.data.data) {
|
||||||
tableData.value = res.data.records || []
|
tableData.value = res.data.data.records || []
|
||||||
total.value = res.data.total || 0
|
total.value = res.data.data.total || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取数据失败')
|
ElMessage.error('获取数据失败')
|
||||||
@@ -651,7 +592,7 @@ const handleReset = () => {
|
|||||||
projectName: '',
|
projectName: '',
|
||||||
category: '',
|
category: '',
|
||||||
eventType: '',
|
eventType: '',
|
||||||
participantType: ''
|
type: ''
|
||||||
})
|
})
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
@@ -664,11 +605,15 @@ const handleAdd = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
const handleEdit = (row) => {
|
const handleEdit = async (row) => {
|
||||||
dialogTitle.value = '编辑项目'
|
dialogTitle.value = '编辑项目'
|
||||||
Object.keys(form).forEach((key) => {
|
Object.keys(form).forEach((key) => {
|
||||||
form[key] = row[key]
|
form[key] = row[key]
|
||||||
})
|
})
|
||||||
|
// 处理字段名映射:后端返回 price,表单使用 registrationFee
|
||||||
|
if (row.price !== undefined) {
|
||||||
|
form.registrationFee = row.price
|
||||||
|
}
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,11 +670,16 @@ const handleSubmit = async () => {
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
// 构建提交数据,确保字段名与后端一致
|
||||||
|
const submitData = {
|
||||||
|
...form,
|
||||||
|
price: form.registrationFee // 后端使用 price 字段
|
||||||
|
}
|
||||||
if (form.id) {
|
if (form.id) {
|
||||||
await updateProject(form)
|
await submitProject(submitData)
|
||||||
ElMessage.success('修改成功')
|
ElMessage.success('修改成功')
|
||||||
} else {
|
} else {
|
||||||
await addProject(form)
|
await submitProject(submitData)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
}
|
}
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
@@ -762,10 +712,9 @@ const resetForm = () => {
|
|||||||
projectName: '',
|
projectName: '',
|
||||||
category: null,
|
category: null,
|
||||||
eventType: null,
|
eventType: null,
|
||||||
participantType: null,
|
type: null,
|
||||||
|
estimatedDuration: 5,
|
||||||
registrationFee: 0,
|
registrationFee: 0,
|
||||||
registrationStartTime: '',
|
|
||||||
registrationEndTime: '',
|
|
||||||
maxParticipants: 100,
|
maxParticipants: 100,
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
rules: '',
|
rules: '',
|
||||||
@@ -830,6 +779,13 @@ const handleExport = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据ID获取赛事名称
|
||||||
|
const getCompetitionName = (competitionId) => {
|
||||||
|
if (!competitionId) return '-'
|
||||||
|
const competition = competitionList.value.find(item => item.id === competitionId)
|
||||||
|
return competition ? competition.competitionName : '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
const formatDate = (date) => {
|
const formatDate = (date) => {
|
||||||
if (!date) return '-'
|
if (!date) return '-'
|
||||||
|
|||||||
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-form-item>
|
||||||
<el-select v-model="searchForm.refereeType" placeholder="裁判类型" clearable size="small" style="width: 150px">
|
<el-select v-model="searchForm.refereeType" placeholder="裁判类型" clearable size="small" style="width: 150px">
|
||||||
<el-option label="全部" :value="null"></el-option>
|
<el-option label="全部" :value="null"></el-option>
|
||||||
<el-option label="裁判长" :value="1"></el-option>
|
<el-option label="主裁判" :value="1"></el-option>
|
||||||
<el-option label="普通裁判" :value="2"></el-option>
|
<el-option label="裁判员" :value="2"></el-option>
|
||||||
|
<el-option label="总裁" :value="3"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -63,8 +64,8 @@
|
|||||||
<el-table-column prop="idCard" label="身份证号" width="180" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="idCard" label="身份证号" width="180" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column prop="refereeType" label="裁判类型" width="100" align="center">
|
<el-table-column prop="refereeType" label="裁判类型" width="100" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.refereeType === 1 ? 'danger' : 'primary'" size="small">
|
<el-tag :type="scope.row.refereeType === 1 ? 'danger' : (scope.row.refereeType === 3 ? 'warning' : 'primary')" size="small">
|
||||||
{{ scope.row.refereeType === 1 ? '主裁判' : '普通裁判' }}
|
{{ scope.row.refereeType === 1 ? '主裁判' : (scope.row.refereeType === 3 ? '总裁' : '裁判员') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -137,7 +138,8 @@
|
|||||||
<el-form-item label="裁判类型" prop="refereeType">
|
<el-form-item label="裁判类型" prop="refereeType">
|
||||||
<el-select v-model="formData.refereeType" placeholder="请选择裁判类型" style="width: 100%">
|
<el-select v-model="formData.refereeType" placeholder="请选择裁判类型" style="width: 100%">
|
||||||
<el-option label="主裁判" :value="1"></el-option>
|
<el-option label="主裁判" :value="1"></el-option>
|
||||||
<el-option label="普通裁判" :value="2"></el-option>
|
<el-option label="裁判员" :value="2"></el-option>
|
||||||
|
<el-option label="总裁" :value="3"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|||||||
@@ -101,12 +101,10 @@
|
|||||||
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
|
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="category" label="类别" width="80"></el-table-column>
|
<el-table-column prop="singleCount" label="单人项目" width="100" align="center"></el-table-column>
|
||||||
<el-table-column prop="individual" label="2队" width="60" align="center"></el-table-column>
|
<el-table-column prop="teamCount" label="集体项目" width="100" align="center"></el-table-column>
|
||||||
<el-table-column prop="dual" label="2组" width="60" align="center"></el-table-column>
|
<el-table-column prop="male" label="男" width="60" align="center"></el-table-column>
|
||||||
<el-table-column prop="team1101" label="1101" width="60" align="center"></el-table-column>
|
<el-table-column prop="female" label="女" width="60" align="center"></el-table-column>
|
||||||
<el-table-column prop="workers" label="工作人员" width="90" align="center"></el-table-column>
|
|
||||||
<el-table-column prop="female" label="女性组别" width="90" align="center"></el-table-column>
|
|
||||||
<el-table-column prop="total" label="合计" width="60" align="center">
|
<el-table-column prop="total" label="合计" width="60" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span class="total-num">+{{ scope.row.total }}</span>
|
<span class="total-num">+{{ scope.row.total }}</span>
|
||||||
@@ -125,10 +123,11 @@
|
|||||||
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
|
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="participantCategory" label="参人/单位" width="100"></el-table-column>
|
<el-table-column prop="projectType" label="单人/集体" width="100" align="center"></el-table-column>
|
||||||
<el-table-column prop="teamCount" label="队伍" width="80" align="center"></el-table-column>
|
<el-table-column prop="athleteCount" label="人数/集体" width="100" align="center"></el-table-column>
|
||||||
<el-table-column prop="singleTeamPeople" label="单位型号人数" width="120" align="center"></el-table-column>
|
<el-table-column prop="groupCount" label="组数" width="80" align="center"></el-table-column>
|
||||||
<el-table-column prop="estimatedDuration" label="剩下显时(公共)" width="150" align="center"></el-table-column>
|
<el-table-column prop="estimatedDuration" label="时长(分)" width="100" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="projectCode" label="项目编码" width="120" align="center"></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -335,76 +334,85 @@ export default {
|
|||||||
const participants = await this.getParticipants()
|
const participants = await this.getParticipants()
|
||||||
console.log('参赛人员列表返回:', participants)
|
console.log('参赛人员列表返回:', participants)
|
||||||
|
|
||||||
|
// 预加载项目信息
|
||||||
|
await this.preloadProjectInfo(participants)
|
||||||
|
|
||||||
// 按单位分组统计
|
// 按单位分组统计
|
||||||
const unitMap = new Map()
|
const unitMap = new Map()
|
||||||
participants.forEach(p => {
|
participants.forEach(p => {
|
||||||
// 兼容驼峰和下划线命名
|
// 兼容驼峰和下划线命名
|
||||||
const unit = p.organization || p.teamName || p.team_name || '未知单位'
|
const unit = p.organization || p.teamName || p.team_name || '未知单位'
|
||||||
|
const projectId = p.projectId || p.project_id
|
||||||
|
const project = this.projectCache.get(projectId)
|
||||||
|
const projectType = project ? (project.type || 1) : 1 // 1=单人, 2=集体
|
||||||
|
|
||||||
if (!unitMap.has(unit)) {
|
if (!unitMap.has(unit)) {
|
||||||
unitMap.set(unit, {
|
unitMap.set(unit, {
|
||||||
schoolUnit: unit,
|
schoolUnit: unit,
|
||||||
category: '',
|
singleCount: 0,
|
||||||
individual: 0,
|
teamCount: 0,
|
||||||
dual: 0,
|
male: 0,
|
||||||
team1101: 0,
|
|
||||||
workers: 0,
|
|
||||||
female: 0,
|
female: 0,
|
||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const stat = unitMap.get(unit)
|
const stat = unitMap.get(unit)
|
||||||
stat.total++
|
stat.total++
|
||||||
if (p.gender === 2) stat.female++
|
|
||||||
|
// 按项目类型统计
|
||||||
|
if (projectType === 1) {
|
||||||
|
stat.singleCount++
|
||||||
|
} else {
|
||||||
|
stat.teamCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按性别统计
|
||||||
|
if (p.gender === 1) {
|
||||||
|
stat.male++
|
||||||
|
} else if (p.gender === 2) {
|
||||||
|
stat.female++
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.participantsData = Array.from(unitMap.values())
|
this.participantsData = Array.from(unitMap.values())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载参赛人员统计失败', err)
|
console.error('加载参赛人员统计失败', err)
|
||||||
this.$message.warning('加载参赛人员统计失败')
|
this.$message.warning('加载参赛人员统计失败')
|
||||||
// 使用空数组作为默认值
|
|
||||||
this.participantsData = []
|
this.participantsData = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 加载项目时间统计(该赛事的所有项目及参赛人数)
|
// 加载项目时间统计(该赛事的所有项目及参赛人数)
|
||||||
async loadProjectTimeStats() {
|
async loadProjectTimeStats() {
|
||||||
try {
|
try {
|
||||||
// 使用缓存的参赛者列表
|
// 使用缓存的参赛者列表
|
||||||
const participants = await this.getParticipants()
|
const participants = await this.getParticipants()
|
||||||
|
|
||||||
// 2. 按项目ID分组
|
// 预加载项目信息
|
||||||
const projectMap = new Map()
|
await this.preloadProjectInfo(participants)
|
||||||
|
|
||||||
|
// 按项目ID分组统计人数
|
||||||
|
const projectAthleteCount = new Map()
|
||||||
participants.forEach(athlete => {
|
participants.forEach(athlete => {
|
||||||
// 兼容驼峰和下划线命名
|
|
||||||
const projectId = athlete.projectId || athlete.project_id
|
const projectId = athlete.projectId || athlete.project_id
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
if (!projectMap.has(projectId)) {
|
projectAthleteCount.set(projectId, (projectAthleteCount.get(projectId) || 0) + 1)
|
||||||
projectMap.set(projectId, [])
|
|
||||||
}
|
|
||||||
projectMap.get(projectId).push(athlete)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. 从缓存中获取项目信息并统计(项目信息已经在 loadRegistrationStats 中预加载)
|
// 从缓存中获取项目信息并构建统计数据
|
||||||
const projectStats = []
|
const projectStats = []
|
||||||
for (const [projectId, athleteList] of projectMap) {
|
for (const [projectId, count] of projectAthleteCount) {
|
||||||
const project = this.projectCache.get(projectId)
|
const project = this.projectCache.get(projectId)
|
||||||
if (project) {
|
if (project) {
|
||||||
|
const projectType = project.type || 1 // 1=单人, 2=集体
|
||||||
projectStats.push({
|
projectStats.push({
|
||||||
projectName: project.projectName || project.project_name || '未知项目',
|
projectName: project.projectName || project.project_name || '未知项目',
|
||||||
participantCategory: project.category || '',
|
projectType: projectType === 1 ? '单人' : '集体',
|
||||||
teamCount: 1, // 简化处理,设为1
|
athleteCount: count,
|
||||||
singleTeamPeople: athleteList.length,
|
groupCount: projectType === 2 ? count : '-', // 集体项目显示组数,单人显示-
|
||||||
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0
|
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0,
|
||||||
})
|
projectCode: project.projectCode || project.project_code || ''
|
||||||
} else {
|
|
||||||
// 如果缓存中没有(理论上<E8AEBA><E4B88A><EFBFBD>应该发生),添加基本信息
|
|
||||||
projectStats.push({
|
|
||||||
projectName: `项目ID:${projectId}`,
|
|
||||||
participantCategory: '',
|
|
||||||
teamCount: 1,
|
|
||||||
singleTeamPeople: athleteList.length,
|
|
||||||
estimatedDuration: 0
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,7 +425,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 加载金额统计(该赛事所有单位的报名金额)
|
// 加载金额统计(该赛事所有单位的报名金额)
|
||||||
async loadAmountStats() {
|
async loadAmountStats() {
|
||||||
try {
|
try {
|
||||||
// 使用缓存的参赛者列表
|
// 使用缓存的参赛者列表
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
<span class="project-meta">{{ getTeamCount(group) }}队</span>
|
<span class="project-meta">{{ getTeamCount(group) }}队</span>
|
||||||
<span class="project-meta">{{ group.items?.length || 0 }}组</span>
|
<span class="project-meta">{{ group.items?.length || 0 }}组</span>
|
||||||
<span class="project-meta">{{ group.code }}</span>
|
<span class="project-meta">{{ group.code }}</span>
|
||||||
|
<span class="project-table-no">表号: {{ generateTableNo(group) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-actions" @click.stop>
|
<div class="project-actions" @click.stop>
|
||||||
<el-popover
|
<el-popover
|
||||||
@@ -276,7 +277,15 @@
|
|||||||
|
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
<el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button>
|
<el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button>
|
||||||
<el-button size="small" @click="handleExport" v-if="isScheduleCompleted">导出</el-button>
|
<el-dropdown v-if="isScheduleCompleted" @command="handleExportCommand" trigger="click">
|
||||||
|
<el-button size="small">导出 <i class="el-icon-arrow-down el-icon--right"></i></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="template1">模板1 - 详细赛程表</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="template2">模板2 - 比赛时间表</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
<el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button>
|
<el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -380,7 +389,8 @@
|
|||||||
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
|
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
|
||||||
import { getVenuesByCompetition } from '@/api/martial/venue'
|
import { getVenuesByCompetition } from '@/api/martial/venue'
|
||||||
import { getCompetitionDetail } from '@/api/martial/competition'
|
import { getCompetitionDetail } from '@/api/martial/competition'
|
||||||
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule } from '@/api/martial/activitySchedule'
|
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule, exportScheduleTemplate2 } from '@/api/martial/activitySchedule'
|
||||||
|
import { updateCheckInStatus, getScheduleConfig } from '@/api/martial/schedulePlan'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MartialScheduleList',
|
name: 'MartialScheduleList',
|
||||||
@@ -419,7 +429,8 @@ export default {
|
|||||||
exceptionDialogVisible: false,
|
exceptionDialogVisible: false,
|
||||||
exceptionList: [], // 异常参赛人员列表
|
exceptionList: [], // 异常参赛人员列表
|
||||||
expandedTeams: {}, // 展开的队伍 { 'groupId-teamId': true }
|
expandedTeams: {}, // 展开的队伍 { 'groupId-teamId': true }
|
||||||
expandedProjects: {} // 展开的项目 { 'groupId': true },默认收起
|
expandedProjects: {}, // 展开的项目 { 'groupId': true },默认收起
|
||||||
|
scheduleConfig: { morningStartTime: '08:00', afternoonStartTime: '14:00' } // 赛程配置
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -485,6 +496,7 @@ export default {
|
|||||||
this.orderId = this.$route.query.orderId
|
this.orderId = this.$route.query.orderId
|
||||||
|
|
||||||
if (this.competitionId) {
|
if (this.competitionId) {
|
||||||
|
this.loadScheduleConfig()
|
||||||
this.loadCompetitionInfo()
|
this.loadCompetitionInfo()
|
||||||
this.loadVenues()
|
this.loadVenues()
|
||||||
this.loadScheduleData()
|
this.loadScheduleData()
|
||||||
@@ -493,6 +505,44 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 生成表号: 场地(1位) + 时段(1位,上午=1/下午=2) + 序号(2位)
|
||||||
|
generateTableNo(group) {
|
||||||
|
// 1. 获取场地编号
|
||||||
|
let venueNo = 1
|
||||||
|
if (group.venueId) {
|
||||||
|
const venue = this.venues.find(v => v.id === group.venueId || String(v.id) === String(group.venueId))
|
||||||
|
if (venue && venue.venueName) {
|
||||||
|
// 从场地名称提取数字
|
||||||
|
const match = venue.venueName.match(/\d+/)
|
||||||
|
if (match) {
|
||||||
|
venueNo = parseInt(match[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取时段:上午=1, 下午=2
|
||||||
|
let period = 1
|
||||||
|
if (group.timeSlot) {
|
||||||
|
const hour = parseInt(group.timeSlot.split(':')[0])
|
||||||
|
period = hour < 12 ? 1 : 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 获取序号:在同场地同时段中的顺序
|
||||||
|
const sameSlotGroups = this.competitionGroups.filter(g => {
|
||||||
|
const gVenueMatch = String(g.venueId) === String(group.venueId)
|
||||||
|
if (!gVenueMatch) return false
|
||||||
|
const gHour = parseInt((g.timeSlot || '08:30').split(':')[0])
|
||||||
|
const gPeriod = gHour < 12 ? 1 : 2
|
||||||
|
return gPeriod === period
|
||||||
|
})
|
||||||
|
// 按id排序保持稳定顺序
|
||||||
|
sameSlotGroups.sort((a, b) => (a.id || 0) - (b.id || 0))
|
||||||
|
const orderIndex = sameSlotGroups.findIndex(g => g.id === group.id) + 1
|
||||||
|
|
||||||
|
// 4. 格式化: 场地(1位) + 时段(1位) + 序号(2位)
|
||||||
|
return `${venueNo}${period}${String(orderIndex).padStart(2, '0')}`
|
||||||
|
},
|
||||||
|
|
||||||
// 检查项目是否展开
|
// 检查项目是否展开
|
||||||
isProjectExpanded(groupId) {
|
isProjectExpanded(groupId) {
|
||||||
return this.expandedProjects[groupId] === true
|
return this.expandedProjects[groupId] === true
|
||||||
@@ -529,6 +579,11 @@ export default {
|
|||||||
|
|
||||||
// 标记选手为异常
|
// 标记选手为异常
|
||||||
markPlayerAsException(group, team, playerIndex) {
|
markPlayerAsException(group, team, playerIndex) {
|
||||||
|
if (this.isScheduleCompleted) {
|
||||||
|
this.$message.warning('编排已完成,无法标记异常')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const player = team.players[playerIndex]
|
const player = team.players[playerIndex]
|
||||||
if (player) {
|
if (player) {
|
||||||
player.status = '异常'
|
player.status = '异常'
|
||||||
@@ -542,12 +597,23 @@ export default {
|
|||||||
playerName: player.playerName,
|
playerName: player.playerName,
|
||||||
status: '异常'
|
status: '异常'
|
||||||
})
|
})
|
||||||
this.$message.success('已标记为异常')
|
|
||||||
|
// 调用后端API保存状态
|
||||||
|
updateCheckInStatus(player.id, '异常').then(() => {
|
||||||
|
this.$message.success(`已将 ${player.playerName} 标记为异常`)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('保存异常状态失败:', err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 取消选手异常状态
|
// 取消选手异常状态
|
||||||
removePlayerException(group, team, playerIndex) {
|
removePlayerException(group, team, playerIndex) {
|
||||||
|
if (this.isScheduleCompleted) {
|
||||||
|
this.$message.warning('编排已完成,无法取消异常')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const player = team.players[playerIndex]
|
const player = team.players[playerIndex]
|
||||||
if (player) {
|
if (player) {
|
||||||
player.status = '未签到'
|
player.status = '未签到'
|
||||||
@@ -558,7 +624,13 @@ export default {
|
|||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
this.exceptionList.splice(idx, 1)
|
this.exceptionList.splice(idx, 1)
|
||||||
}
|
}
|
||||||
this.$message.success('已取消异常标记')
|
|
||||||
|
// 调用后端API恢复状态
|
||||||
|
updateCheckInStatus(player.id, '未签到').then(() => {
|
||||||
|
this.$message.success(`已将 ${player.playerName} 取消异常标记`)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('恢复状态失败:', err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -715,32 +787,6 @@ export default {
|
|||||||
this.$message.success('下移成功')
|
this.$message.success('下移成功')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 标记单个选手为异常
|
|
||||||
markPlayerAsException(group, player) {
|
|
||||||
if (this.isScheduleCompleted) {
|
|
||||||
this.$message.warning('编排已完成,无法标记异常')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在 group.items 中找到该选手并修改状态
|
|
||||||
const item = group.items.find(i => i.id === player.id)
|
|
||||||
if (item) {
|
|
||||||
item.status = '异常'
|
|
||||||
|
|
||||||
// 添加到异常列表
|
|
||||||
this.exceptionList.push({
|
|
||||||
groupId: group.id,
|
|
||||||
groupTitle: group.title,
|
|
||||||
participantId: player.id,
|
|
||||||
schoolUnit: player.schoolUnit,
|
|
||||||
playerName: player.playerName,
|
|
||||||
status: '异常'
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$message.success(`已将 ${player.playerName} 标记为异常`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
goBack() {
|
goBack() {
|
||||||
this.$router.go(-1)
|
this.$router.go(-1)
|
||||||
},
|
},
|
||||||
@@ -769,15 +815,43 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 根据开始和结束时间生成时间段列表
|
// 根据开始和结束时间生成时间段列表
|
||||||
|
// 加载赛程配置
|
||||||
|
async loadScheduleConfig() {
|
||||||
|
try {
|
||||||
|
const res = await getScheduleConfig()
|
||||||
|
if (res.data?.data) {
|
||||||
|
this.scheduleConfig = res.data.data
|
||||||
|
console.log('赛程配置:', this.scheduleConfig)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载赛程配置失败', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 格式化时间为显示格式 (08:00 -> 8:00)
|
||||||
|
formatTimeForDisplay(time) {
|
||||||
|
if (!time) return '8:00'
|
||||||
|
const parts = time.split(':')
|
||||||
|
const hour = parseInt(parts[0], 10)
|
||||||
|
const minute = parts[1] || '00'
|
||||||
|
return `${hour}:${minute}`
|
||||||
|
},
|
||||||
|
|
||||||
generateTimeSlots() {
|
generateTimeSlots() {
|
||||||
const startTime = this.competitionInfo.competitionStartTime
|
const startTime = this.competitionInfo.competitionStartTime
|
||||||
const endTime = this.competitionInfo.competitionEndTime
|
const endTime = this.competitionInfo.competitionEndTime
|
||||||
|
|
||||||
|
// 从配置获取时间,格式化为显示格式
|
||||||
|
const morningTime = this.formatTimeForDisplay(this.scheduleConfig.morningStartTime || '08:00')
|
||||||
|
const afternoonTime = this.formatTimeForDisplay(this.scheduleConfig.afternoonStartTime || '14:00')
|
||||||
|
|
||||||
if (!startTime || !endTime) {
|
if (!startTime || !endTime) {
|
||||||
this.$message.warning('赛事时间信息不完整,使用默认时间段')
|
this.$message.warning('赛事时间信息不完整,使用默认时间段')
|
||||||
|
const today = new Date()
|
||||||
|
const dateStr = `${today.getFullYear()}年${today.getMonth() + 1}月${today.getDate()}日`
|
||||||
this.timeSlots = [
|
this.timeSlots = [
|
||||||
'2025年11月6日 上午8:30',
|
`${dateStr} 上午${morningTime}`,
|
||||||
'2025年11月6日 下午13:30'
|
`${dateStr} 下午${afternoonTime}`
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -788,7 +862,6 @@ export default {
|
|||||||
|
|
||||||
// 遍历每一天
|
// 遍历每一天
|
||||||
let currentDate = new Date(start)
|
let currentDate = new Date(start)
|
||||||
let dayIndex = 1
|
|
||||||
|
|
||||||
while (currentDate <= end) {
|
while (currentDate <= end) {
|
||||||
const year = currentDate.getFullYear()
|
const year = currentDate.getFullYear()
|
||||||
@@ -796,15 +869,14 @@ export default {
|
|||||||
const day = currentDate.getDate()
|
const day = currentDate.getDate()
|
||||||
const dateStr = `${year}年${month}月${day}日`
|
const dateStr = `${year}年${month}月${day}日`
|
||||||
|
|
||||||
// 添加上午时段 8:30
|
// 添加上午时段
|
||||||
slots.push(`${dateStr} 上午8:30`)
|
slots.push(`${dateStr} 上午${morningTime}`)
|
||||||
|
|
||||||
// 添加下午时段 13:30
|
// 添加下午时段
|
||||||
slots.push(`${dateStr} 下午13:30`)
|
slots.push(`${dateStr} 下午${afternoonTime}`)
|
||||||
|
|
||||||
// 下一天
|
// 下一天
|
||||||
currentDate.setDate(currentDate.getDate() + 1)
|
currentDate.setDate(currentDate.getDate() + 1)
|
||||||
dayIndex++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timeSlots = slots
|
this.timeSlots = slots
|
||||||
@@ -993,7 +1065,12 @@ export default {
|
|||||||
status: '异常'
|
status: '异常'
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$message.success(`已将 ${item.schoolUnit} 标记为异常`)
|
// 调用后端API保存状态
|
||||||
|
updateCheckInStatus(item.id, '异常').then(() => {
|
||||||
|
this.$message.success(`已将 ${item.schoolUnit} 标记为异常`)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('保存异常状态失败:', err)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 显示异常组对话框
|
// 显示异常组对话框
|
||||||
@@ -1020,7 +1097,12 @@ export default {
|
|||||||
|
|
||||||
// 从异常列表中移除
|
// 从异常列表中移除
|
||||||
this.exceptionList.splice(index, 1)
|
this.exceptionList.splice(index, 1)
|
||||||
this.$message.success(`已将 ${exceptionItem.schoolUnit} 从异常组移除`)
|
// 调用后端API恢复状态
|
||||||
|
updateCheckInStatus(exceptionItem.participantId, '未签到').then(() => {
|
||||||
|
this.$message.success(`已将 ${exceptionItem.schoolUnit} 从异常组移除`)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('恢复状态失败:', err)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 触发自动编排
|
// 触发自动编排
|
||||||
@@ -1113,6 +1195,34 @@ export default {
|
|||||||
group.items.splice(itemIndex + 1, 0, temp)
|
group.items.splice(itemIndex + 1, 0, temp)
|
||||||
this.$message.success('下移成功')
|
this.$message.success('下移成功')
|
||||||
},
|
},
|
||||||
|
handleExportCommand(command) {
|
||||||
|
if (command === 'template1') {
|
||||||
|
this.handleExport()
|
||||||
|
} else if (command === 'template2') {
|
||||||
|
this.handleExportTemplate2()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleExportTemplate2() {
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
const venueId = this.selectedVenueId
|
||||||
|
const venue = this.venues.find(v => v.id === venueId)
|
||||||
|
const venueName = venue ? venue.venueName : null
|
||||||
|
const res = await exportScheduleTemplate2(this.competitionId, venueId, venueName, null)
|
||||||
|
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = window.URL.createObjectURL(blob)
|
||||||
|
link.download = `比赛时间_${venueName || '全部场地'}_${this.competitionInfo.competitionName || this.competitionId}.xlsx`
|
||||||
|
link.click()
|
||||||
|
window.URL.revokeObjectURL(link.href)
|
||||||
|
this.$message.success('导出成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出失败:', error)
|
||||||
|
this.$message.error('导出失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
async handleExport() {
|
async handleExport() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
@@ -1386,6 +1496,16 @@ export default {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-table-no {
|
||||||
|
color: #409EFF;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-actions {
|
.project-actions {
|
||||||
|
|||||||
@@ -94,7 +94,8 @@
|
|||||||
|
|
||||||
<el-table-column label="总裁判分数" width="120" align="center" fixed="right">
|
<el-table-column label="总裁判分数" width="120" align="center" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span class="total-score">{{ formatScore(scope.row.totalScore) }}</span>
|
<span v-if="scope.row.scoreStatus === 2" class="total-score">{{ formatScore(scope.row.chiefJudgeScore) }}</span>
|
||||||
|
<span v-else class="pending-score">待确认</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
@@ -153,12 +154,17 @@
|
|||||||
|
|
||||||
<div class="total-score-display">
|
<div class="total-score-display">
|
||||||
<span class="label">总分:</span>
|
<span class="label">总分:</span>
|
||||||
<span class="value">{{ formatScore(currentDetail.totalScore) }}</span>
|
<template v-if="currentDetail.scoreStatus === 2">
|
||||||
<div class="calculation-note">
|
<span class="value">{{ formatScore(currentDetail.chiefJudgeScore) }}</span>
|
||||||
<span v-if="currentDetail.judgeScores.length > 2">
|
<div class="calculation-note">(主裁判已确认)</div>
|
||||||
(去掉最高分和最低分后的平均分)
|
</template>
|
||||||
</span>
|
<template v-else>
|
||||||
</div>
|
<span class="value pending">待确认最终得分</span>
|
||||||
|
<div class="calculation-note">
|
||||||
|
裁判员评分: {{ formatScore(currentDetail.totalScore) }}
|
||||||
|
<span v-if="currentDetail.judgeScores.length > 2">(去掉最高最低分后平均)</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -398,7 +404,9 @@ export default {
|
|||||||
playerNo: score.playerNo || '',
|
playerNo: score.playerNo || '',
|
||||||
judgeScores: [],
|
judgeScores: [],
|
||||||
scoreDetails: [],
|
scoreDetails: [],
|
||||||
totalScore: 0
|
totalScore: 0,
|
||||||
|
chiefJudgeScore: score.chiefJudgeScore,
|
||||||
|
scoreStatus: score.scoreStatus || 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +573,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total-score {
|
.total-score {
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-score {
|
||||||
|
color: #e6a23c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.pending {
|
||||||
|
color: #e6a23c;
|
||||||
|
font-weight: 500;
|
||||||
color: #1b7c5e;
|
color: #1b7c5e;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<i class="el-icon-s-order"></i>
|
<i class="el-icon-s-order"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-info">
|
<div class="card-info">
|
||||||
<h3>订单管理</h3>
|
<h3>赛事管理</h3>
|
||||||
<p>查看和管理订单</p>
|
<p>查看和管理订单</p>
|
||||||
</div>
|
</div>
|
||||||
<i class="card-arrow el-icon-arrow-right"></i>
|
<i class="card-arrow el-icon-arrow-right"></i>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export default ({ mode, command }) => {
|
|||||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 2888,
|
port: 8083,
|
||||||
|
host: '0.0.0.0',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8123',
|
target: 'http://localhost:8123',
|
||||||
|
|||||||
Reference in New Issue
Block a user