11 Commits
devops ... main

Author SHA1 Message Date
DevOps
11eb5f2db8 feat: 裁判端添加退出登录按钮
- 在评分列表页导航栏添加退出按钮
- 点击后弹出确认框,确认后清除登录信息并返回登录页

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 12:38:28 +08:00
DevOps
56a33707d5 feat: 场地无项目时显示提示并隐藏选手列表
- 添加"当前场地暂无比赛项目"提示
- 当没有项目时隐藏选手列表
- 添加no-project-tip样式

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 11:31:40 +08:00
DevOps
1b305fc2bd feat(score): 分数支持直接编辑输入 2025-12-31 17:12:20 +08:00
DevOps
a780ee6b2c fix(score): 修复评分详情页projectId传递问题 2025-12-31 16:48:17 +08:00
DevOps
90ee38a57b feat(score): 支持点击分数直接输入编辑 2025-12-30 19:05:43 +08:00
DevOps
84b84dd951 docs: 更新README,简化内容并更新域名 2025-12-29 14:21:56 +08:00
DevOps
941112dd4c feat: 总裁页面同时显示待确认和已确认成绩
- 新增已确认成绩列表区域
- 调用 /mini/general/confirmed 接口获取已确认数据
- 区分显示待确认(橙色)和已确认(绿色)状态

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 16:09:59 +08:00
DevOps
314b507748 feat: 添加总裁(裁判长)确认页面
- 新增general-judge.vue总裁确认页面
- 支持查看待确认成绩列表
- 支持确认/修改最终分数
- 更新pages.json路由配置
- 更新login.vue支持总裁角色跳转

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 15:49:31 +08:00
DevOps
a3680f7d3e refactor: 裁判角色名称修改 - 裁判长→主裁判, 普通裁判→裁判员
- 修改pages目录下的Vue组件注释
- 修改api目录下的接口注释
- 修改mock目录下的模拟数据注释
- 修改utils/dataAdapter.js中的注释

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:37:50 +08:00
DevOps
711779dc57 fix: 修复裁判端选手列表数据问题
- score-list添加competitionId参数传递
- 项目筛选改为横向滑动布局
- 修复env.config.js生产环境配置

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 15:45:57 +08:00
DevOps
edd64cda47 feat: 项目筛选改为横向滑动布局
- 使用 scroll-view + scroll-x 实现横向滚动
- 项目标签一行显示,超出可滑动
- 优化移动端交互体验

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 10:46:48 +08:00
32 changed files with 1210 additions and 535 deletions

311
README.md
View File

@@ -1,296 +1,77 @@
# 武术评分系统 - UniApp版本 # 武术赛事管理系统 - 裁判端
这是一个基于UniApp开发的武术比赛评分系统支持H5小程序平台 基于 UniApp 开发的武术比赛评分小程序,支持 H5 和微信小程序。
## 项目简介 ## 在线访问
本项目是一个完整的武术比赛评分管理系统,包含评委打分、裁判长修改评分、多场地管理等功能。 | 服务 | 地址 |
|------|------|
| 裁判端 H5 | https://martial-mini.aitisai.com |
| 后端 API | https://martial-api.aitisai.com |
## 功能特点 ## 功能特点
- ✅ 支持H5和微信小程序双平台 - 裁判员登录与评分
- ✅ 评委登录与评分功能 - 主裁判评分审核
- ✅ 裁判长评分修改功能 - 总裁最终确认
- 多场地、多项目切换 - 多场地切换
- 扣分项多选功能 - 扣分项管理
- 精确到0.001分的评分控制 - 精确到 0.001 分的评分控制
- ✅ 实时评分统计
## 页面结构 ## 三级裁判系统
### 1. 登录页面 (`pages/login/login.vue`) ```
- 输入比赛编码 裁判员 → 主裁判 → 总裁
- 输入评委邀请码 ↓ ↓ ↓
- 验证登录 打分 审核修改 最终确认
```
### 2. 评分列表页 (`pages/score-list/score-list.vue`)
- 显示比赛信息
- 查看选手列表
- 评委评分入口
- 显示我的评分和总分
### 3. 修改评分页 (`pages/modify-score/modify-score.vue`)
- 裁判长专用功能
- 查看所有评委评分
- 修改总分±0.001分)
- 添加修改备注
### 4. 多场地评分列表页 (`pages/score-list-multi/score-list-multi.vue`)
- 场地切换功能
- 项目切换功能
- 裁判长可查看所有场地
- 普通评委仅看自己的场地
### 5. 评分详情页 (`pages/score-detail/score-detail.vue`)
- 评委打分界面
- 分数精确调整±0.001分)
- 扣分项多选
- 添加评分备注
## 技术栈 ## 技术栈
- **框架**: UniApp - **框架**: UniApp
- **前端**: Vue.js 2.x - **平台**: H5、微信小程序
- **样式**: CSS3 (支持rpx单位) - **样式**: SCSS
- **平台支持**: H5、微信小程序
## 目录结构 ## 快速开始
```
martial-admin-mini/
├── common/ # 公共资源
│ └── common.css # 全局样式
├── pages/ # 页面目录
│ ├── login/ # 登录页面
│ ├── score-list/ # 评分列表页
│ ├── modify-score/ # 修改评分页
│ ├── score-list-multi/ # 多场地列表页
│ └── score-detail/ # 评分详情页
├── image/ # 设计图片
├── App.vue # 应用配置
├── manifest.json # 应用配置清单
├── pages.json # 页面配置
└── package.json # 项目依赖
```
## 快速开始(推荐方式)
### 使用HBuilderX运行最简单
1. **下载并安装HBuilderX**
- 官网下载https://www.dcloud.io/hbuilderx.html
- 建议下载标准版或App开发版
2. **打开项目**
- 打开HBuilderX
- 点击菜单:文件 -> 打开目录
- 选择本项目的根目录:`martial-admin-mini`
3. **运行到H5**
- 在项目管理器中,右键点击项目根目录
- 选择:运行 -> 运行到浏览器 -> Chrome或其他浏览器
- 浏览器会自动打开并显示项目
4. **运行到微信小程序**
- 首先安装并打开微信开发者工具
- 在HBuilderX中右键点击项目根目录
- 选择:运行 -> 运行到小程序模拟器 -> 微信开发者工具
- 首次运行会提示配置小程序工具路径,按提示配置即可
### 常见问题解决
#### 问题1HBuilderX提示"未安装依赖"或"缺少插件"
**解决方案:**
- 点击HBuilderX顶部菜单工具 -> 插件安装
- 安装以下插件:
- uni-app编译器必需
- App真机运行如需真机调试
- 微信小程序支持(如需开发小程序)
#### 问题2运行时报错"Cannot find module"
**解决方案:**
```bash ```bash
# 在项目根目录执行 # 安装依赖
npm install npm install
```
#### 问题3小程序无法运行 # H5 开发
**解决方案:**
1. 确保已安装微信开发者工具
2. 在HBuilderX中配置微信开发者工具路径
- 工具 -> 设置 -> 运行配置 -> 小程序运行配置
- 配置微信开发者工具的安装路径
3. 在manifest.json中配置小程序appid或使用测试号
#### 问题4H5运行时样式异常
**解决方案:**
- 清除浏览器缓存后重新运行
- 或使用Ctrl+F5强制刷新
## 开发指南
### 环境要求
- HBuilderX 3.0+ (推荐)
- 或 Node.js 12+ + Vue CLI
- 微信开发者工具 (小程序开发需要)
### 方式一使用HBuilderX推荐
1. 使用HBuilderX打开项目根目录
2. 点击运行 -> 运行到浏览器 (H5开发)
3. 或点击运行 -> 运行到小程序模拟器 -> 微信开发者工具
### 方式二:使用命令行
#### 安装依赖
```bash
npm install
```
#### 运行项目
**H5开发**
```bash
npm run dev:h5 npm run dev:h5
```
**微信小程序开发** # 微信小程序开发
```bash
npm run dev:mp-weixin npm run dev:mp-weixin
```
**构建H5** # 构建 H5
```bash
npm run build:h5 npm run build:h5
``` ```
**构建微信小程序** ## 项目结构
```bash
npm run build:mp-weixin
```
## 设计还原说明
本项目严格按照提供的5张设计图进行一比一还原
1. **颜色方案**
- 主色调:绿色 (#1B7C5E - #2A9D7E 渐变)
- 强调色:红色 (#FF4D6A) - 用于提示信息
- 背景色:浅灰 (#F5F5F5)
2. **字体大小**
- 导航标题36rpx
- 页面标题40rpx
- 正文内容26-32rpx
- 提示文字22-24rpx
3. **间距与圆角**
- 卡片圆角16rpx
- 按钮圆角8-16rpx
- 标准间距30rpx
- 内边距20-40rpx
4. **交互效果**
- 按钮点击反馈
- 分数增减控制
- 多选框交互
- 页面跳转动画
## 功能说明
### 评分规则
- 分数范围5.0 - 10.0 分
- 精度0.001 分
- 评委评分保留3位小数
- 裁判长可修改总分 ±0.005 分
### 权限区分
- **普通评委**:仅能查看和评分自己负责的场地和项目
- **裁判长**:可查看所有场地和项目,可修改评分
### 扣分项
- 支持多选
- 每个项目可配置不同的扣分项
- 扣分项选择后自动计入总分
## 🚀 API对接状态
### ✅ 前端已完全准备就绪100%
本项目已完成API对接准备工作可以立即开始后端对接
-**dataAdapter架构** - 支持Mock/API双模式无缝切换
-**API接口定义** - 9个接口全部定义完成
-**网络请求封装** - 统一的错误处理和Token管理
-**Mock数据完整** - 可独立演示所有功能
-**文档体系完善** - 21个文档约25,000+行
### 📋 快速开始API对接
#### 1. 配置后端地址30秒
编辑 `config/env.config.js`:
```javascript
apiBaseURL: 'http://localhost:8080' // 修改为实际后端地址
```
#### 2. 切换数据模式
```javascript
// Mock模式后端未就绪时
dataMode: 'mock'
// API模式后端就绪后
dataMode: 'api'
```
#### 3. 查看文档
- **快速上手**: [API对接快速启动指南.md](doc/API对接快速启动指南.md) - 5分钟快速上手
- **后端开发**: [后端接口开发清单.md](doc/后端接口开发清单.md) - 详细的开发规范
- **前端联调**: [前端API对接指南.md](doc/前端API对接指南.md) - 前端联调指南
- **快速参考**: [快速参考.md](快速参考.md) - 一页纸快速参考
### ⚠️ 后端待开发接口5个
| 接口 | 路径 | 优先级 | 工作量 |
|------|------|--------|--------|
| 登录验证 | `POST /api/mini/login` | 🔴 高 | 2天 |
| 普通评委选手列表 | `GET /api/mini/athletes` | 🔴 高 | 1天 |
| 裁判长选手列表 | `GET /api/mini/athletes/admin` | 🟡 中 | 1天 |
| 评分详情 | `GET /api/mini/score/detail/{id}` | 🟡 中 | 1天 |
| 修改评分 | `PUT /api/mini/score/modify` | 🟡 中 | 1天 |
**预计总工作量**: 6人天约1周
详细规范请查看:[后端接口开发清单.md](doc/后端接口开发清单.md)
### 📊 项目状态
``` ```
前端开发: ████████████████████ 100% ✅ martial-admin-mini/
后端开发: ████████░░░░░░░░░░░░ 44% ⚠️ ├── pages/
文档完成: ████████████████████ 100% ✅ │ ├── login/ # 登录页
│ ├── score-list/ # 评分列表
│ ├── score-list-multi/ # 多场地评分
│ ├── score-detail/ # 评分详情
│ └── modify-score/ # 修改评分(裁判长)
├── components/ # 公共组件
├── static/ # 静态资源
├── pages.json # 页面配置
└── manifest.json # 应用配置
``` ```
查看实时状态:[项目状态看板.md](项目状态看板.md) ## 相关仓库
## 注意事项 | 仓库 | 说明 |
|------|------|
| [martial-master](https://git.waypeak.work/martial/martial-master) | 后端 API |
| [martial-web](https://git.waypeak.work/martial/martial-web) | 管理后台 |
| [martial-mini](https://git.waypeak.work/martial/martial-mini) | 用户端小程序 |
1. ✅ 本项目已完成API对接准备支持Mock/API双模式 ---
2. ✅ Mock模式下所有功能可独立演示
3. ✅ API模式下需要后端实现5个专用接口
4. ✅ 页面跳转已配置,可直接运行演示
5. ✅ 适配了主流手机屏幕尺寸
## 许可证 **最后更新**: 2024-12-29
MIT License
## 联系方式
如有问题或建议,请联系项目负责人。

View File

@@ -9,13 +9,13 @@ import request from '@/utils/request.js'
* 获取选手列表(根据裁判类型返回不同数据) * 获取选手列表(根据裁判类型返回不同数据)
* @param {Object} params * @param {Object} params
* @param {String} params.judgeId - 评委ID * @param {String} params.judgeId - 评委ID
* @param {Number} params.refereeType - 裁判类型1-裁判, 2-普通裁判) * @param {Number} params.refereeType - 裁判类型1-裁判, 2-裁判
* @param {String} params.venueId - 场地ID可选 * @param {String} params.venueId - 场地ID可选
* @param {String} params.projectId - 项目ID可选 * @param {String} params.projectId - 项目ID可选
* @returns {Promise} * @returns {Promise}
* *
* 普通裁判:返回待评分的选手列表 * 裁判:返回待评分的选手列表
* 裁判:返回已有评分的选手列表 * 裁判:返回已有评分的选手列表
* *
* 后端路径: GET /api/mini/score/athletes * 后端路径: GET /api/mini/score/athletes
*/ */
@@ -32,7 +32,7 @@ export function getMyAthletes(params) {
} }
/** /**
* 获取选手列表(裁判 * 获取选手列表(裁判)
* @param {Object} params * @param {Object} params
* @param {String} params.competitionId - 比赛ID * @param {String} params.competitionId - 比赛ID
* @param {String} params.venueId - 场地ID * @param {String} params.venueId - 场地ID
@@ -52,7 +52,7 @@ export function getAthletesForAdmin(params) {
method: 'GET', method: 'GET',
params: { params: {
judgeId: judgeId, judgeId: judgeId,
refereeType: 1, // 裁判 refereeType: 1, // 裁判
venueId: params.venueId, venueId: params.venueId,
projectId: params.projectId, projectId: params.projectId,
size: 200 // 确保获取所有选手 size: 200 // 确保获取所有选手

View File

@@ -71,7 +71,7 @@ export default {
* "msg": "登录成功", * "msg": "登录成功",
* "data": { * "data": {
* "token": "xxx", * "token": "xxx",
* "refereeType": 2, // 1-裁判, 2-普通裁判 * "refereeType": 2, // 1-裁判, 2-裁判
* "matchId": "123", * "matchId": "123",
* "matchName": "2025年全国武术散打锦标赛...", * "matchName": "2025年全国武术散打锦标赛...",
* "matchTime": "2025年6月25日 9:00", * "matchTime": "2025年6月25日 9:00",

View File

@@ -46,7 +46,7 @@ export default {
getMyAthletes: athleteApi.getMyAthletes, getMyAthletes: athleteApi.getMyAthletes,
/** /**
* 获取选手列表(裁判 * 获取选手列表(裁判)
* @param {Object} params - { competitionId, venueId, projectId } * @param {Object} params - { competitionId, venueId, projectId }
* @returns {Promise} * @returns {Promise}
*/ */
@@ -82,14 +82,14 @@ export default {
submitScore: scoreApi.submitScore, submitScore: scoreApi.submitScore,
/** /**
* 获取评分详情(裁判查看) * 获取评分详情(裁判查看)
* @param {Object} params - { athleteId } * @param {Object} params - { athleteId }
* @returns {Promise} * @returns {Promise}
*/ */
getScoreDetail: scoreApi.getScoreDetail, getScoreDetail: scoreApi.getScoreDetail,
/** /**
* 修改评分(裁判 * 修改评分(裁判)
* @param {Object} data - { athleteId, modifierId, modifiedScore, note } * @param {Object} data - { athleteId, modifierId, modifiedScore, note }
* @returns {Promise} * @returns {Promise}
*/ */
@@ -127,7 +127,7 @@ export default {
* 1. 需要实现的新接口(小程序专用): * 1. 需要实现的新接口(小程序专用):
* - POST /api/mini/login # 登录验证 * - POST /api/mini/login # 登录验证
* - GET /api/mini/athletes # 普通评委选手列表 * - GET /api/mini/athletes # 普通评委选手列表
* - GET /api/mini/athletes/admin # 裁判选手列表 * - GET /api/mini/athletes/admin # 裁判选手列表
* - GET /api/mini/score/detail/{athleteId} # 评分详情 * - GET /api/mini/score/detail/{athleteId} # 评分详情
* - PUT /api/mini/score/modify # 修改评分 * - PUT /api/mini/score/modify # 修改评分
* *

View File

@@ -44,7 +44,7 @@ export function submitScore(data) {
} }
/** /**
* 获取评分详情(裁判查看) * 获取评分详情(裁判查看)
* @param {Object} params * @param {Object} params
* @param {String} params.athleteId - 选手ID * @param {String} params.athleteId - 选手ID
* @returns {Promise} * @returns {Promise}
@@ -61,7 +61,7 @@ export function getScoreDetail(params) {
} }
/** /**
* 修改评分(裁判 * 修改评分(裁判)
* @param {Object} data * @param {Object} data
* @param {String} data.athleteId - 选手ID * @param {String} data.athleteId - 选手ID
* @param {String} data.modifierId - 修改人ID * @param {String} data.modifierId - 修改人ID
@@ -86,7 +86,7 @@ export function modifyScore(data) {
* 获取选手列表 * 获取选手列表
* @param {Object} params * @param {Object} params
* @param {String} params.judgeId - 裁判ID * @param {String} params.judgeId - 裁判ID
* @param {Number} params.refereeType - 裁判类型1-裁判, 2-普通裁判) * @param {Number} params.refereeType - 裁判类型1-裁判, 2-裁判
* @param {String} params.projectId - 项目ID可选 * @param {String} params.projectId - 项目ID可选
* @param {String} params.venueId - 场地ID可选 * @param {String} params.venueId - 场地ID可选
* @returns {Promise} * @returns {Promise}
@@ -180,7 +180,7 @@ export default {
* } * }
* *
* 实现逻辑: * 实现逻辑:
* 1. 验证权限(只有裁判可以修改) * 1. 验证权限(只有裁判可以修改)
* 2. 保存 originalScore如果是第一次修改 * 2. 保存 originalScore如果是第一次修改
* 3. 更新 totalScore * 3. 更新 totalScore
* 4. 记录 modifyReason 和 modifyTime * 4. 记录 modifyReason 和 modifyTime

View File

@@ -7,7 +7,7 @@
* 获取选手列表(根据裁判类型返回不同数据) * 获取选手列表(根据裁判类型返回不同数据)
* @param {Object} params * @param {Object} params
* @param {String} params.judgeId - 评委ID * @param {String} params.judgeId - 评委ID
* @param {Number} params.refereeType - 裁判类型1-裁判, 2-普通裁判) * @param {Number} params.refereeType - 裁判类型1-裁判, 2-裁判
* @param {String} params.venueId - 场地ID可选 * @param {String} params.venueId - 场地ID可选
* @param {String} params.projectId - 项目ID可选 * @param {String} params.projectId - 项目ID可选
* @returns {Array} 选手列表 * @returns {Array} 选手列表
@@ -15,7 +15,7 @@
export function getMyAthletes(params) { export function getMyAthletes(params) {
const { refereeType } = params const { refereeType } = params
// 裁判:返回已有评分的选手 // 裁判:返回已有评分的选手
if (refereeType === 1) { if (refereeType === 1) {
return [ return [
{ {
@@ -54,7 +54,7 @@ export function getMyAthletes(params) {
] ]
} }
// 普通裁判:返回待评分的选手 // 裁判:返回待评分的选手
return [ return [
{ {
athleteId: 3, athleteId: 3,
@@ -80,7 +80,7 @@ export function getMyAthletes(params) {
} }
/** /**
* 获取选手列表(裁判 * 获取选手列表(裁判)
* @param {Object} params * @param {Object} params
* @param {String} params.competitionId - 比赛ID * @param {String} params.competitionId - 比赛ID
* @param {String} params.venueId - 场地ID * @param {String} params.venueId - 场地ID

View File

@@ -34,7 +34,7 @@ export default {
getMyAthletes: athleteMock.getMyAthletes, getMyAthletes: athleteMock.getMyAthletes,
/** /**
* 获取选手列表(裁判 * 获取选手列表(裁判)
* @param {Object} params - { competitionId, venueId, projectId } * @param {Object} params - { competitionId, venueId, projectId }
* @returns {Array} 选手列表(带评分统计) * @returns {Array} 选手列表(带评分统计)
*/ */
@@ -70,14 +70,14 @@ export default {
submitScore: scoreMock.submitScore, submitScore: scoreMock.submitScore,
/** /**
* 获取评分详情(裁判查看) * 获取评分详情(裁判查看)
* @param {Object} params - { athleteId } * @param {Object} params - { athleteId }
* @returns {Object} 评分详情(选手信息+评委评分) * @returns {Object} 评分详情(选手信息+评委评分)
*/ */
getScoreDetail: scoreMock.getScoreDetail, getScoreDetail: scoreMock.getScoreDetail,
/** /**
* 修改评分(裁判 * 修改评分(裁判)
* @param {Object} params - { athleteId, modifierId, modifiedScore, note } * @param {Object} params - { athleteId, modifierId, modifiedScore, note }
* @returns {Object} 修改结果 * @returns {Object} 修改结果
*/ */

View File

@@ -23,13 +23,13 @@ export function login(params) {
// 返回Mock登录数据 // 返回Mock登录数据
return { return {
token: 'mock_token_' + Date.now(), token: 'mock_token_' + Date.now(),
refereeType: role === 'pub' ? 2 : 1, // 1-裁判, 2-普通裁判 refereeType: role === 'pub' ? 2 : 1, // 1-裁判, 2-裁判
matchId: matchCode || '200', // 使用传入的比赛编码默认200 matchId: matchCode || '200', // 使用传入的比赛编码默认200
matchName: '2025年全国武术散打锦标赛暨第十七届世界武术锦标赛选拔赛', matchName: '2025年全国武术散打锦标赛暨第十七届世界武术锦标赛选拔赛',
matchTime: '2025年6月25日 9:00', matchTime: '2025年6月25日 9:00',
judgeId: '456', judgeId: '456',
judgeName: '欧阳丽娜', judgeName: '欧阳丽娜',
// 普通评委有固定场地,裁判可以查看所有场地 // 普通评委有固定场地,裁判可以查看所有场地
venueId: role === 'pub' ? '1' : null, venueId: role === 'pub' ? '1' : null,
venueName: role === 'pub' ? '第一场地' : null, venueName: role === 'pub' ? '第一场地' : null,
// 分配的项目列表 // 分配的项目列表

View File

@@ -62,7 +62,7 @@ export function submitScore(params) {
} }
/** /**
* 获取评分详情(裁判查看) * 获取评分详情(裁判查看)
* @param {Object} params * @param {Object} params
* @param {String} params.athleteId - 选手ID * @param {String} params.athleteId - 选手ID
* @returns {Object} 评分详情 * @returns {Object} 评分详情
@@ -131,10 +131,10 @@ export function getScoreDetail(params) {
} }
/** /**
* 修改评分(裁判 * 修改评分(裁判)
* @param {Object} params * @param {Object} params
* @param {String} params.athleteId - 选手ID * @param {String} params.athleteId - 选手ID
* @param {String} params.modifierId - 修改人ID裁判 * @param {String} params.modifierId - 修改人ID裁判)
* @param {Number} params.modifiedScore - 修改后的分数 * @param {Number} params.modifiedScore - 修改后的分数
* @param {String} params.note - 修改原因 * @param {String} params.note - 修改原因
* @returns {Object} 修改结果 * @returns {Object} 修改结果

View File

@@ -122,14 +122,14 @@ export default {
// 保存用户信息到全局数据 // 保存用户信息到全局数据
getApp().globalData = { getApp().globalData = {
userRole, // 'pub' 或 'admin' userRole, // 'pub' 或 'admin'
refereeType, // 1-裁判, 2-普通裁判 refereeType, // 1-裁判, 2-裁判
matchCode: this.matchCode, matchCode: this.matchCode,
matchId, matchId,
matchName, matchName,
matchTime, matchTime,
judgeId, judgeId,
judgeName, judgeName,
venueId, // 普通评委有场地,裁判为null venueId, // 普通评委有场地,裁判为null
venueName, venueName,
projects, // 分配的项目列表(从登录接口返回) projects, // 分配的项目列表(从登录接口返回)
currentProjectIndex: 0 // 当前选中的项目索引 currentProjectIndex: 0 // 当前选中的项目索引
@@ -155,12 +155,12 @@ export default {
// 根据角色跳转到不同页面 // 根据角色跳转到不同页面
setTimeout(() => { setTimeout(() => {
if (userRole === 'admin') { if (userRole === 'admin') {
// 裁判跳转到多场地列表页(可以修改评分) // 裁判跳转到多场地列表页(可以修改评分)
uni.navigateTo({ uni.navigateTo({
url: '/pages/score-list-multi/score-list-multi' url: '/pages/score-list-multi/score-list-multi'
}) })
} else { } else {
// 普通裁判跳转到评分列表页(可以评分) // 裁判跳转到评分列表页(可以评分)
uni.navigateTo({ uni.navigateTo({
url: '/pages/score-list/score-list' url: '/pages/score-list/score-list'
}) })

View File

@@ -142,7 +142,7 @@ export default {
// 获取当前选手信息(从 score-list-multi 页面传递) // 获取当前选手信息(从 score-list-multi 页面传递)
const currentAthlete = globalData.currentAthlete || const currentAthlete = globalData.currentAthlete ||
// 获取裁判ID // 获取裁判ID
this.modifierId = globalData.judgeId this.modifierId = globalData.judgeId
// 调试信息 // 调试信息

View File

@@ -31,28 +31,22 @@
<view class="score-control"> <view class="score-control">
<view class="control-btn decrease" @click="decreaseScore"> <view class="control-btn decrease" @click="decreaseScore">
<text class="btn-symbol"></text> <text class="btn-symbol"></text>
<!-- <text class="btn-value">-0.001</text> -->
</view> </view>
<view class="score-display"> <view class="score-display" @click="showScoreInput">
<text class="current-score">{{ currentScore.toFixed(3) }}</text> <text class="current-score">{{ currentScore.toFixed(3) }}</text>
<text class="edit-hint">点击编辑</text>
</view> </view>
<view class="control-btn increase" @click="increaseScore"> <view class="control-btn increase" @click="increaseScore">
<text class="btn-symbol"></text> <text class="btn-symbol"></text>
<!-- <text class="btn-value">+0.001</text> -->
</view> </view>
</view> </view>
<!-- <view class="judge-tip">
裁判评分保留3位小数点超过上限或下限时按钮置灰
</view> -->
<!-- 扣分项 --> <!-- 扣分项 -->
<view class="deduction-section"> <view class="deduction-section">
<view class="deduction-header"> <view class="deduction-header">
<text class="deduction-label">扣分项</text> <text class="deduction-label">扣分项</text>
<!-- <text class="deduction-hint">扣分项多选</text> -->
</view> </view>
<view class="deduction-list"> <view class="deduction-list">
@@ -82,12 +76,35 @@
v-model="note" v-model="note"
maxlength="200" maxlength="200"
/> />
<!-- <text class="optional-text">可不填</text> -->
</view> </view>
</view> </view>
<!-- 提交按钮 --> <!-- 提交按钮 -->
<button class="submit-btn" @click="handleSubmit">提交</button> <button class="submit-btn" @click="handleSubmit">提交</button>
<!-- 分数输入弹窗 -->
<view v-if="showInputModal" class="modal-overlay" @click="hideScoreInput">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">输入分数</text>
</view>
<view class="modal-body">
<input
type="digit"
class="score-input"
v-model="inputScore"
placeholder="请输入5-10之间的分数"
:focus="showInputModal"
@confirm="confirmScoreInput"
/>
<text class="input-hint">分数范围{{ minScore }} - {{ maxScore }}保留3位小数</text>
</view>
<view class="modal-footer">
<button class="modal-btn cancel" @click="hideScoreInput">取消</button>
<button class="modal-btn confirm" @click="confirmScoreInput">确定</button>
</view>
</view>
</view>
</view> </view>
</template> </template>
@@ -113,16 +130,16 @@ export default {
note: '', note: '',
minScore: 5.0, minScore: 5.0,
maxScore: 10.0, maxScore: 10.0,
deductions: [] deductions: [],
showInputModal: false,
inputScore: ''
} }
}, },
async onLoad() { async onLoad() {
// 获取全局数据
const app = getApp() const app = getApp()
const globalData = app.globalData || {} const globalData = app.globalData || {}
// 加载当前选手信息(从 score-list 页面传递)
const currentAthlete = globalData.currentAthlete || {} const currentAthlete = globalData.currentAthlete || {}
this.player = { this.player = {
athleteId: currentAthlete.athleteId || '', athleteId: currentAthlete.athleteId || '',
@@ -132,18 +149,15 @@ export default {
number: currentAthlete.number || '' number: currentAthlete.number || ''
} }
// 如果选手已评分,加载其原有评分
if (currentAthlete.scored && currentAthlete.myScore) { if (currentAthlete.scored && currentAthlete.myScore) {
this.currentScore = currentAthlete.myScore this.currentScore = currentAthlete.myScore
} }
// 加载评委ID和项目ID
this.judgeId = globalData.judgeId this.judgeId = globalData.judgeId
this.projectId = globalData.currentProjectId || '' this.projectId = globalData.currentProjectId || ''
this.competitionId = globalData.matchId || globalData.matchCode || '' this.competitionId = globalData.matchId || globalData.matchCode || ''
this.venueId = globalData.currentVenueId || globalData.venueId || '' this.venueId = globalData.currentVenueId || globalData.venueId || ''
// 调试信息
if (config.debug) { if (config.debug) {
console.log('评分详情页加载:', { console.log('评分详情页加载:', {
athlete: this.player, athlete: this.player,
@@ -155,22 +169,17 @@ export default {
}) })
} }
// 加载扣分项列表
await this.loadDeductions() await this.loadDeductions()
}, },
methods: { methods: {
async loadDeductions() { async loadDeductions() {
try { try {
// 🔥 关键改动:使用 dataAdapter 获取扣分项列表
// Mock模式调用 mock/score.js 的 getDeductions 函数
// API模式调用 api/score.js 的 getDeductions 函数GET /martial/deductionItem/list
const response = await dataAdapter.getData('getDeductions', { const response = await dataAdapter.getData('getDeductions', {
projectId: this.projectId projectId: this.projectId
}) })
// 为每个扣分项添加 checked 状态,并映射字段名 const records = response.data && response.data.records ? response.data.records : []
const records = response.data?.records || []
this.deductions = records.map(item => ({ this.deductions = records.map(item => ({
deductionId: item.id, deductionId: item.id,
deductionName: item.itemName, deductionName: item.itemName,
@@ -178,7 +187,6 @@ export default {
checked: false checked: false
})) }))
// 调试信息
if (config.debug) { if (config.debug) {
console.log('扣分项加载成功:', this.deductions) console.log('扣分项加载成功:', this.deductions)
} }
@@ -201,7 +209,6 @@ export default {
delta: 1, delta: 1,
fail: (err) => { fail: (err) => {
console.error('返回失败:', err) console.error('返回失败:', err)
// 如果返回失败,尝试跳转到评分列表页
uni.redirectTo({ uni.redirectTo({
url: '/pages/score-list/score-list' url: '/pages/score-list/score-list'
}) })
@@ -221,12 +228,44 @@ export default {
} }
}, },
showScoreInput() {
this.inputScore = this.currentScore.toFixed(3)
this.showInputModal = true
},
hideScoreInput() {
this.showInputModal = false
this.inputScore = ''
},
confirmScoreInput() {
const score = parseFloat(this.inputScore)
if (isNaN(score)) {
uni.showToast({
title: '请输入有效的数字',
icon: 'none'
})
return
}
if (score < this.minScore || score > this.maxScore) {
uni.showToast({
title: `分数必须在${this.minScore}-${this.maxScore}之间`,
icon: 'none'
})
return
}
this.currentScore = parseFloat(score.toFixed(3))
this.hideScoreInput()
},
toggleDeduction(index) { toggleDeduction(index) {
this.deductions[index].checked = !this.deductions[index].checked this.deductions[index].checked = !this.deductions[index].checked
}, },
async handleSubmit() { async handleSubmit() {
// 验证评分范围
if (this.currentScore < this.minScore || this.currentScore > this.maxScore) { if (this.currentScore < this.minScore || this.currentScore > this.maxScore) {
uni.showToast({ uni.showToast({
title: `评分必须在${this.minScore}-${this.maxScore}分之间`, title: `评分必须在${this.minScore}-${this.maxScore}分之间`,
@@ -235,7 +274,6 @@ export default {
return return
} }
// 验证必需字段
if (!this.competitionId) { if (!this.competitionId) {
uni.showToast({ uni.showToast({
title: '缺少比赛ID请重新登录', title: '缺少比赛ID请重新登录',
@@ -252,7 +290,6 @@ export default {
return return
} }
// 收集选中的扣分项ID
const selectedDeductions = this.deductions const selectedDeductions = this.deductions
.filter(item => item.checked) .filter(item => item.checked)
.map(item => item.deductionId) .map(item => item.deductionId)
@@ -263,7 +300,6 @@ export default {
mask: true mask: true
}) })
// 准备提交数据
const submitData = { const submitData = {
athleteId: this.player.athleteId, athleteId: this.player.athleteId,
judgeId: this.judgeId, judgeId: this.judgeId,
@@ -275,19 +311,14 @@ export default {
note: this.note note: this.note
} }
// 调试日志:打印提交数据
if (config.debug) { if (config.debug) {
console.log('准备提交评分数据:', submitData) console.log('准备提交评分数据:', submitData)
} }
// 🔥 关键改动:使用 dataAdapter 提交评分
// Mock模式调用 mock/score.js 的 submitScore 函数
// API模式调用 api/score.js 的 submitScore 函数POST /martial/score/submit
const response = await dataAdapter.getData('submitScore', submitData) const response = await dataAdapter.getData('submitScore', submitData)
uni.hideLoading() uni.hideLoading()
// 调试信息
if (config.debug) { if (config.debug) {
console.log('评分提交成功:', { console.log('评分提交成功:', {
athleteId: this.player.athleteId, athleteId: this.player.athleteId,
@@ -297,14 +328,12 @@ export default {
}) })
} }
// 显示成功提示
uni.showToast({ uni.showToast({
title: '提交成功', title: '提交成功',
icon: 'success', icon: 'success',
duration: 1500 duration: 1500
}) })
// 返回上一页
setTimeout(() => { setTimeout(() => {
uni.navigateBack() uni.navigateBack()
}, 1500) }, 1500)
@@ -485,6 +514,14 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
cursor: pointer;
padding: 20rpx;
border-radius: 16rpx;
transition: background-color 0.2s;
}
.score-display:active {
background-color: rgba(27, 124, 94, 0.1);
} }
.current-score { .current-score {
@@ -493,6 +530,12 @@ export default {
color: #1B7C5E; color: #1B7C5E;
} }
.edit-hint {
font-size: 22rpx;
color: #999999;
margin-top: 8rpx;
}
.judge-tip { .judge-tip {
padding: 0 30rpx; padding: 0 30rpx;
font-size: 24rpx; font-size: 24rpx;
@@ -628,4 +671,95 @@ export default {
.submit-btn:active { .submit-btn:active {
opacity: 0.9; opacity: 0.9;
} }
/* 分数输入弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 600rpx;
background-color: #FFFFFF;
border-radius: 24rpx;
overflow: hidden;
}
.modal-header {
padding: 40rpx 30rpx 20rpx;
text-align: center;
}
.modal-title {
font-size: 34rpx;
font-weight: 600;
color: #333333;
}
.modal-body {
padding: 20rpx 30rpx 30rpx;
}
.score-input {
width: 100%;
height: 90rpx;
border: 2rpx solid #E0E0E0;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 36rpx;
text-align: center;
color: #1B7C5E;
font-weight: 600;
}
.score-input:focus {
border-color: #1B7C5E;
}
.input-hint {
display: block;
margin-top: 16rpx;
font-size: 24rpx;
color: #999999;
text-align: center;
}
.modal-footer {
display: flex;
border-top: 1rpx solid #E0E0E0;
}
.modal-btn {
flex: 1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
background: none;
border: none;
border-radius: 0;
}
.modal-btn.cancel {
color: #666666;
border-right: 1rpx solid #E0E0E0;
}
.modal-btn.confirm {
color: #1B7C5E;
font-weight: 600;
}
.modal-btn:active {
background-color: #F5F5F5;
}
</style> </style>

View File

@@ -32,7 +32,7 @@
</scroll-view> </scroll-view>
<view class="venue-tip"> <view class="venue-tip">
<!-- <text class="tip-bold">裁判可看见所有场地和项目</text> --> <!-- <text class="tip-bold">裁判可看见所有场地和项目</text> -->
<!-- <text class="tip-normal">场地和项目可动态全部可以点击切换</text> --> <!-- <text class="tip-normal">场地和项目可动态全部可以点击切换</text> -->
</view> </view>
@@ -130,12 +130,12 @@ export default {
time: globalData.matchTime || '比赛时间' time: globalData.matchTime || '比赛时间'
} }
// 注意:裁判没有固定场地和项目,需要查看所有 // 注意:裁判没有固定场地和项目,需要查看所有
this.competitionId = globalData.matchId this.competitionId = globalData.matchId
// 调试信息 // 调试信息
if (config.debug) { if (config.debug) {
console.log('裁判列表页加载:', { console.log('裁判列表页加载:', {
userRole: globalData.userRole, userRole: globalData.userRole,
competitionId: this.competitionId competitionId: this.competitionId
}) })
@@ -212,7 +212,7 @@ export default {
mask: true mask: true
}) })
// 🔥 关键改动:使用 dataAdapter 获取选手列表(裁判视图) // 🔥 关键改动:使用 dataAdapter 获取选手列表(裁判视图)
// Mock模式调用 mock/athlete.js 的 getAthletesForAdmin 函数 // Mock模式调用 mock/athlete.js 的 getAthletesForAdmin 函数
// API模式调用 api/athlete.js 的 getAthletesForAdmin 函数GET /api/mini/athletes/admin // API模式调用 api/athlete.js 的 getAthletesForAdmin 函数GET /api/mini/athletes/admin
const response = await dataAdapter.getData('getAthletesForAdmin', { const response = await dataAdapter.getData('getAthletesForAdmin', {
@@ -226,7 +226,7 @@ export default {
// 保存选手列表 // 保存选手列表
this.players = (response.data.records || response.data) || [] this.players = (response.data.records || response.data) || []
// 计算评分统计(裁判视图:统计有总分的选手) // 计算评分统计(裁判视图:统计有总分的选手)
this.totalCount = this.players.length this.totalCount = this.players.length
this.scoredCount = this.players.filter(p => p.scoringComplete).length this.scoredCount = this.players.filter(p => p.scoringComplete).length

View File

@@ -39,6 +39,7 @@
> >
{{ project.projectName }} {{ project.projectName }}
</view> </view>
<view class="no-project-tip" v-if="projects.length === 0">当前场地暂无比赛项目</view>
</view> </view>
</view> </view>
@@ -49,7 +50,7 @@
</view> </view>
<!-- 选手列表 --> <!-- 选手列表 -->
<view class="player-list"> <view class="player-list" v-if="projects.length > 0">
<!-- 遍历选手列表 --> <!-- 遍历选手列表 -->
<view <view
class="player-card" class="player-card"
@@ -60,7 +61,7 @@
<view class="player-header"> <view class="player-header">
<view class="player-name">{{ player.name }}</view> <view class="player-name">{{ player.name }}</view>
<!-- 裁判显示总分和已评分裁判数 --> <!-- 裁判显示总分和已评分裁判数 -->
<view class="player-scores" v-if="refereeType === 1"> <view class="player-scores" v-if="refereeType === 1">
<text class="total-score"> <text class="total-score">
总分{{ player.scoringComplete ? player.totalScore : '评分中' }} 总分{{ player.scoringComplete ? player.totalScore : '评分中' }}
@@ -70,7 +71,7 @@
</text> </text>
</view> </view>
<!-- 普通裁判根据评分状态显示不同内容 --> <!-- 裁判根据评分状态显示不同内容 -->
<view class="judge-action" v-else> <view class="judge-action" v-else>
<!-- 已评分显示分数和修改按钮 --> <!-- 已评分显示分数和修改按钮 -->
<view class="scored-info" v-if="player.scored"> <view class="scored-info" v-if="player.scored">
@@ -124,7 +125,7 @@ export default {
}, },
judgeId: '', judgeId: '',
matchId: '', matchId: '',
refereeType: 2, // 裁判类型1-裁判, 2-普通裁判) refereeType: 2, // 裁判类型1-裁判, 2-裁判
venues: [], // 所有场地列表 venues: [], // 所有场地列表
currentVenueIndex: 0, // 当前选中的场地索引 currentVenueIndex: 0, // 当前选中的场地索引
projects: [], // 所有项目列表 projects: [], // 所有项目列表
@@ -149,7 +150,7 @@ export default {
this.judgeId = globalData.judgeId this.judgeId = globalData.judgeId
this.matchId = globalData.matchId || globalData.matchCode this.matchId = globalData.matchId || globalData.matchCode
this.refereeType = globalData.refereeType || 2 // 默认为普通裁判 this.refereeType = globalData.refereeType || 2 // 默认为裁判
// 调试信息 // 调试信息
if (config.debug) { if (config.debug) {
@@ -277,15 +278,15 @@ export default {
/** /**
* 处理选手卡片点击 * 处理选手卡片点击
* - 裁判:跳转到查看详情页面 * - 裁判:跳转到查看详情页面
* - 普通裁判:不处理(通过评分按钮跳转) * - 裁判:不处理(通过评分按钮跳转)
*/ */
handlePlayerClick(player) { handlePlayerClick(player) {
if (this.refereeType === 1) { if (this.refereeType === 1) {
// 裁判:查看评分详情 // 裁判:查看评分详情
this.goToScoreDetail(player) this.goToScoreDetail(player)
} }
// 普通裁判不处理卡片点击,只能通过评分按钮跳转 // 裁判不处理卡片点击,只能通过评分按钮跳转
}, },
goToScoreDetail(player) { goToScoreDetail(player) {
@@ -685,4 +686,12 @@ export default {
color: #666666; color: #666666;
line-height: 1.5; line-height: 1.5;
} }
.no-project-tip {
padding: 30rpx;
text-align: center;
color: #999;
font-size: 28rpx;
width: 100%;
}
</style> </style>

View File

@@ -28,14 +28,14 @@ export function getMyAthletes(params) {
method: 'GET', method: 'GET',
params: { params: {
...params, ...params,
refereeType: 2 // 普通裁判 refereeType: 2 // 裁判
}, },
showLoading: true showLoading: true
}) })
} }
/** /**
* 获取选手列表(裁判 * 获取选手列表(裁判)
* @param {Object} params * @param {Object} params
* @param {String} params.competitionId - 比赛ID * @param {String} params.competitionId - 比赛ID
* @param {String} params.venueId - 场地ID * @param {String} params.venueId - 场地ID
@@ -51,7 +51,7 @@ export function getAthletesForAdmin(params) {
method: 'GET', method: 'GET',
params: { params: {
...params, ...params,
refereeType: 1 // 裁判 refereeType: 1 // 裁判
}, },
showLoading: true showLoading: true
}) })

View File

@@ -46,7 +46,7 @@ export default {
getMyAthletes: athleteApi.getMyAthletes, getMyAthletes: athleteApi.getMyAthletes,
/** /**
* 获取选手列表(裁判 * 获取选手列表(裁判)
* @param {Object} params - { competitionId, venueId, projectId } * @param {Object} params - { competitionId, venueId, projectId }
* @returns {Promise} * @returns {Promise}
*/ */
@@ -82,14 +82,14 @@ export default {
submitScore: scoreApi.submitScore, submitScore: scoreApi.submitScore,
/** /**
* 获取评分详情(裁判查看) * 获取评分详情(裁判查看)
* @param {Object} params - { athleteId } * @param {Object} params - { athleteId }
* @returns {Promise} * @returns {Promise}
*/ */
getScoreDetail: scoreApi.getScoreDetail, getScoreDetail: scoreApi.getScoreDetail,
/** /**
* 修改评分(裁判 * 修改评分(裁判)
* @param {Object} data - { athleteId, modifierId, modifiedScore, note } * @param {Object} data - { athleteId, modifierId, modifiedScore, note }
* @returns {Promise} * @returns {Promise}
*/ */
@@ -127,7 +127,7 @@ export default {
* 1. 需要实现的新接口(小程序专用): * 1. 需要实现的新接口(小程序专用):
* - POST /api/mini/login # 登录验证 * - POST /api/mini/login # 登录验证
* - GET /api/mini/athletes # 普通评委选手列表 * - GET /api/mini/athletes # 普通评委选手列表
* - GET /api/mini/athletes/admin # 裁判选手列表 * - GET /api/mini/athletes/admin # 裁判选手列表
* - GET /api/mini/score/detail/{athleteId} # 评分详情 * - GET /api/mini/score/detail/{athleteId} # 评分详情
* - PUT /api/mini/score/modify # 修改评分 * - PUT /api/mini/score/modify # 修改评分
* *

View File

@@ -44,7 +44,7 @@ export function submitScore(data) {
} }
/** /**
* 获取评分详情(裁判查看) * 获取评分详情(裁判查看)
* @param {Object} params * @param {Object} params
* @param {String} params.athleteId - 选手ID * @param {String} params.athleteId - 选手ID
* @returns {Promise} * @returns {Promise}
@@ -61,7 +61,7 @@ export function getScoreDetail(params) {
} }
/** /**
* 修改评分(裁判 * 修改评分(裁判)
* @param {Object} data * @param {Object} data
* @param {String} data.athleteId - 选手ID * @param {String} data.athleteId - 选手ID
* @param {String} data.modifierId - 修改人ID * @param {String} data.modifierId - 修改人ID
@@ -158,7 +158,7 @@ export default {
* } * }
* *
* 实现逻辑: * 实现逻辑:
* 1. 验证权限(只有裁判可以修改) * 1. 验证权限(只有裁判可以修改)
* 2. 保存 originalScore如果是第一次修改 * 2. 保存 originalScore如果是第一次修改
* 3. 更新 totalScore * 3. 更新 totalScore
* 4. 记录 modifyReason 和 modifyTime * 4. 记录 modifyReason 和 modifyTime

View File

@@ -18,7 +18,7 @@ const ENV_CONFIG = {
// API基础路径dataMode为'api'时使用) // API基础路径dataMode为'api'时使用)
// uni.request 不支持 devServer proxy必须用完整地址 // uni.request 不支持 devServer proxy必须用完整地址
apiBaseURL: 'http://142.91.105.230:8123', apiBaseURL: 'https://martial-api.aitisai.com',
// 请求超时时间(毫秒) // 请求超时时间(毫秒)
timeout: 30000, timeout: 30000,
@@ -39,7 +39,7 @@ const ENV_CONFIG = {
// 生产环境配置 // 生产环境配置
production: { production: {
dataMode: 'api', dataMode: 'api',
apiBaseURL: 'https://api.yourdomain.com', apiBaseURL: 'https://martial-api.aitisai.com',
debug: false, debug: false,
timeout: 30000, timeout: 30000,
mockDelay: 0 mockDelay: 0

View File

@@ -51,7 +51,7 @@ export function getMyAthletes(params) {
} }
/** /**
* 获取选手列表(裁判 * 获取选手列表(裁判)
* @param {Object} params * @param {Object} params
* @param {String} params.competitionId - 比赛ID * @param {String} params.competitionId - 比赛ID
* @param {String} params.venueId - 场地ID * @param {String} params.venueId - 场地ID

View File

@@ -34,7 +34,7 @@ export default {
getMyAthletes: athleteMock.getMyAthletes, getMyAthletes: athleteMock.getMyAthletes,
/** /**
* 获取选手列表(裁判 * 获取选手列表(裁判)
* @param {Object} params - { competitionId, venueId, projectId } * @param {Object} params - { competitionId, venueId, projectId }
* @returns {Array} 选手列表(带评分统计) * @returns {Array} 选手列表(带评分统计)
*/ */
@@ -70,14 +70,14 @@ export default {
submitScore: scoreMock.submitScore, submitScore: scoreMock.submitScore,
/** /**
* 获取评分详情(裁判查看) * 获取评分详情(裁判查看)
* @param {Object} params - { athleteId } * @param {Object} params - { athleteId }
* @returns {Object} 评分详情(选手信息+评委评分) * @returns {Object} 评分详情(选手信息+评委评分)
*/ */
getScoreDetail: scoreMock.getScoreDetail, getScoreDetail: scoreMock.getScoreDetail,
/** /**
* 修改评分(裁判 * 修改评分(裁判)
* @param {Object} params - { athleteId, modifierId, modifiedScore, note } * @param {Object} params - { athleteId, modifierId, modifiedScore, note }
* @returns {Object} 修改结果 * @returns {Object} 修改结果
*/ */

View File

@@ -29,7 +29,7 @@ export function login(params) {
matchTime: '2025年6月25日 9:00', matchTime: '2025年6月25日 9:00',
judgeId: '456', judgeId: '456',
judgeName: '欧阳丽娜', judgeName: '欧阳丽娜',
// 普通评委有固定场地,裁判可以查看所有场地 // 普通评委有固定场地,裁判可以查看所有场地
venueId: role === 'pub' ? '1' : null, venueId: role === 'pub' ? '1' : null,
venueName: role === 'pub' ? '第一场地' : null, venueName: role === 'pub' ? '第一场地' : null,
// 分配的项目列表(对象数组格式) // 分配的项目列表(对象数组格式)

View File

@@ -56,7 +56,7 @@ export function submitScore(params) {
} }
/** /**
* 获取评分详情(裁判查看) * 获取评分详情(裁判查看)
* @param {Object} params * @param {Object} params
* @param {String} params.athleteId - 选手ID * @param {String} params.athleteId - 选手ID
* @returns {Object} 评分详情 * @returns {Object} 评分详情
@@ -125,10 +125,10 @@ export function getScoreDetail(params) {
} }
/** /**
* 修改评分(裁判 * 修改评分(裁判)
* @param {Object} params * @param {Object} params
* @param {String} params.athleteId - 选手ID * @param {String} params.athleteId - 选手ID
* @param {String} params.modifierId - 修改人ID裁判 * @param {String} params.modifierId - 修改人ID裁判)
* @param {Number} params.modifiedScore - 修改后的分数 * @param {Number} params.modifiedScore - 修改后的分数
* @param {String} params.note - 修改原因 * @param {String} params.note - 修改原因
* @returns {Object} 修改结果 * @returns {Object} 修改结果

View File

@@ -36,6 +36,13 @@
"navigationBarTitleText": "", "navigationBarTitleText": "",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "pages/general-judge/general-judge",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
} }
], ],
"globalStyle": { "globalStyle": {

View File

@@ -0,0 +1,556 @@
<template>
<view class="container">
<!-- 导航栏 -->
<view class="nav-bar">
<view class="nav-title">总裁评分系统</view>
<view class="nav-right">
<view class="logout-btn" @click="handleLogout">退出</view>
</view>
</view>
<!-- 用户信息 -->
<view class="user-info">
<view class="user-name">{{ judgeName }}</view>
<view class="user-role">总裁裁判长</view>
<view class="match-name">{{ matchName }}</view>
</view>
<!-- 场地选择 -->
<view class="venue-section">
<view class="section-title">选择场地</view>
<view class="venue-list">
<view
class="venue-item"
:class="{ active: selectedVenueId === null }"
@click="selectVenue(null)"
>
<text>全部场地</text>
</view>
<view
v-for="venue in venues"
:key="venue.id"
class="venue-item"
:class="{ active: selectedVenueId === venue.id }"
@click="selectVenue(venue.id)"
>
<text>{{ venue.venueName }}</text>
</view>
</view>
</view>
<!-- 待确认成绩列表 -->
<view class="result-section">
<view class="section-title">
待确认成绩
<text class="count">({{ pendingResults.length }})</text>
</view>
<view v-if="loading" class="loading">
<text>加载中...</text>
</view>
<view v-else-if="pendingResults.length === 0" class="empty">
<text>暂无待确认成绩</text>
</view>
<view v-else class="result-list">
<view
v-for="result in pendingResults"
:key="result.id"
class="result-item pending"
@click="showConfirmDialog(result)"
>
<view class="result-info">
<view class="player-name">{{ result.playerName }}</view>
<view class="team-name">{{ result.teamName }}</view>
</view>
<view class="score-info">
<view class="chief-score">
<text class="label">主裁判分:</text>
<text class="value">{{ result.chiefJudgeScore || result.finalScore }}</text>
</view>
<view class="status-tag pending">待确认</view>
</view>
</view>
</view>
</view>
<!-- 已确认成绩列表 -->
<view class="result-section">
<view class="section-title">
已确认成绩
<text class="count">({{ confirmedResults.length }})</text>
</view>
<view v-if="confirmedResults.length === 0" class="empty">
<text>暂无已确认成绩</text>
</view>
<view v-else class="result-list">
<view
v-for="result in confirmedResults"
:key="result.id"
class="result-item confirmed"
>
<view class="result-info">
<view class="player-name">{{ result.playerName }}</view>
<view class="team-name">{{ result.teamName }}</view>
</view>
<view class="score-info">
<view class="chief-score">
<text class="label">最终得分:</text>
<text class="value confirmed">{{ result.finalScore }}</text>
</view>
<view class="status-tag confirmed">已确认</view>
</view>
</view>
</view>
</view>
<!-- 确认弹窗 -->
<view v-if="showDialog" class="dialog-mask" @click="closeDialog">
<view class="dialog-content" @click.stop>
<view class="dialog-title">确认/修改分数</view>
<view class="dialog-body">
<view class="info-row">
<text class="label">选手:</text>
<text class="value">{{ currentResult.playerName }}</text>
</view>
<view class="info-row">
<text class="label">主裁判分:</text>
<text class="value">{{ currentResult.chiefJudgeScore || currentResult.finalScore }}</text>
</view>
<view class="input-row">
<text class="label">确认分数:</text>
<input
type="digit"
v-model="confirmScore"
placeholder="留空则确认原分数"
class="score-input"
/>
</view>
<view class="input-row">
<text class="label">备注:</text>
<input
type="text"
v-model="confirmNote"
placeholder="可选"
class="note-input"
/>
</view>
</view>
<view class="dialog-footer">
<button class="btn-cancel" @click="closeDialog">取消</button>
<button class="btn-confirm" @click="confirmResult">确认</button>
</view>
</view>
</view>
</view>
</template>
<script>
import config from "@/config/env.config.js"
export default {
data() {
return {
judgeName: "",
matchName: "",
matchId: null,
venues: [],
selectedVenueId: null,
pendingResults: [],
confirmedResults: [],
loading: false,
showDialog: false,
currentResult: {},
confirmScore: "",
confirmNote: ""
}
},
onLoad() {
const app = getApp()
this.judgeName = app.globalData.judgeName || ""
this.matchName = app.globalData.matchName || ""
this.matchId = app.globalData.matchId
this.loadVenues()
this.loadAllResults()
},
methods: {
async loadVenues() {
try {
const res = await uni.request({
url: config.apiBaseURL + "/mini/general/venues",
method: "GET",
data: { competitionId: this.matchId },
header: {
"Authorization": uni.getStorageSync("token")
}
})
if (res[1].data.success) {
this.venues = res[1].data.data || []
}
} catch (e) {
console.error("加载场地失败:", e)
}
},
async loadAllResults() {
this.loading = true
try {
// 加载待确认成绩
const pendingRes = await uni.request({
url: config.apiBaseURL + "/mini/general/pending",
method: "GET",
data: { competitionId: this.matchId },
header: {
"Authorization": uni.getStorageSync("token")
}
})
if (pendingRes[1].data.success) {
let results = pendingRes[1].data.data || []
if (this.selectedVenueId) {
results = results.filter(r => r.venueId === this.selectedVenueId)
}
this.pendingResults = results
}
// 加载已确认成绩
const confirmedRes = await uni.request({
url: config.apiBaseURL + "/mini/general/confirmed",
method: "GET",
data: { competitionId: this.matchId },
header: {
"Authorization": uni.getStorageSync("token")
}
})
if (confirmedRes[1].data.success) {
let results = confirmedRes[1].data.data || []
if (this.selectedVenueId) {
results = results.filter(r => r.venueId === this.selectedVenueId)
}
this.confirmedResults = results
}
} catch (e) {
console.error("加载成绩失败:", e)
} finally {
this.loading = false
}
},
selectVenue(venueId) {
this.selectedVenueId = venueId
this.loadAllResults()
},
showConfirmDialog(result) {
this.currentResult = result
this.confirmScore = ""
this.confirmNote = ""
this.showDialog = true
},
closeDialog() {
this.showDialog = false
this.currentResult = {}
},
async confirmResult() {
const app = getApp()
try {
uni.showLoading({ title: "提交中...", mask: true })
const res = await uni.request({
url: config.apiBaseURL + "/mini/general/confirm",
method: "POST",
data: {
resultId: String(this.currentResult.id),
generalJudgeId: String(app.globalData.judgeId),
score: this.confirmScore ? parseFloat(this.confirmScore) : null,
note: this.confirmNote || null
},
header: {
"Content-Type": "application/json",
"Authorization": uni.getStorageSync("token")
}
})
uni.hideLoading()
if (res[1].data.success) {
uni.showToast({ title: "确认成功", icon: "success" })
this.closeDialog()
this.loadAllResults()
} else {
uni.showToast({ title: res[1].data.msg || "确认失败", icon: "none" })
}
} catch (e) {
uni.hideLoading()
uni.showToast({ title: "网络错误", icon: "none" })
}
},
handleLogout() {
uni.showModal({
title: "提示",
content: "确定要退出登录吗?",
success: (res) => {
if (res.confirm) {
uni.removeStorageSync("token")
uni.reLaunch({ url: "/pages/login/login" })
}
}
})
}
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 40rpx;
}
.nav-bar {
height: 90rpx;
background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 0 30rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-right {
position: absolute;
right: 30rpx;
}
.logout-btn {
font-size: 28rpx;
color: #FFFFFF;
padding: 10rpx 20rpx;
background: rgba(255,255,255,0.2);
border-radius: 8rpx;
}
.user-info {
background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%);
padding: 30rpx;
color: #FFFFFF;
}
.user-name {
font-size: 40rpx;
font-weight: 600;
}
.user-role {
font-size: 28rpx;
opacity: 0.9;
margin-top: 10rpx;
}
.match-name {
font-size: 26rpx;
opacity: 0.8;
margin-top: 10rpx;
}
.venue-section, .result-section {
margin: 20rpx;
background: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.count {
font-size: 28rpx;
color: #999;
font-weight: normal;
}
.venue-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.venue-item {
padding: 16rpx 32rpx;
background: #F5F5F5;
border-radius: 8rpx;
font-size: 28rpx;
color: #666;
}
.venue-item.active {
background: #8B4513;
color: #FFFFFF;
}
.loading, .empty {
text-align: center;
padding: 40rpx;
color: #999;
font-size: 28rpx;
}
.result-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background: #FAFAFA;
border-radius: 12rpx;
}
.result-item.pending {
border-left: 6rpx solid #FF9800;
}
.result-item.confirmed {
border-left: 6rpx solid #4CAF50;
}
.player-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.team-name {
font-size: 26rpx;
color: #666;
margin-top: 8rpx;
}
.score-info {
text-align: right;
}
.chief-score .label {
font-size: 24rpx;
color: #999;
}
.chief-score .value {
font-size: 36rpx;
font-weight: 600;
color: #FF9800;
margin-left: 10rpx;
}
.chief-score .value.confirmed {
color: #4CAF50;
}
.status-tag {
font-size: 24rpx;
margin-top: 8rpx;
}
.status-tag.pending {
color: #FF9800;
}
.status-tag.confirmed {
color: #4CAF50;
}
.dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.dialog-content {
width: 80%;
background: #FFFFFF;
border-radius: 16rpx;
overflow: hidden;
}
.dialog-title {
font-size: 34rpx;
font-weight: 600;
text-align: center;
padding: 30rpx;
border-bottom: 1rpx solid #EEE;
}
.dialog-body {
padding: 30rpx;
}
.info-row, .input-row {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.info-row .label, .input-row .label {
width: 160rpx;
font-size: 28rpx;
color: #666;
}
.info-row .value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.score-input, .note-input {
flex: 1;
height: 70rpx;
border: 1rpx solid #DDD;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.dialog-footer {
display: flex;
border-top: 1rpx solid #EEE;
}
.btn-cancel, .btn-confirm {
flex: 1;
height: 90rpx;
line-height: 90rpx;
text-align: center;
font-size: 32rpx;
border: none;
border-radius: 0;
}
.btn-cancel {
background: #F5F5F5;
color: #666;
}
.btn-confirm {
background: #8B4513;
color: #FFFFFF;
}
</style>

View File

@@ -129,7 +129,7 @@ export default {
matchTime, matchTime,
judgeId, judgeId,
judgeName, judgeName,
venueId, // 普通评委有场地,裁判为null venueId, // 普通评委有场地,裁判为null
venueName, venueName,
projects, // 分配的项目列表 projects, // 分配的项目列表
currentProjectIndex: 0 // 当前选中的项目索引 currentProjectIndex: 0 // 当前选中的项目索引
@@ -154,13 +154,18 @@ export default {
// 根据角色跳转到不同页面 // 根据角色跳转到不同页面
setTimeout(() => { setTimeout(() => {
if (userRole === 'admin') { if (userRole === 'general') {
// 裁判长跳转到多场地列表页(可以修改评分) // 总裁跳转到总裁专用页面
uni.navigateTo({
url: '/pages/general-judge/general-judge'
})
} else if (userRole === 'admin') {
// 主裁判跳转到多场地列表页(可以修改评分)
uni.navigateTo({ uni.navigateTo({
url: '/pages/score-list-multi/score-list-multi' url: '/pages/score-list-multi/score-list-multi'
}) })
} else { } else {
// 普通裁判跳转到评分列表页(可以评分) // 裁判跳转到评分列表页(可以评分)
uni.navigateTo({ uni.navigateTo({
url: '/pages/score-list/score-list' url: '/pages/score-list/score-list'
}) })

View File

@@ -135,9 +135,9 @@ export default {
return return
} }
// 检查是否是裁判 // 检查是否是裁判
if (globalData.userRole !== 'admin') { if (globalData.userRole !== 'admin') {
console.warn('非裁判用户,无权修改评分') console.warn('非裁判用户,无权修改评分')
uni.showToast({ uni.showToast({
title: '无权限', title: '无权限',
icon: 'none', icon: 'none',
@@ -166,7 +166,7 @@ export default {
// 获取当前选手信息(从 score-list-multi 页面传递) // 获取当前选手信息(从 score-list-multi 页面传递)
const currentAthlete = globalData.currentAthlete || {} const currentAthlete = globalData.currentAthlete || {}
// 获取裁判ID // 获取裁判ID
this.modifierId = globalData.judgeId this.modifierId = globalData.judgeId
// 🔥 关键修复:先用传递过来的选手数据初始化页面 // 🔥 关键修复:先用传递过来的选手数据初始化页面

View File

@@ -24,35 +24,36 @@
<!-- 评分提示 --> <!-- 评分提示 -->
<view class="score-tip"> <view class="score-tip">
点击分数填写或拖动滑块打分5-10 直接输入分数或使用加减按钮调整5-10
</view> </view>
<!-- 分数调整 --> <!-- 分数调整 -->
<view class="score-control"> <view class="score-control">
<view class="control-btn decrease" @click="decreaseScore"> <view class="control-btn decrease" @click="decreaseScore">
<text class="btn-symbol"></text> <text class="btn-symbol"></text>
<!-- <text class="btn-value">-0.001</text> -->
</view> </view>
<view class="score-display"> <view class="score-display">
<text class="current-score">{{ currentScore.toFixed(3) }}</text> <input
type="digit"
class="score-input-inline"
:value="scoreInputValue"
@input="onScoreInput"
@blur="onScoreBlur"
@confirm="onScoreConfirm"
placeholder="8.000"
/>
</view> </view>
<view class="control-btn increase" @click="increaseScore"> <view class="control-btn increase" @click="increaseScore">
<text class="btn-symbol"></text> <text class="btn-symbol"></text>
<!-- <text class="btn-value">+0.001</text> -->
</view> </view>
</view> </view>
<!-- <view class="judge-tip">
裁判评分保留3位小数点超过上限或下限时按钮置灰
</view> -->
<!-- 扣分项 --> <!-- 扣分项 -->
<view class="deduction-section"> <view class="deduction-section">
<view class="deduction-header"> <view class="deduction-header">
<text class="deduction-label">扣分项</text> <text class="deduction-label">扣分项</text>
<!-- <text class="deduction-hint">扣分项多选</text> -->
</view> </view>
<view class="deduction-list"> <view class="deduction-list">
@@ -82,12 +83,35 @@
v-model="note" v-model="note"
maxlength="200" maxlength="200"
/> />
<!-- <text class="optional-text">可不填</text> -->
</view> </view>
</view> </view>
<!-- 提交按钮 --> <!-- 提交按钮 -->
<button class="submit-btn" @click="handleSubmit">提交</button> <button class="submit-btn" @click="handleSubmit">提交</button>
<!-- 分数输入弹窗 -->
<view v-if="showInputModal" class="modal-overlay" @click="hideScoreInput">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">输入分数</text>
</view>
<view class="modal-body">
<input
type="digit"
class="score-input"
v-model="inputScore"
placeholder="请输入5-10之间的分数"
:focus="showInputModal"
@confirm="confirmScoreInput"
/>
<text class="input-hint">分数范围{{ minScore }} - {{ maxScore }}保留3位小数</text>
</view>
<view class="modal-footer">
<button class="modal-btn cancel" @click="hideScoreInput">取消</button>
<button class="modal-btn confirm" @click="confirmScoreInput">确定</button>
</view>
</view>
</view>
</view> </view>
</template> </template>
@@ -107,50 +131,28 @@ export default {
}, },
judgeId: '', judgeId: '',
projectId: '', projectId: '',
competitionId: '',
venueId: '',
currentScore: 8.000, currentScore: 8.000,
note: '', note: '',
minScore: 5.0, minScore: 5.0,
maxScore: 10.0, maxScore: 10.0,
deductions: [] deductions: [],
showInputModal: false,
inputScore: ''
}
},
computed: {
scoreInputValue() {
return this.currentScore.toFixed(3)
} }
}, },
async onLoad() { async onLoad() {
// 获取全局数据
const app = getApp() const app = getApp()
const globalData = app.globalData || {} const globalData = app.globalData || {}
// 检查登录状态
if (!globalData.judgeId || !globalData.token) {
console.warn('用户未登录,跳转到登录页')
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 1500
})
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/login'
})
}, 1500)
return
}
// 检查是否有选手信息
if (!globalData.currentAthlete || !globalData.currentAthlete.athleteId) {
console.warn('没有选手信息,返回列表页')
uni.showToast({
title: '请选择选手',
icon: 'none',
duration: 1500
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
return
}
// 加载当前选手信息(从 score-list 页面传递)
const currentAthlete = globalData.currentAthlete || {} const currentAthlete = globalData.currentAthlete || {}
this.player = { this.player = {
athleteId: currentAthlete.athleteId || '', athleteId: currentAthlete.athleteId || '',
@@ -160,57 +162,80 @@ export default {
number: currentAthlete.number || '' number: currentAthlete.number || ''
} }
// 如果选手已评分,加载其原有评分
if (currentAthlete.scored && currentAthlete.myScore) { if (currentAthlete.scored && currentAthlete.myScore) {
this.currentScore = currentAthlete.myScore this.currentScore = currentAthlete.myScore
} }
// 加载评委ID和项目ID
this.judgeId = globalData.judgeId this.judgeId = globalData.judgeId
const projects = globalData.projects || [] this.projectId = globalData.currentProjectId || ''
const currentIndex = globalData.currentProjectIndex || 0 this.competitionId = globalData.matchId || globalData.matchCode || ''
const currentProject = projects[currentIndex] || {} this.venueId = globalData.currentVenueId || globalData.venueId || ''
this.projectId = currentProject.projectId
// 调试信息
if (config.debug) { if (config.debug) {
console.log('评分详情页加载:', { console.log('评分详情页加载:', {
athlete: this.player, athlete: this.player,
judgeId: this.judgeId, judgeId: this.judgeId,
projectId: this.projectId, projectId: this.projectId,
competitionId: this.competitionId,
venueId: this.venueId,
initialScore: this.currentScore initialScore: this.currentScore
}) })
} }
// 加载扣分项列表
await this.loadDeductions() await this.loadDeductions()
}, },
methods: { methods: {
onScoreInput(e) {
// Allow typing, validation happens on blur
},
onScoreBlur(e) {
this.validateAndSetScore(e.detail.value)
},
onScoreConfirm(e) {
this.validateAndSetScore(e.detail.value)
},
validateAndSetScore(value) {
const score = parseFloat(value)
if (isNaN(score)) {
uni.showToast({
title: '请输入有效的数字',
icon: 'none'
})
return
}
if (score < this.minScore || score > this.maxScore) {
uni.showToast({
title: '分数必须在' + this.minScore + '-' + this.maxScore + '之间',
icon: 'none'
})
// Reset to valid range
this.currentScore = Math.max(this.minScore, Math.min(this.maxScore, score))
return
}
this.currentScore = parseFloat(score.toFixed(3))
},
async loadDeductions() { async loadDeductions() {
try { try {
// 🔥 关键改动:使用 dataAdapter 获取扣分项列表
// Mock模式调用 mock/score.js 的 getDeductions 函数
// API模式调用 api/score.js 的 getDeductions 函数GET /blade-martial/deductionItem/list
const response = await dataAdapter.getData('getDeductions', { const response = await dataAdapter.getData('getDeductions', {
projectId: this.projectId projectId: this.projectId
}) })
// 获取返回数据(兼容分页和非分页格式) const records = response.data && response.data.records ? response.data.records : []
const responseData = response.data || {} this.deductions = records.map(item => ({
const records = responseData.records || response.data || [] deductionId: item.id,
deductionName: item.itemName,
// 为每个扣分项添加 checked 状态,并映射字段名 deductionScore: parseFloat(item.deductionPoint || 0),
// 后端字段: id, itemName
// 前端字段: deductionId, deductionName
this.deductions = (Array.isArray(records) ? records : []).map(item => ({
deductionId: item.deductionId || item.id,
deductionName: item.deductionName || item.itemName,
deductionPoint: item.deductionPoint || 0,
checked: false checked: false
})) }))
// 调试信息
if (config.debug) { if (config.debug) {
console.log('扣分项加载成功:', this.deductions) console.log('扣分项加载成功:', this.deductions)
} }
@@ -225,7 +250,19 @@ export default {
}, },
goBack() { goBack() {
uni.navigateBack() if (config.debug) {
console.log('返回上一页')
}
uni.navigateBack({
delta: 1,
fail: (err) => {
console.error('返回失败:', err)
uni.redirectTo({
url: '/pages/score-list/score-list'
})
}
})
}, },
decreaseScore() { decreaseScore() {
@@ -240,12 +277,44 @@ export default {
} }
}, },
showScoreInput() {
this.inputScore = this.currentScore.toFixed(3)
this.showInputModal = true
},
hideScoreInput() {
this.showInputModal = false
this.inputScore = ''
},
confirmScoreInput() {
const score = parseFloat(this.inputScore)
if (isNaN(score)) {
uni.showToast({
title: '请输入有效的数字',
icon: 'none'
})
return
}
if (score < this.minScore || score > this.maxScore) {
uni.showToast({
title: `分数必须在${this.minScore}-${this.maxScore}之间`,
icon: 'none'
})
return
}
this.currentScore = parseFloat(score.toFixed(3))
this.hideScoreInput()
},
toggleDeduction(index) { toggleDeduction(index) {
this.deductions[index].checked = !this.deductions[index].checked this.deductions[index].checked = !this.deductions[index].checked
}, },
async handleSubmit() { async handleSubmit() {
// 验证评分范围
if (this.currentScore < this.minScore || this.currentScore > this.maxScore) { if (this.currentScore < this.minScore || this.currentScore > this.maxScore) {
uni.showToast({ uni.showToast({
title: `评分必须在${this.minScore}-${this.maxScore}分之间`, title: `评分必须在${this.minScore}-${this.maxScore}分之间`,
@@ -254,10 +323,25 @@ export default {
return return
} }
// 收集选中的扣分项ID转为数字类型后端期望 List<Long> if (!this.competitionId) {
uni.showToast({
title: '缺少比赛ID请重新登录',
icon: 'none'
})
return
}
if (!this.projectId) {
uni.showToast({
title: '缺少项目ID请返回重新选择',
icon: 'none'
})
return
}
const selectedDeductions = this.deductions const selectedDeductions = this.deductions
.filter(item => item.checked) .filter(item => item.checked)
.map(item => String(item.deductionId)) .map(item => item.deductionId)
try { try {
uni.showLoading({ uni.showLoading({
@@ -265,26 +349,25 @@ export default {
mask: true mask: true
}) })
// 🔥 关键改动:使用 dataAdapter 提交评分 const submitData = {
// Mock模式调用 mock/score.js 的 submitScore 函数 athleteId: this.player.athleteId,
// API模式调用 api/score.js 的 submitScore 函数POST /mini/score/submit judgeId: this.judgeId,
const app = getApp() projectId: this.projectId,
const globalData = app.globalData || {} competitionId: this.competitionId,
const response = await dataAdapter.getData('submitScore', { venueId: this.venueId,
athleteId: String(this.player.athleteId),
judgeId: String(this.judgeId),
score: this.currentScore, score: this.currentScore,
projectId: String(this.projectId),
competitionId: globalData.matchId ? String(globalData.matchId) : null,
venueId: globalData.venueId ? String(globalData.venueId) : null,
scheduleId: globalData.scheduleId ? String(globalData.scheduleId) : null,
deductions: selectedDeductions, deductions: selectedDeductions,
note: this.note || '' note: this.note
}) }
if (config.debug) {
console.log('准备提交评分数据:', submitData)
}
const response = await dataAdapter.getData('submitScore', submitData)
uni.hideLoading() uni.hideLoading()
// 调试信息
if (config.debug) { if (config.debug) {
console.log('评分提交成功:', { console.log('评分提交成功:', {
athleteId: this.player.athleteId, athleteId: this.player.athleteId,
@@ -294,14 +377,12 @@ export default {
}) })
} }
// 显示成功提示
uni.showToast({ uni.showToast({
title: '提交成功', title: '提交成功',
icon: 'success', icon: 'success',
duration: 1500 duration: 1500
}) })
// 返回上一页
setTimeout(() => { setTimeout(() => {
uni.navigateBack() uni.navigateBack()
}, 1500) }, 1500)
@@ -341,12 +422,19 @@ export default {
.nav-left { .nav-left {
position: absolute; position: absolute;
left: 30rpx; left: 0;
width: 60rpx; top: 0;
height: 60rpx; width: 120rpx;
height: 90rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 10;
cursor: pointer;
}
.nav-left:active {
opacity: 0.6;
} }
.back-icon { .back-icon {
@@ -475,12 +563,26 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
padding: 10rpx;
border-radius: 16rpx;
min-width: 240rpx;
} }
.current-score { .score-input-inline {
font-size: 80rpx; width: 200rpx;
height: 100rpx;
font-size: 64rpx;
font-weight: 600; font-weight: 600;
color: #1B7C5E; color: #1B7C5E;
text-align: center;
border: 2rpx solid #E0E0E0;
border-radius: 12rpx;
background-color: #FFFFFF;
}
.score-input-inline:focus {
border-color: #1B7C5E;
} }
.judge-tip { .judge-tip {
@@ -618,4 +720,95 @@ export default {
.submit-btn:active { .submit-btn:active {
opacity: 0.9; opacity: 0.9;
} }
/* 分数输入弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 600rpx;
background-color: #FFFFFF;
border-radius: 24rpx;
overflow: hidden;
}
.modal-header {
padding: 40rpx 30rpx 20rpx;
text-align: center;
}
.modal-title {
font-size: 34rpx;
font-weight: 600;
color: #333333;
}
.modal-body {
padding: 20rpx 30rpx 30rpx;
}
.score-input {
width: 100%;
height: 90rpx;
border: 2rpx solid #E0E0E0;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 36rpx;
text-align: center;
color: #1B7C5E;
font-weight: 600;
}
.score-input:focus {
border-color: #1B7C5E;
}
.input-hint {
display: block;
margin-top: 16rpx;
font-size: 24rpx;
color: #999999;
text-align: center;
}
.modal-footer {
display: flex;
border-top: 1rpx solid #E0E0E0;
}
.modal-btn {
flex: 1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
background: none;
border: none;
border-radius: 0;
}
.modal-btn.cancel {
color: #666666;
border-right: 1rpx solid #E0E0E0;
}
.modal-btn.confirm {
color: #1B7C5E;
font-weight: 600;
}
.modal-btn:active {
background-color: #F5F5F5;
}
</style> </style>

View File

@@ -146,9 +146,9 @@ export default {
return return
} }
// 检查是否是裁判 // 检查是否是裁判
if (globalData.userRole !== 'admin') { if (globalData.userRole !== 'admin') {
console.warn('非裁判用户,跳转到普通评分页') console.warn('非裁判用户,跳转到普通评分页')
uni.reLaunch({ uni.reLaunch({
url: '/pages/score-list/score-list' url: '/pages/score-list/score-list'
}) })
@@ -161,7 +161,7 @@ export default {
time: globalData.matchTime || '' time: globalData.matchTime || ''
} }
// 从 globalData 获取场地信息(与普通裁判相同) // 从 globalData 获取场地信息(与裁判相同)
this.venueInfo = { this.venueInfo = {
id: globalData.venueId, id: globalData.venueId,
name: globalData.venueName || '场地' name: globalData.venueName || '场地'
@@ -175,7 +175,7 @@ export default {
// 调试信息 // 调试信息
if (config.debug) { if (config.debug) {
console.log('裁判列表页加载:', { console.log('裁判列表页加载:', {
userRole: globalData.userRole, userRole: globalData.userRole,
judgeId: this.judgeId, judgeId: this.judgeId,
venueId: this.venueInfo.id, venueId: this.venueInfo.id,
@@ -280,7 +280,7 @@ export default {
console.log('请求选手列表参数:', params) console.log('请求选手列表参数:', params)
} }
// 裁判使用 getAthletesForAdmin 接口 // 裁判使用 getAthletesForAdmin 接口
const response = await dataAdapter.getData('getAthletesForAdmin', params) const response = await dataAdapter.getData('getAthletesForAdmin', params)
if (config.debug) { if (config.debug) {
@@ -300,7 +300,7 @@ export default {
} else { } else {
this.players = [...this.players, ...records] this.players = [...this.players, ...records]
} }
// 裁判视图:统计有总分的选手 // 裁判视图:统计有总分的选手
this.scoredCount = this.players.filter(p => p.totalScore).length this.scoredCount = this.players.filter(p => p.totalScore).length
this.hasMore = this.players.length < total this.hasMore = this.players.length < total
@@ -330,6 +330,7 @@ export default {
goToModify(player) { goToModify(player) {
const app = getApp() const app = getApp()
app.globalData.currentAthlete = player app.globalData.currentAthlete = player
app.globalData.currentProjectId = this.projectInfo.id
uni.navigateTo({ url: '/pages/modify-score/modify-score' }) uni.navigateTo({ url: '/pages/modify-score/modify-score' })
}, },

View File

@@ -4,6 +4,7 @@
<view class="nav-bar"> <view class="nav-bar">
<view class="nav-title">评分系统</view> <view class="nav-title">评分系统</view>
<view class="nav-right"> <view class="nav-right">
<view class="logout-btn" @click="handleLogout">退出</view>
<view class="nav-dots">···</view> <view class="nav-dots">···</view>
<view class="nav-circle"></view> <view class="nav-circle"></view>
</view> </view>
@@ -23,9 +24,9 @@
<view class="refresh-link" @click="handleRefresh">刷新</view> <view class="refresh-link" @click="handleRefresh">刷新</view>
</view> </view>
<!-- 项目筛选 --> <!-- 项目筛选 - 横向滑动 -->
<view class="project-row"> <scroll-view class="project-scroll" scroll-x="true" :show-scrollbar="false">
<view class="project-grid"> <view class="project-row">
<view <view
class="project-chip" class="project-chip"
:class="{ active: index === currentProjectIndex }" :class="{ active: index === currentProjectIndex }"
@@ -36,7 +37,8 @@
{{ project.projectName }} {{ project.projectName }}
</view> </view>
</view> </view>
</view> </scroll-view>
<view class="no-project-tip" v-if="projects.length === 0">当前场地暂无比赛项目</view>
</view> </view>
<!-- 评分统计 --> <!-- 评分统计 -->
@@ -46,7 +48,7 @@
</view> </view>
<!-- 选手列表 --> <!-- 选手列表 -->
<view class="player-list"> <view class="player-list" v-if="projects.length > 0">
<!-- 选手卡片 --> <!-- 选手卡片 -->
<view <view
class="player-card" class="player-card"
@@ -193,7 +195,6 @@ export default {
}, },
async onShow() { async onShow() {
// 从评分详情页返回时刷新数据
if (!this.isFirstLoad) { if (!this.isFirstLoad) {
if (config.debug) { if (config.debug) {
console.log('页面显示,刷新数据') console.log('页面显示,刷新数据')
@@ -232,34 +233,23 @@ export default {
}, },
formatScore(score) { formatScore(score) {
// 处理 null、undefined、-1 等无效值
if (score === null || score === undefined || score === -1 || score === '-1') { if (score === null || score === undefined || score === -1 || score === '-1') {
return '--' return '--'
} }
// 如果是字符串类型的数字,直接返回
if (typeof score === 'string' && !isNaN(parseFloat(score))) { if (typeof score === 'string' && !isNaN(parseFloat(score))) {
return score return score
} }
// 如果是数字类型保留3位小数
if (typeof score === 'number') { if (typeof score === 'number') {
return score.toFixed(3) return score.toFixed(3)
} }
return score return score
}, },
/**
* 计算选手总分
* 规则:所有裁判评分完成后,去掉一个最高分和一个最低分,取剩余分数的平均值
* @param {Object} player - 选手对象
* @returns {Number|null} 计算后的总分,如果未完成评分返回 null
*/
calculateTotalScore(player) { calculateTotalScore(player) {
// 检查是否有裁判评分数据
if (!player.judgeScores || !Array.isArray(player.judgeScores)) { if (!player.judgeScores || !Array.isArray(player.judgeScores)) {
return null return null
} }
// 检查是否所有裁判都已评分
const totalJudges = player.totalJudges || 0 const totalJudges = player.totalJudges || 0
const scoredCount = player.judgeScores.length const scoredCount = player.judgeScores.length
@@ -267,34 +257,22 @@ export default {
return null return null
} }
// 提取所有分数
const scores = player.judgeScores.map(j => parseFloat(j.score)).filter(s => !isNaN(s)) const scores = player.judgeScores.map(j => parseFloat(j.score)).filter(s => !isNaN(s))
if (scores.length < 3) { if (scores.length < 3) {
// 少于3个评分无法去掉最高最低直接取平均
if (scores.length === 0) return null if (scores.length === 0) return null
const sum = scores.reduce((a, b) => a + b, 0) const sum = scores.reduce((a, b) => a + b, 0)
return sum / scores.length return sum / scores.length
} }
// 排序
scores.sort((a, b) => a - b) scores.sort((a, b) => a - b)
// 去掉最高分和最低分
const trimmedScores = scores.slice(1, -1) const trimmedScores = scores.slice(1, -1)
// 计算平均分
const sum = trimmedScores.reduce((a, b) => a + b, 0) const sum = trimmedScores.reduce((a, b) => a + b, 0)
const average = sum / trimmedScores.length const average = sum / trimmedScores.length
return average return average
}, },
/**
* 检查选手是否所有裁判都已评分
* @param {Object} player - 选手对象
* @returns {Boolean}
*/
isAllJudgesScored(player) { isAllJudgesScored(player) {
if (!player.judgeScores || !Array.isArray(player.judgeScores)) { if (!player.judgeScores || !Array.isArray(player.judgeScores)) {
return false return false
@@ -303,11 +281,6 @@ export default {
return totalJudges > 0 && player.judgeScores.length >= totalJudges return totalJudges > 0 && player.judgeScores.length >= totalJudges
}, },
/**
* 获取选手的显示总分
* @param {Object} player - 选手对象
* @returns {String} 格式化后的总分或 '--'
*/
getDisplayTotalScore(player) { getDisplayTotalScore(player) {
const score = this.calculateTotalScore(player) const score = this.calculateTotalScore(player)
if (score === null) { if (score === null) {
@@ -316,17 +289,26 @@ export default {
return score.toFixed(3) return score.toFixed(3)
}, },
/**
* 获取裁判评分进度
* @param {Object} player - 选手对象
* @returns {String} 进度字符串,如 "3/6"
*/
getJudgeProgress(player) { getJudgeProgress(player) {
const scored = player.judgeScores ? player.judgeScores.length : 0 const scored = player.judgeScores ? player.judgeScores.length : 0
const total = player.totalJudges || '?' const total = player.totalJudges || '?'
return scored + '/' + total return scored + '/' + total
}, },
handleLogout() {
uni.showModal({
title: "提示",
content: "确定要退出登录吗?",
success: (res) => {
if (res.confirm) {
uni.removeStorageSync("judgeInfo")
uni.removeStorageSync("token")
uni.reLaunch({ url: "/pages/login/login" })
}
}
})
},
async handleRefresh() { async handleRefresh() {
if (this.isLoading) return if (this.isLoading) return
uni.showToast({ title: '刷新中...', icon: 'loading', duration: 1000 }) uni.showToast({ title: '刷新中...', icon: 'loading', duration: 1000 })
@@ -349,6 +331,7 @@ export default {
const globalData = app.globalData || {} const globalData = app.globalData || {}
const params = { const params = {
matchCode: globalData.matchCode, matchCode: globalData.matchCode,
competitionId: globalData.matchId,
judgeId: this.judgeId, judgeId: this.judgeId,
venueId: this.venueInfo.id, venueId: this.venueInfo.id,
projectId: this.projectInfo.id, projectId: this.projectInfo.id,
@@ -413,6 +396,7 @@ export default {
goToScoreDetail(player) { goToScoreDetail(player) {
const app = getApp() const app = getApp()
app.globalData.currentAthlete = player app.globalData.currentAthlete = player
app.globalData.currentProjectId = this.projectInfo.id
uni.navigateTo({ url: '/pages/score-detail/score-detail' }) uni.navigateTo({ url: '/pages/score-detail/score-detail' })
}, },
@@ -468,6 +452,15 @@ export default {
gap: 20rpx; gap: 20rpx;
} }
.logout-btn {
font-size: 26rpx;
color: #FFFFFF;
background: rgba(255, 255, 255, 0.2);
padding: 8rpx 20rpx;
border-radius: 20rpx;
margin-right: 10rpx;
}
.nav-dots, .nav-circle { .nav-dots, .nav-circle {
font-size: 32rpx; font-size: 32rpx;
color: #FFFFFF; color: #FFFFFF;
@@ -521,28 +514,30 @@ export default {
color: #4A90D9; color: #4A90D9;
} }
.project-row { /* ==================== 项目筛选 - 横向滑动 ==================== */
display: flex; .project-scroll {
flex-direction: column; width: 100%;
white-space: nowrap;
} }
.project-grid { .project-row {
display: grid; display: inline-flex;
grid-template-columns: repeat(3, 1fr); flex-direction: row;
gap: 16rpx; gap: 16rpx;
padding: 4rpx 0;
} }
.project-chip { .project-chip {
padding: 20rpx 12rpx; display: inline-block;
padding: 20rpx 32rpx;
border: 2rpx solid #1B7C5E; border: 2rpx solid #1B7C5E;
border-radius: 8rpx; border-radius: 8rpx;
font-size: 26rpx; font-size: 26rpx;
color: #1B7C5E; color: #1B7C5E;
background-color: #FFFFFF; background-color: #FFFFFF;
text-align: center; text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0;
} }
.project-chip.active { .project-chip.active {
@@ -692,4 +687,12 @@ export default {
font-size: 28rpx; font-size: 28rpx;
color: #999999; color: #999999;
} }
.no-project-tip {
padding: 30rpx;
text-align: center;
color: #999;
font-size: 28rpx;
width: 100%;
}
</style> </style>

View File

@@ -247,7 +247,7 @@ export default new DataAdapter()
* |---------------------|----------------------|---------------------|---------------| * |---------------------|----------------------|---------------------|---------------|
* | login | mockData.login | apiService.login | 登录验证 | * | login | mockData.login | apiService.login | 登录验证 |
* | getMyAthletes | mockData.getMyAthletes | apiService.getMyAthletes | 选手列表(评委) | * | getMyAthletes | mockData.getMyAthletes | apiService.getMyAthletes | 选手列表(评委) |
* | getAthletesForAdmin | mockData.getAthletesForAdmin | apiService.getAthletesForAdmin | 选手列表(裁判) | * | getAthletesForAdmin | mockData.getAthletesForAdmin | apiService.getAthletesForAdmin | 选手列表(裁判) |
* | submitScore | mockData.submitScore | apiService.submitScore | 提交评分 | * | submitScore | mockData.submitScore | apiService.submitScore | 提交评分 |
* | getScoreDetail | mockData.getScoreDetail | apiService.getScoreDetail | 评分详情 | * | getScoreDetail | mockData.getScoreDetail | apiService.getScoreDetail | 评分详情 |
* | modifyScore | mockData.modifyScore | apiService.modifyScore | 修改评分 | * | modifyScore | mockData.modifyScore | apiService.modifyScore | 修改评分 |

View File

@@ -247,7 +247,7 @@ export default new DataAdapter()
* |---------------------|----------------------|---------------------|---------------| * |---------------------|----------------------|---------------------|---------------|
* | login | mockData.login | apiService.login | 登录验证 | * | login | mockData.login | apiService.login | 登录验证 |
* | getMyAthletes | mockData.getMyAthletes | apiService.getMyAthletes | 选手列表(评委) | * | getMyAthletes | mockData.getMyAthletes | apiService.getMyAthletes | 选手列表(评委) |
* | getAthletesForAdmin | mockData.getAthletesForAdmin | apiService.getAthletesForAdmin | 选手列表(裁判) | * | getAthletesForAdmin | mockData.getAthletesForAdmin | apiService.getAthletesForAdmin | 选手列表(裁判) |
* | submitScore | mockData.submitScore | apiService.submitScore | 提交评分 | * | submitScore | mockData.submitScore | apiService.submitScore | 提交评分 |
* | getScoreDetail | mockData.getScoreDetail | apiService.getScoreDetail | 评分详情 | * | getScoreDetail | mockData.getScoreDetail | apiService.getScoreDetail | 评分详情 |
* | modifyScore | mockData.modifyScore | apiService.modifyScore | 修改评分 | * | modifyScore | mockData.modifyScore | apiService.modifyScore | 修改评分 |

View File

@@ -1,27 +1,17 @@
module.exports = { module.exports = {
// 输出目录
outputDir: 'dist/build/h5', outputDir: 'dist/build/h5',
// 静态资源目录
assetsDir: 'static', assetsDir: 'static',
// 公共路径 - 重要!确保静态资源能正确加载
publicPath: process.env.NODE_ENV === 'production' ? './' : '/', publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
// 生产环境配置
productionSourceMap: false, productionSourceMap: false,
// CSS 提取配置
css: { css: {
extract: true, extract: true,
sourceMap: false sourceMap: false
}, },
// 开发服务器配置
devServer: { devServer: {
port: 8080, port: 8080,
host: '0.0.0.0', host: '0.0.0.0',
open: true, open: false,
disableHostCheck: true,
overlay: { overlay: {
warnings: false, warnings: false,
errors: true errors: true
@@ -37,14 +27,10 @@ module.exports = {
} }
} }
}, },
chainWebpack: config => { chainWebpack: config => {
// 禁用 gzip 大小报告
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
config.performance.hints(false) config.performance.hints(false)
} }
// 确保 CSS 文件正确处理
config.module config.module
.rule('vue') .rule('vue')
.use('vue-loader') .use('vue-loader')