17 Commits

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
DevOps
88a931976d fix: 裁判长修改评分时传递venueId参数
配合后端修复,确保修改评分记录包含正确的场地ID
2025-12-25 10:55:22 +08:00
DevOps
96bc2d92a2 Fix: 完善裁判评分列表功能
1. 修复分页问题:添加 size=200 参数确保获取所有选手
2. 裁判长页面:使用 scoringComplete 判断评分完成状态
3. 普通裁判页面:已评分选手显示分数和修改按钮
4. 修复 getAthletesForAdmin 调用正确的接口路径

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 11:18:33 +08:00
DevOps
a9c5c4a904 Feat: 裁判长页面显示所有选手(含未完成评分)
修改裁判长页面选手列表显示逻辑:
- 显示所有选手,不再仅显示已完成评分的选手
- 已完成评分的选手:显示总分 + 修改按钮
- 未完成评分的选手:显示评分中...提示,不显示修改按钮
- 新增 .scoring-status 样式,使用橙色背景突出显示评分中状态

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 10:19:57 +08:00
DevOps
5349b80cf8 Fix: iOS Safari 双击缩放问题 - UniApp H5 专用解决方案
问题描述:
- 用户在 iOS Safari 上快速点击加分/减分按钮时触发页面缩放
- 影响用户体验,导致操作困难

解决方案:
1. 全局事件拦截(index.html)
   - 拦截 touchstart/touchend 事件,检测快速连续触摸(<350ms)
   - 完全禁用 dblclick 和 gesture 事件
   - 使用 MutationObserver 动态监听 DOM 变化
   - 添加 CSS 强制禁用缩放

2. 组件级优化(modify-score.vue)
   - 使用 touchstart/touchend 替代 click 事件
   - 添加 300ms 防抖机制,忽略快速连续触摸
   - 实现长按连续加减分功能(500ms 后每 100ms 触发一次)
   - H5 平台条件编译,添加原生事件监听器
   - 清理定时器,防止内存泄漏

3. UniApp 特性应用
   - 使用条件编译 #ifdef H5 针对 H5 平台特殊处理
   - 利用 $nextTick 确保 DOM 渲染完成后添加事件监听
   - 保持跨平台兼容性(小程序、App 不受影响)

技术要点:
- touch-action: none 禁用触摸动作
- event.preventDefault() 阻止默认行为
- capture: true 在捕获阶段拦截事件
- passive: false 允许调用 preventDefault()

测试建议:
- 在 iOS Safari 上快速点击按钮,验证不再缩放
- 测试长按功能是否正常工作
- 验证其他平台(微信小程序、Android)不受影响

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 01:24:36 +08:00
DevOps
56c1320e40 Fix iOS Safari double-tap zoom issue with comprehensive solution
Implemented multiple layers of protection to prevent iOS Safari from zooming when users quickly tap the score adjustment buttons:

1. Enhanced touch event handling in modify-score.vue:
   - Changed from touchend to touchstart for immediate response
   - Added .stop.prevent modifiers to all touch events (touchstart, touchmove, touchend, touchcancel)
   - Added noop() handlers to absorb unwanted events
   - Replaced time-based debouncing with isProcessing flag using requestAnimationFrame
   - Ensured all child elements have pointer-events: none

2. Comprehensive index.html protection:
   - Added iOS-specific meta tags (apple-mobile-web-app-capable, format-detection)
   - Enhanced CSS with touch-action: pan-y for scrolling while preventing zoom
   - Implemented 7-layer JavaScript protection:
     * Layer 1: Intercept rapid touchstart events with counter
     * Layer 2: Block touchend events within 300ms
     * Layer 3: Completely disable dblclick events
     * Layer 4: Prevent gesture events (gesturestart/change/end)
     * Layer 5: Use Pointer Events API for additional blocking
     * Layer 6: Filter rapid click events
     * Layer 7: Add capture-phase listeners to buttons
   - All event listeners use { passive: false, capture: true } for maximum control

This multi-layered approach addresses the root cause: iOS Safari triggers zoom at the browser level before JavaScript can normally intercept it. By using capture phase and preventing events at multiple stages, we ensure the zoom behavior is blocked.

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 01:13:55 +08:00
DevOps
c5c31e8088 Fix iOS Safari double-tap zoom issue on score modification buttons
Problem:
- Rapid tapping on +0.001/-0.001 buttons triggered page zoom on iOS Safari
- Previous solutions (viewport meta, touch-action: manipulation) were ineffective

Solution implemented:
1. Enhanced global touch event handling in index.html:
   - Added comprehensive gesture event prevention (gesturestart/change/end)
   - Improved touchend debouncing with stopPropagation
   - Added specific CSS rules for button elements with touch-action: none

2. Modified button interaction in modify-score.vue:
   - Replaced @click events with @touchstart/@touchend handlers
   - Added preventDefault and stopPropagation on touch events
   - Implemented 100ms debounce to prevent rapid successive touches
   - Added pointer-events: none to child elements to ensure touch targets
   - Changed touch-action from 'manipulation' to 'none' for complete control

Technical details:
- touch-action: none completely disables browser touch gestures
- Event handlers use { passive: false } to allow preventDefault
- Debounce mechanism prevents accidental double-triggers
- Child elements have pointer-events: none to ensure parent handles all touches

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 01:01:54 +08:00
33 changed files with 1703 additions and 644 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
- **前端**: Vue.js 2.x
- **样式**: CSS3 (支持rpx单位)
- **平台支持**: H5、微信小程序
- **平台**: H5、微信小程序
- **样式**: SCSS
## 目录结构
## 快速开始
```
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
# 在项目根目录执行
# 安装依赖
npm install
```
#### 问题3小程序无法运行
**解决方案:**
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
# H5 开发
npm run dev:h5
```
**微信小程序开发**
```bash
# 微信小程序开发
npm run dev:mp-weixin
```
**构建H5**
```bash
# 构建 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% ✅
后端开发: ████████░░░░░░░░░░░░ 44% ⚠️
文档完成: ████████████████████ 100% ✅
martial-admin-mini/
├── pages/
│ ├── 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. ✅ 适配了主流手机屏幕尺寸
---
## 许可证
MIT License
## 联系方式
如有问题或建议,请联系项目负责人。
**最后更新**: 2024-12-29

View File

@@ -9,13 +9,13 @@ import request from '@/utils/request.js'
* 获取选手列表(根据裁判类型返回不同数据)
* @param {Object} params
* @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.projectId - 项目ID可选
* @returns {Promise}
*
* 普通裁判:返回待评分的选手列表
* 裁判:返回已有评分的选手列表
* 裁判:返回待评分的选手列表
* 裁判:返回已有评分的选手列表
*
* 后端路径: GET /api/mini/score/athletes
*/
@@ -23,27 +23,40 @@ export function getMyAthletes(params) {
return request({
url: '/mini/score/athletes',
method: 'GET',
params: params, // GET 请求使用 params
params: {
...params,
size: 200 // 确保获取所有选手
},
showLoading: true
})
}
/**
* 获取选手列表(裁判
* 获取选手列表(裁判)
* @param {Object} params
* @param {String} params.competitionId - 比赛ID
* @param {String} params.venueId - 场地ID
* @param {String} params.projectId - 项目ID
* @returns {Promise}
*
* 注意:此接口需要后端实现
* 建议路径: GET /api/mini/athletes/admin
* 实际调用 /mini/score/athletes 接口,传递 refereeType=1
*/
export function getAthletesForAdmin(params) {
// 从 globalData 获取 judgeId
const app = getApp()
const globalData = app.globalData || {}
const judgeId = globalData.judgeId
return request({
url: '/mini/athletes/admin',
url: '/mini/score/athletes',
method: 'GET',
params: params, // GET 请求使用 params
params: {
judgeId: judgeId,
refereeType: 1, // 主裁判
venueId: params.venueId,
projectId: params.projectId,
size: 200 // 确保获取所有选手
},
showLoading: true
})
}
@@ -90,55 +103,3 @@ export default {
getVenues,
getProjects
}
/**
* 后端接口规范:
*
* GET /api/mini/score/athletes
*
* 请求参数:
* {
* "judgeId": "456",
* "refereeType": 2, // 1-裁判长, 2-普通裁判
* "venueId": "1", // 可选
* "projectId": "5" // 可选
* }
*
* 响应(普通裁判 - 待评分选手):
* {
* "code": 200,
* "success": true,
* "msg": "操作成功",
* "data": [
* {
* "athleteId": 1,
* "name": "张三",
* "number": "123-4567898275",
* "team": "少林寺武术大学院",
* "projectName": "女子组长拳",
* "orderNum": 1,
* "competitionStatus": 0
* }
* ]
* }
*
* 响应(裁判长 - 已有评分选手):
* {
* "code": 200,
* "success": true,
* "msg": "操作成功",
* "data": [
* {
* "athleteId": 1,
* "name": "张三",
* "number": "123-4567898275",
* "team": "少林寺武术大学院",
* "projectName": "女子组长拳",
* "orderNum": 1,
* "totalScore": 8.907,
* "scoredJudgeCount": 3,
* "competitionStatus": 2
* }
* ]
* }
*/

View File

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

View File

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

View File

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

View File

@@ -3,32 +3,229 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 关键:使用最严格的 viewport 设置 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no">
<title>武术评分系统</title>
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
<style>
* {
touch-action: manipulation;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
/* 允许垂直滚动,但禁用其他触摸动作 */
touch-action: pan-y !important;
-webkit-touch-callout: none !important;
-webkit-tap-highlight-color: transparent !important;
-webkit-user-select: none !important;
user-select: none !important;
}
/* 针对按钮元素完全禁用所有触摸动作 */
button,
.control-btn,
[class*="btn"],
[class*="control"],
.decrease,
.increase {
touch-action: none !important;
-webkit-user-select: none !important;
user-select: none !important;
-webkit-touch-callout: none !important;
pointer-events: auto !important;
}
/* 允许输入框正常交互 */
input, textarea {
touch-action: manipulation !important;
-webkit-user-select: text !important;
user-select: text !important;
}
/* 防止页面整体缩放 */
html, body {
touch-action: pan-y !important;
-ms-touch-action: pan-y !important;
}
</style>
<script>
// 禁用 iOS Safari 双击缩放
// UniApp H5 专用:iOS Safari 双击缩放终极解决方案
(function() {
'use strict';
var lastTouchEnd = 0;
var touchStartTime = 0;
var touchCount = 0;
var resetTimer = null;
var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
console.log('iOS 检测:', isIOS);
console.log('User Agent:', navigator.userAgent);
// 方案1: 全局拦截 touchstart - 最高优先级
document.addEventListener('touchstart', function(event) {
var now = Date.now();
touchStartTime = now;
// 清除重置计时器
if (resetTimer) {
clearTimeout(resetTimer);
}
// 检查是否是快速连续触摸
var timeSinceLastTouch = now - lastTouchEnd;
if (timeSinceLastTouch < 350) {
touchCount++;
// 如果是第二次或更多次快速触摸,立即阻止
if (touchCount >= 1) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
console.log('阻止快速连续触摸', touchCount, timeSinceLastTouch);
return false;
}
} else {
touchCount = 0;
}
// 600ms 后重置计数器
resetTimer = setTimeout(function() {
touchCount = 0;
}, 600);
}, { passive: false, capture: true });
// 方案2: 全局拦截 touchend
document.addEventListener('touchend', function(event) {
var now = Date.now();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, { passive: false });
var touchDuration = now - touchStartTime;
var timeSinceLastTouch = now - lastTouchEnd;
// 禁用双击缩放
// 如果触摸时间很短(<150ms)且距离上次触摸很近(<350ms),很可能是双击
if (touchDuration < 150 && timeSinceLastTouch < 350 && timeSinceLastTouch > 0) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
console.log('阻止疑似双击', touchDuration, timeSinceLastTouch);
return false;
}
lastTouchEnd = now;
}, { passive: false, capture: true });
// 方案3: 完全禁用 dblclick 事件
document.addEventListener('dblclick', function(event) {
event.preventDefault();
}, { passive: false });
event.stopPropagation();
event.stopImmediatePropagation();
console.log('阻止 dblclick 事件');
return false;
}, { passive: false, capture: true });
// 方案4: 禁用手势缩放
document.addEventListener('gesturestart', function(event) {
event.preventDefault();
event.stopPropagation();
console.log('阻止 gesturestart');
}, { passive: false, capture: true });
document.addEventListener('gesturechange', function(event) {
event.preventDefault();
event.stopPropagation();
}, { passive: false, capture: true });
document.addEventListener('gestureend', function(event) {
event.preventDefault();
event.stopPropagation();
}, { passive: false, capture: true });
// 方案5: 监听 click 事件,过滤快速连续点击
var lastClickTime = 0;
document.addEventListener('click', function(event) {
var now = Date.now();
var timeSinceLastClick = now - lastClickTime;
// 如果距离上次点击小于350ms阻止
if (timeSinceLastClick < 350 && timeSinceLastClick > 0) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
console.log('阻止快速连续点击', timeSinceLastClick);
return false;
}
lastClickTime = now;
}, { passive: false, capture: true });
// 方案6: 针对按钮元素的特殊处理
function addButtonProtection() {
var selectors = [
'.control-btn',
'.control-btn.decrease',
'.control-btn.increase',
'button',
'[class*="btn"]'
];
selectors.forEach(function(selector) {
var elements = document.querySelectorAll(selector);
elements.forEach(function(element) {
// 移除所有现有的事件监听器(通过克隆节点)
var newElement = element.cloneNode(true);
element.parentNode.replaceChild(newElement, element);
// 添加新的保护性事件监听器
['touchstart', 'touchend', 'touchmove', 'click', 'dblclick'].forEach(function(eventType) {
newElement.addEventListener(eventType, function(e) {
if (eventType === 'dblclick') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return false;
}
}, { passive: false, capture: true });
});
});
});
}
// DOM 加载完成后添加按钮保护
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(addButtonProtection, 100);
// 使用 MutationObserver 监听 DOM 变化
var observer = new MutationObserver(function(mutations) {
addButtonProtection();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
} else {
setTimeout(addButtonProtection, 100);
}
// 方案7: 使用 CSS 强制禁用缩放
var style = document.createElement('style');
style.innerHTML = `
* {
touch-action: pan-y !important;
}
.control-btn,
.control-btn *,
button,
button * {
touch-action: none !important;
-webkit-user-select: none !important;
user-select: none !important;
-webkit-touch-callout: none !important;
}
`;
document.head.appendChild(style);
console.log('iOS Safari 双击缩放防护已启用 - UniApp H5 专用版本');
})();
</script>
</head>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,13 @@
</view>
<view class="score-control">
<view class="control-btn decrease" @touchstart.prevent="decreaseScore" @click.prevent="decreaseScore">
<!-- 减分按钮 - 使用 catchtouchstart 阻止事件冒泡 -->
<view
class="control-btn decrease"
@touchstart="onDecreaseStart"
@touchend="onDecreaseEnd"
@touchcancel="onTouchCancel"
>
<text class="btn-symbol"></text>
<text class="btn-value">-0.001</text>
</view>
@@ -60,15 +66,17 @@
<text class="no-modify-text">可不改</text>
</view>
<view class="control-btn increase" @touchstart.prevent="increaseScore" @click.prevent="increaseScore">
<!-- 加分按钮 - 使用 catchtouchstart 阻止事件冒泡 -->
<view
class="control-btn increase"
@touchstart="onIncreaseStart"
@touchend="onIncreaseEnd"
@touchcancel="onTouchCancel"
>
<text class="btn-symbol"></text>
<text class="btn-value">+0.001</text>
</view>
</view>
<!-- <view class="modify-tip">
裁判长修改保留3位小数点超过上限或下限时按钮置灰
</view> -->
</view>
<!-- 备注 -->
@@ -114,7 +122,15 @@ export default {
originalScore: 8.000,
note: '',
minScore: 5.0,
maxScore: 10.0
maxScore: 10.0,
// 防止双击的状态管理
isTouching: false,
touchTimer: null,
lastTouchTime: 0,
// 长按相关
longPressTimer: null,
longPressInterval: null,
isLongPressing: false
}
},
@@ -124,9 +140,9 @@ export default {
const globalData = app.globalData || {}
// 获取当前选手信息(从 score-list-multi 页面传递)
const currentAthlete = globalData.currentAthlete || {}
const currentAthlete = globalData.currentAthlete ||
// 获取裁判ID
// 获取裁判ID
this.modifierId = globalData.judgeId
// 调试信息
@@ -141,9 +157,151 @@ export default {
if (currentAthlete.athleteId) {
await this.loadScoreDetail(currentAthlete.athleteId)
}
// H5 平台特殊处理:禁用双击缩放
// #ifdef H5
this.disableDoubleTapZoom()
// #endif
},
onUnload() {
// 清理定时器
this.clearAllTimers()
},
methods: {
// #ifdef H5
disableDoubleTapZoom() {
// 在 H5 环境下,添加额外的事件监听来防止双击缩放
this.$nextTick(() => {
const decreaseBtn = document.querySelector('.control-btn.decrease')
const increaseBtn = document.querySelector('.control-btn.increase')
const preventZoom = (e) => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
return false
}
if (decreaseBtn) {
decreaseBtn.addEventListener('touchstart', preventZoom, { passive: false, capture: true })
decreaseBtn.addEventListener('touchend', preventZoom, { passive: false, capture: true })
decreaseBtn.addEventListener('touchmove', preventZoom, { passive: false, capture: true })
decreaseBtn.addEventListener('click', preventZoom, { passive: false, capture: true })
}
if (increaseBtn) {
increaseBtn.addEventListener('touchstart', preventZoom, { passive: false, capture: true })
increaseBtn.addEventListener('touchend', preventZoom, { passive: false, capture: true })
increaseBtn.addEventListener('touchmove', preventZoom, { passive: false, capture: true })
increaseBtn.addEventListener('click', preventZoom, { passive: false, capture: true })
}
})
},
// #endif
clearAllTimers() {
if (this.touchTimer) {
clearTimeout(this.touchTimer)
this.touchTimer = null
}
if (this.longPressTimer) {
clearTimeout(this.longPressTimer)
this.longPressTimer = null
}
if (this.longPressInterval) {
clearInterval(this.longPressInterval)
this.longPressInterval = null
}
},
// 减分按钮 - touchstart
onDecreaseStart(e) {
e.preventDefault()
e.stopPropagation()
const now = Date.now()
// 防止快速连续触摸300ms内的触摸被忽略
if (now - this.lastTouchTime < 300) {
return
}
this.lastTouchTime = now
this.isTouching = true
// 立即执行一次减分
this.decreaseScore()
// 设置长按定时器500ms后开始连续减分
this.longPressTimer = setTimeout(() => {
this.isLongPressing = true
// 每100ms执行一次减分
this.longPressInterval = setInterval(() => {
this.decreaseScore()
}, 100)
}, 500)
},
// 减分按钮 - touchend
onDecreaseEnd(e) {
e.preventDefault()
e.stopPropagation()
this.isTouching = false
this.isLongPressing = false
this.clearAllTimers()
},
// 加分按钮 - touchstart
onIncreaseStart(e) {
e.preventDefault()
e.stopPropagation()
const now = Date.now()
// 防止快速连续触摸300ms内的触摸被忽略
if (now - this.lastTouchTime < 300) {
return
}
this.lastTouchTime = now
this.isTouching = true
// 立即执行一次加分
this.increaseScore()
// 设置长按定时器500ms后开始连续加分
this.longPressTimer = setTimeout(() => {
this.isLongPressing = true
// 每100ms执行一次加分
this.longPressInterval = setInterval(() => {
this.increaseScore()
}, 100)
}, 500)
},
// 加分按钮 - touchend
onIncreaseEnd(e) {
e.preventDefault()
e.stopPropagation()
this.isTouching = false
this.isLongPressing = false
this.clearAllTimers()
},
// 触摸取消
onTouchCancel(e) {
e.preventDefault()
e.stopPropagation()
this.isTouching = false
this.isLongPressing = false
this.clearAllTimers()
},
async loadScoreDetail(athleteId) {
try {
uni.showLoading({
@@ -151,9 +309,6 @@ export default {
mask: true
})
// 🔥 关键改动:使用 dataAdapter 获取评分详情
// Mock模式调用 mock/score.js 的 getScoreDetail 函数
// API模式调用 api/score.js 的 getScoreDetail 函数GET /api/mini/score/detail/{athleteId}
const response = await dataAdapter.getData('getScoreDetail', {
athleteId: athleteId
})
@@ -202,12 +357,26 @@ export default {
decreaseScore() {
if (this.currentScore > this.minScore) {
this.currentScore = parseFloat((this.currentScore - 0.001).toFixed(3))
// 添加触觉反馈(仅在支持的平台)
// #ifndef H5
uni.vibrateShort({
type: 'light'
})
// #endif
}
},
increaseScore() {
if (this.currentScore < this.maxScore) {
this.currentScore = parseFloat((this.currentScore + 0.001).toFixed(3))
// 添加触觉反馈(仅在支持的平台)
// #ifndef H5
uni.vibrateShort({
type: 'light'
})
// #endif
}
},
@@ -236,14 +405,16 @@ export default {
mask: true
})
// 🔥 关键改动:使用 dataAdapter 修改评分
// Mock模式调用 mock/score.js 的 modifyScore 函数
// API模式调用 api/score.js 的 modifyScore 函数PUT /api/mini/score/modify
// 获取场地ID
const app = getApp()
const venueId = app.globalData?.currentVenueId
const response = await dataAdapter.getData('modifyScore', {
athleteId: this.athleteInfo.athleteId,
modifierId: this.modifierId,
modifiedScore: this.currentScore,
note: this.note
note: this.note,
venueId: venueId // 添加场地ID
})
uni.hideLoading()
@@ -459,18 +630,22 @@ export default {
}
.control-btn {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
user-select: none;
-webkit-user-select: none;
width: 140rpx;
height: 140rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #F5F5F5;
border-radius: 12rpx;
cursor: pointer;
position: relative;
/* 关键:禁用所有可能导致缩放的触摸行为 */
touch-action: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
.control-btn.decrease {
@@ -484,6 +659,7 @@ export default {
.btn-symbol {
font-size: 48rpx;
font-weight: 300;
pointer-events: none;
}
.control-btn.decrease .btn-symbol {
@@ -497,6 +673,7 @@ export default {
.btn-value {
font-size: 24rpx;
margin-top: 8rpx;
pointer-events: none;
}
.control-btn.decrease .btn-value {
@@ -525,13 +702,6 @@ export default {
margin-top: 8rpx;
}
.modify-tip {
font-size: 24rpx;
color: #FF4D6A;
line-height: 1.6;
text-align: center;
}
/* 备注 */
.note-section {
margin: 30rpx;

View File

@@ -31,28 +31,22 @@
<view class="score-control">
<view class="control-btn decrease" @click="decreaseScore">
<text class="btn-symbol"></text>
<!-- <text class="btn-value">-0.001</text> -->
</view>
<view class="score-display">
<view class="score-display" @click="showScoreInput">
<text class="current-score">{{ currentScore.toFixed(3) }}</text>
<text class="edit-hint">点击编辑</text>
</view>
<view class="control-btn increase" @click="increaseScore">
<text class="btn-symbol"></text>
<!-- <text class="btn-value">+0.001</text> -->
</view>
</view>
<!-- <view class="judge-tip">
裁判评分保留3位小数点超过上限或下限时按钮置灰
</view> -->
<!-- 扣分项 -->
<view class="deduction-section">
<view class="deduction-header">
<text class="deduction-label">扣分项</text>
<!-- <text class="deduction-hint">扣分项多选</text> -->
</view>
<view class="deduction-list">
@@ -82,12 +76,35 @@
v-model="note"
maxlength="200"
/>
<!-- <text class="optional-text">可不填</text> -->
</view>
</view>
<!-- 提交按钮 -->
<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>
</template>
@@ -113,16 +130,16 @@ export default {
note: '',
minScore: 5.0,
maxScore: 10.0,
deductions: []
deductions: [],
showInputModal: false,
inputScore: ''
}
},
async onLoad() {
// 获取全局数据
const app = getApp()
const globalData = app.globalData || {}
// 加载当前选手信息(从 score-list 页面传递)
const currentAthlete = globalData.currentAthlete || {}
this.player = {
athleteId: currentAthlete.athleteId || '',
@@ -132,18 +149,15 @@ export default {
number: currentAthlete.number || ''
}
// 如果选手已评分,加载其原有评分
if (currentAthlete.scored && currentAthlete.myScore) {
this.currentScore = currentAthlete.myScore
}
// 加载评委ID和项目ID
this.judgeId = globalData.judgeId
this.projectId = globalData.currentProjectId || ''
this.competitionId = globalData.matchId || globalData.matchCode || ''
this.venueId = globalData.currentVenueId || globalData.venueId || ''
// 调试信息
if (config.debug) {
console.log('评分详情页加载:', {
athlete: this.player,
@@ -155,22 +169,17 @@ export default {
})
}
// 加载扣分项列表
await this.loadDeductions()
},
methods: {
async loadDeductions() {
try {
// 🔥 关键改动:使用 dataAdapter 获取扣分项列表
// Mock模式调用 mock/score.js 的 getDeductions 函数
// API模式调用 api/score.js 的 getDeductions 函数GET /martial/deductionItem/list
const response = await dataAdapter.getData('getDeductions', {
projectId: this.projectId
})
// 为每个扣分项添加 checked 状态,并映射字段名
const records = response.data?.records || []
const records = response.data && response.data.records ? response.data.records : []
this.deductions = records.map(item => ({
deductionId: item.id,
deductionName: item.itemName,
@@ -178,7 +187,6 @@ export default {
checked: false
}))
// 调试信息
if (config.debug) {
console.log('扣分项加载成功:', this.deductions)
}
@@ -201,7 +209,6 @@ export default {
delta: 1,
fail: (err) => {
console.error('返回失败:', err)
// 如果返回失败,尝试跳转到评分列表页
uni.redirectTo({
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) {
this.deductions[index].checked = !this.deductions[index].checked
},
async handleSubmit() {
// 验证评分范围
if (this.currentScore < this.minScore || this.currentScore > this.maxScore) {
uni.showToast({
title: `评分必须在${this.minScore}-${this.maxScore}分之间`,
@@ -235,7 +274,6 @@ export default {
return
}
// 验证必需字段
if (!this.competitionId) {
uni.showToast({
title: '缺少比赛ID请重新登录',
@@ -252,7 +290,6 @@ export default {
return
}
// 收集选中的扣分项ID
const selectedDeductions = this.deductions
.filter(item => item.checked)
.map(item => item.deductionId)
@@ -263,7 +300,6 @@ export default {
mask: true
})
// 准备提交数据
const submitData = {
athleteId: this.player.athleteId,
judgeId: this.judgeId,
@@ -275,19 +311,14 @@ export default {
note: this.note
}
// 调试日志:打印提交数据
if (config.debug) {
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)
uni.hideLoading()
// 调试信息
if (config.debug) {
console.log('评分提交成功:', {
athleteId: this.player.athleteId,
@@ -297,14 +328,12 @@ export default {
})
}
// 显示成功提示
uni.showToast({
title: '提交成功',
icon: 'success',
duration: 1500
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
@@ -485,6 +514,14 @@ export default {
display: flex;
flex-direction: column;
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 {
@@ -493,6 +530,12 @@ export default {
color: #1B7C5E;
}
.edit-hint {
font-size: 22rpx;
color: #999999;
margin-top: 8rpx;
}
.judge-tip {
padding: 0 30rpx;
font-size: 24rpx;
@@ -628,4 +671,95 @@ export default {
.submit-btn:active {
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>

View File

@@ -32,7 +32,7 @@
</scroll-view>
<view class="venue-tip">
<!-- <text class="tip-bold">裁判可看见所有场地和项目</text> -->
<!-- <text class="tip-bold">裁判可看见所有场地和项目</text> -->
<!-- <text class="tip-normal">场地和项目可动态全部可以点击切换</text> -->
</view>
@@ -68,13 +68,20 @@
<view class="player-header">
<view class="player-name">{{ player.name }}</view>
<!-- 已评分显示总分和修改按钮 -->
<view class="action-area" v-if="player.totalScore">
<text class="total-score">总分{{ player.totalScore }}</text>
<view class="chief-actions">
<!-- <text class="chief-hint">裁判长功能修改评分修改按钮需等总分出来才出现</text> -->
<button class="modify-btn" @click="goToModify(player)">修改</button>
</view>
<!-- 动作区域始终显示 -->
<view class="action-area">
<!-- 已评分显示总分和修改按钮 -->
<template v-if="player.scoringComplete && player.totalScore > 0">
<text class="total-score">总分{{ player.totalScore }}</text>
<view class="chief-actions">
<button class="modify-btn" @click="goToModify(player)">修改</button>
</view>
</template>
<!-- 未评分显示评分中提示 -->
<template v-else>
<text class="scoring-status">评分中...</text>
</template>
</view>
</view>
@@ -123,12 +130,12 @@ export default {
time: globalData.matchTime || '比赛时间'
}
// 注意:裁判没有固定场地和项目,需要查看所有
// 注意:裁判没有固定场地和项目,需要查看所有
this.competitionId = globalData.matchId
// 调试信息
if (config.debug) {
console.log('裁判列表页加载:', {
console.log('裁判列表页加载:', {
userRole: globalData.userRole,
competitionId: this.competitionId
})
@@ -205,7 +212,7 @@ export default {
mask: true
})
// 🔥 关键改动:使用 dataAdapter 获取选手列表(裁判视图)
// 🔥 关键改动:使用 dataAdapter 获取选手列表(裁判视图)
// Mock模式调用 mock/athlete.js 的 getAthletesForAdmin 函数
// API模式调用 api/athlete.js 的 getAthletesForAdmin 函数GET /api/mini/athletes/admin
const response = await dataAdapter.getData('getAthletesForAdmin', {
@@ -217,11 +224,11 @@ export default {
uni.hideLoading()
// 保存选手列表
this.players = response.data || []
this.players = (response.data.records || response.data) || []
// 计算评分统计(裁判视图:统计有总分的选手)
// 计算评分统计(裁判视图:统计有总分的选手)
this.totalCount = this.players.length
this.scoredCount = this.players.filter(p => p.totalScore).length
this.scoredCount = this.players.filter(p => p.scoringComplete).length
// 调试信息
if (config.debug) {
@@ -509,6 +516,15 @@ export default {
font-weight: 600;
}
.scoring-status {
font-size: 26rpx;
color: #FF9800;
font-weight: 500;
padding: 8rpx 20rpx;
background-color: #FFF3E0;
border-radius: 8rpx;
}
.chief-actions {
display: flex;
flex-direction: column;

View File

@@ -39,6 +39,7 @@
>
{{ project.projectName }}
</view>
<view class="no-project-tip" v-if="projects.length === 0">当前场地暂无比赛项目</view>
</view>
</view>
@@ -49,7 +50,7 @@
</view>
<!-- 选手列表 -->
<view class="player-list">
<view class="player-list" v-if="projects.length > 0">
<!-- 遍历选手列表 -->
<view
class="player-card"
@@ -60,20 +61,37 @@
<view class="player-header">
<view class="player-name">{{ player.name }}</view>
<!-- 裁判显示总分和已评分裁判数 -->
<!-- 裁判显示总分和已评分裁判数 -->
<view class="player-scores" v-if="refereeType === 1">
<text class="total-score">总分{{ player.totalScore || '未评分' }}</text>
<text class="judge-count">已评分{{ player.scoredJudgeCount || 0 }}</text>
<text class="total-score">
总分{{ player.scoringComplete ? player.totalScore : '评分中' }}
</text>
<text class="judge-count">
已评分{{ player.scoredJudgeCount || 0 }}/{{ player.requiredJudgeCount || 0 }}
</text>
</view>
<!-- 普通裁判显示评分按钮 -->
<button
class="score-btn"
v-else
@click.stop="goToScoreDetail(player)"
>
评分
</button>
<!-- 裁判根据评分状态显示不同内容 -->
<view class="judge-action" v-else>
<!-- 已评分显示分数和修改按钮 -->
<view class="scored-info" v-if="player.scored">
<text class="my-score-text">我的评分{{ player.myScore }}</text>
<button
class="score-btn modify-btn"
@click.stop="goToScoreDetail(player)"
>
修改
</button>
</view>
<!-- 未评分显示评分按钮 -->
<button
class="score-btn"
v-else
@click.stop="goToScoreDetail(player)"
>
评分
</button>
</view>
</view>
<view class="player-info">
@@ -107,7 +125,7 @@ export default {
},
judgeId: '',
matchId: '',
refereeType: 2, // 裁判类型1-裁判, 2-普通裁判)
refereeType: 2, // 裁判类型1-裁判, 2-裁判
venues: [], // 所有场地列表
currentVenueIndex: 0, // 当前选中的场地索引
projects: [], // 所有项目列表
@@ -132,7 +150,7 @@ export default {
this.judgeId = globalData.judgeId
this.matchId = globalData.matchId || globalData.matchCode
this.refereeType = globalData.refereeType || 2 // 默认为普通裁判
this.refereeType = globalData.refereeType || 2 // 默认为裁判
// 调试信息
if (config.debug) {
@@ -260,15 +278,15 @@ export default {
/**
* 处理选手卡片点击
* - 裁判:跳转到查看详情页面
* - 普通裁判:不处理(通过评分按钮跳转)
* - 裁判:跳转到查看详情页面
* - 裁判:不处理(通过评分按钮跳转)
*/
handlePlayerClick(player) {
if (this.refereeType === 1) {
// 裁判:查看评分详情
// 裁判:查看评分详情
this.goToScoreDetail(player)
}
// 普通裁判不处理卡片点击,只能通过评分按钮跳转
// 裁判不处理卡片点击,只能通过评分按钮跳转
},
goToScoreDetail(player) {
@@ -634,6 +652,29 @@ export default {
opacity: 0.9;
}
.judge-action {
display: flex;
align-items: center;
}
.scored-info {
display: flex;
align-items: center;
gap: 20rpx;
}
.my-score-text {
font-size: 28rpx;
color: #1B7C5E;
font-weight: 600;
}
.modify-btn {
background: linear-gradient(135deg, #FF9500 0%, #FFB340 100%);
padding: 10rpx 30rpx;
font-size: 26rpx;
}
.player-info {
display: flex;
flex-direction: column;
@@ -645,4 +686,12 @@ export default {
color: #666666;
line-height: 1.5;
}
.no-project-tip {
padding: 30rpx;
text-align: center;
color: #999;
font-size: 28rpx;
width: 100%;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,35 +24,36 @@
<!-- 评分提示 -->
<view class="score-tip">
点击分数填写或拖动滑块打分5-10
直接输入分数或使用加减按钮调整5-10
</view>
<!-- 分数调整 -->
<view class="score-control">
<view class="control-btn decrease" @click="decreaseScore">
<text class="btn-symbol"></text>
<!-- <text class="btn-value">-0.001</text> -->
</view>
<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 class="control-btn increase" @click="increaseScore">
<text class="btn-symbol"></text>
<!-- <text class="btn-value">+0.001</text> -->
</view>
</view>
<!-- <view class="judge-tip">
裁判评分保留3位小数点超过上限或下限时按钮置灰
</view> -->
<!-- 扣分项 -->
<view class="deduction-section">
<view class="deduction-header">
<text class="deduction-label">扣分项</text>
<!-- <text class="deduction-hint">扣分项多选</text> -->
</view>
<view class="deduction-list">
@@ -82,12 +83,35 @@
v-model="note"
maxlength="200"
/>
<!-- <text class="optional-text">可不填</text> -->
</view>
</view>
<!-- 提交按钮 -->
<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>
</template>
@@ -107,50 +131,28 @@ export default {
},
judgeId: '',
projectId: '',
competitionId: '',
venueId: '',
currentScore: 8.000,
note: '',
minScore: 5.0,
maxScore: 10.0,
deductions: []
deductions: [],
showInputModal: false,
inputScore: ''
}
},
computed: {
scoreInputValue() {
return this.currentScore.toFixed(3)
}
},
async onLoad() {
// 获取全局数据
const app = getApp()
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 || {}
this.player = {
athleteId: currentAthlete.athleteId || '',
@@ -160,57 +162,80 @@ export default {
number: currentAthlete.number || ''
}
// 如果选手已评分,加载其原有评分
if (currentAthlete.scored && currentAthlete.myScore) {
this.currentScore = currentAthlete.myScore
}
// 加载评委ID和项目ID
this.judgeId = globalData.judgeId
const projects = globalData.projects || []
const currentIndex = globalData.currentProjectIndex || 0
const currentProject = projects[currentIndex] || {}
this.projectId = currentProject.projectId
this.projectId = globalData.currentProjectId || ''
this.competitionId = globalData.matchId || globalData.matchCode || ''
this.venueId = globalData.currentVenueId || globalData.venueId || ''
// 调试信息
if (config.debug) {
console.log('评分详情页加载:', {
athlete: this.player,
judgeId: this.judgeId,
projectId: this.projectId,
competitionId: this.competitionId,
venueId: this.venueId,
initialScore: this.currentScore
})
}
// 加载扣分项列表
await this.loadDeductions()
},
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() {
try {
// 🔥 关键改动:使用 dataAdapter 获取扣分项列表
// Mock模式调用 mock/score.js 的 getDeductions 函数
// API模式调用 api/score.js 的 getDeductions 函数GET /blade-martial/deductionItem/list
const response = await dataAdapter.getData('getDeductions', {
projectId: this.projectId
})
// 获取返回数据(兼容分页和非分页格式)
const responseData = response.data || {}
const records = responseData.records || response.data || []
// 为每个扣分项添加 checked 状态,并映射字段名
// 后端字段: 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,
const records = response.data && response.data.records ? response.data.records : []
this.deductions = records.map(item => ({
deductionId: item.id,
deductionName: item.itemName,
deductionScore: parseFloat(item.deductionPoint || 0),
checked: false
}))
// 调试信息
if (config.debug) {
console.log('扣分项加载成功:', this.deductions)
}
@@ -225,7 +250,19 @@ export default {
},
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() {
@@ -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) {
this.deductions[index].checked = !this.deductions[index].checked
},
async handleSubmit() {
// 验证评分范围
if (this.currentScore < this.minScore || this.currentScore > this.maxScore) {
uni.showToast({
title: `评分必须在${this.minScore}-${this.maxScore}分之间`,
@@ -254,10 +323,25 @@ export default {
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
.filter(item => item.checked)
.map(item => String(item.deductionId))
.map(item => item.deductionId)
try {
uni.showLoading({
@@ -265,26 +349,25 @@ export default {
mask: true
})
// 🔥 关键改动:使用 dataAdapter 提交评分
// Mock模式调用 mock/score.js 的 submitScore 函数
// API模式调用 api/score.js 的 submitScore 函数POST /mini/score/submit
const app = getApp()
const globalData = app.globalData || {}
const response = await dataAdapter.getData('submitScore', {
athleteId: String(this.player.athleteId),
judgeId: String(this.judgeId),
const submitData = {
athleteId: this.player.athleteId,
judgeId: this.judgeId,
projectId: this.projectId,
competitionId: this.competitionId,
venueId: this.venueId,
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,
note: this.note || ''
})
note: this.note
}
if (config.debug) {
console.log('准备提交评分数据:', submitData)
}
const response = await dataAdapter.getData('submitScore', submitData)
uni.hideLoading()
// 调试信息
if (config.debug) {
console.log('评分提交成功:', {
athleteId: this.player.athleteId,
@@ -294,14 +377,12 @@ export default {
})
}
// 显示成功提示
uni.showToast({
title: '提交成功',
icon: 'success',
duration: 1500
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
@@ -341,12 +422,19 @@ export default {
.nav-left {
position: absolute;
left: 30rpx;
width: 60rpx;
height: 60rpx;
left: 0;
top: 0;
width: 120rpx;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
cursor: pointer;
}
.nav-left:active {
opacity: 0.6;
}
.back-icon {
@@ -475,12 +563,26 @@ export default {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx;
border-radius: 16rpx;
min-width: 240rpx;
}
.current-score {
font-size: 80rpx;
.score-input-inline {
width: 200rpx;
height: 100rpx;
font-size: 64rpx;
font-weight: 600;
color: #1B7C5E;
text-align: center;
border: 2rpx solid #E0E0E0;
border-radius: 12rpx;
background-color: #FFFFFF;
}
.score-input-inline:focus {
border-color: #1B7C5E;
}
.judge-tip {
@@ -618,4 +720,95 @@ export default {
.submit-btn:active {
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>

View File

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

View File

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

View File

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

View File

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

View File

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