Compare commits

..

14 Commits

Author SHA1 Message Date
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
DevOps
c978a5bf64 fix: 修复 iOS Safari 快速点击按钮触发页面缩放问题
- 添加 touch-action: manipulation 禁用双击缩放
- 添加 -webkit-tap-highlight-color: transparent 移除点击高亮
- 在全局样式和修改评分页面按钮上应用

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 00:46:48 +08:00
DevOps
f9efd8baa8 fix: 优化总分显示逻辑,使用后端返回的 scoringComplete 字段
1. 总分显示条件改为使用 player.scoringComplete
2. 等待状态显示已评分/应评裁判数量
3. 移除前端计算逻辑,统一由后端控制

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 23:10:03 +08:00
DevOps
53c865a076 refactor: 优化评分列表页面UI和代码结构
- 重构场地和项目选择区域布局
- 优化选手卡片样式
- 添加日期时间格式化方法
- 改进分数显示格式
- 统一样式命名规范

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 17:26:34 +08:00
DevOps
569f8a14d1 裁判长修改分数功能优化
1. 限制裁判长修改分数范围为±0.050
2. 优化评委评分展示样式,添加灰色边框背景块

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-21 15:22:03 +08:00
DevOps
bcf040bb15 fix: 修复评分列表总分显示问题
- 添加formatScore方法,将-1显示为'--'
- score-detail.vue: 提交评分时使用String()转换ID

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:29:54 +08:00
DevOps
7620d9bf96 feat: 更新评分相关页面和API配置
- 更新环境配置文件
- 修改运动员和评分API
- 优化登录、评分详情、评分列表等页面
- 更新pages.json和vue.config.js配置

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 14:43:16 +08:00
DevOps
736aa08fba docs: 添加 Linux 命令行编译样式问题修复记录
详细记录了问题原因、解决方案和技术细节

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 17:46:28 +08:00
DevOps
e8a2a5cef6 fix: 修复 Linux/Mac 命令行编译样式异常问题
问题原因:
- 自定义 postcss.config.js 覆盖了 uni-app 默认配置
- 导致 rpx 单位没有被正确转换

修复方案:
- 在 postcss.config.js 中添加 uni-app 的 postcss 插件
- 降级 postcss 到版本 7 以兼容 postcss-loader 3.x
- 降级 sass 到 1.32.13 以提高兼容性

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 17:35:12 +08:00
23 changed files with 1829 additions and 2155 deletions

31
App.vue
View File

@@ -2,17 +2,44 @@
export default {
onLaunch: function() {
console.log('App Launch')
// 禁用 iOS Safari 双击缩放
this.disableDoubleTapZoom()
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
},
methods: {
disableDoubleTapZoom() {
// #ifdef H5
let lastTouchEnd = 0
document.documentElement.addEventListener('touchstart', function(event) {
if (event.touches.length > 1) {
event.preventDefault()
}
}, { passive: false })
document.documentElement.addEventListener('touchend', function(event) {
const now = Date.now()
if (now - lastTouchEnd <= 300) {
event.preventDefault()
}
lastTouchEnd = now
}, { passive: false })
// 禁用手势缩放
document.documentElement.addEventListener('gesturestart', function(event) {
event.preventDefault()
}, { passive: false })
// #endif
}
}
}
</script>
<style>
/* 注意要写在第一行同时给style标签加入lang="scss"属性 */
@import "common/common.css";
/* 注意要写在第一行同时给style标签加入lang=scss属性 */
@import common/common.css;
</style>

View File

@@ -23,7 +23,10 @@ export function getMyAthletes(params) {
return request({
url: '/mini/score/athletes',
method: 'GET',
params: params, // GET 请求使用 params
params: {
...params,
size: 200 // 确保获取所有选手
},
showLoading: true
})
}
@@ -36,14 +39,24 @@ export function getMyAthletes(params) {
* @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

@@ -28,3 +28,14 @@ button::after {
input {
outline: none;
}
/* 防止 iOS Safari 双击缩放 */
button, .control-btn, [class*="btn"] {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* 全局禁用双击缩放 */
html {
touch-action: manipulation;
}

View File

@@ -17,7 +17,11 @@ const ENV_CONFIG = {
dataMode: 'api',
// API基础路径dataMode为'api'时使用)
apiBaseURL: 'http://localhost:8123',
// uni.request 不支持 devServer proxy必须用完整地址
apiBaseURL: 'http://142.91.105.230:8123',
// 调试模式
debug: true,
// 请求超时时间(毫秒)
timeout: 30000,

View File

@@ -0,0 +1,166 @@
# Linux/Mac 命令行编译样式异常问题修复记录
## 问题描述
在 Windows 上使用 HBuilderX 运行项目时样式正常,但在 Linux/Mac 上使用 `npm run dev:h5` 命令行编译时,页面样式完全异常,组件的 scoped 样式没有生效。
## 问题现象
- 页面布局错乱
- rpx 单位没有被转换为 px
- 组件的 scoped 样式没有被正确打包
## 根本原因
### 1. postcss.config.js 配置覆盖问题
项目中自定义的 `postcss.config.js` 文件覆盖了 uni-app 的默认 postcss 配置:
```javascript
// 原配置 - 问题配置
const autoprefixer = require('autoprefixer')
module.exports = {
plugins: [
autoprefixer()
]
}
```
这个配置**没有包含 uni-app 的 postcss 插件**,导致:
- rpx 单位没有被转换为 `%?数值?%` 占位符格式
- uni-app 运行时无法在浏览器中将占位符转换为实际的 px 值
### 2. HBuilderX vs 命令行编译的区别
| 特性 | HBuilderX | 命令行 (npm run dev:h5) |
|------|-----------|------------------------|
| 编译器 | 内置优化版编译器 | 依赖 node_modules |
| rpx 处理 | 自动转换 | 需要 postcss 插件 |
| 样式处理 | 完善的内置处理 | 依赖配置文件 |
| 版本兼容 | 内部统一管理 | 可能存在版本冲突 |
### 3. PostCSS 版本兼容问题
- 项目使用 `postcss-loader@3.0.0`(旧版)
-`postcss` 被升级到了 8.x 版本
- postcss-loader 3.x 与 postcss 8 不兼容
## 解决方案
### 1. 修改 postcss.config.js
添加 uni-app 的 postcss 插件:
```javascript
const autoprefixer = require('autoprefixer')
// 引入 uni-app 的 postcss 插件来处理 rpx 转换
const uniappPlugin = require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
module.exports = {
plugins: [
uniappPlugin,
autoprefixer
]
}
```
### 2. 降级 postcss 版本
```bash
npm install postcss@7 --save --legacy-peer-deps
```
### 3. 降级 sass 版本(可选,提高兼容性)
```bash
npm install sass@1.32.13 --save
```
## 修复后的依赖版本
```json
{
"dependencies": {
"postcss": "^7.0.39",
"postcss-loader": "^3.0.0",
"sass": "^1.32.13"
}
}
```
## 验证方法
1. 构建项目:
```bash
npm run build:h5
```
2. 检查构建后的 JS 文件中 rpx 是否被转换:
```bash
# 应该看到 %?90?% 这样的占位符,而不是 90rpx
grep -oE "height:%\?[0-9]+\?%" dist/build/h5/static/js/pages-login-login.*.js
```
3. 启动开发服务器验证样式:
```bash
npm run dev:h5
# 访问 http://localhost:8080 查看样式
```
## 技术细节
### uni-app 的 rpx 转换流程
1. **编译时**postcss 插件将 `90rpx` 转换为 `%?90?%` 占位符
2. **运行时**uni-app 的 Vue 运行时根据屏幕宽度将占位符转换为实际 px 值
3. **计算公式**`px = rpx * (屏幕宽度 / 750)`
### 相关文件
- `/postcss.config.js` - PostCSS 配置
- `/node_modules/@dcloudio/vue-cli-plugin-uni/packages/postcss/index.js` - uni-app postcss 插件
- `/node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js` - 运行时 rpx 转换
## 注意事项
1. **不要随意升级 postcss 版本**postcss-loader 3.x 只兼容 postcss 7.x
2. **保留 uni-app postcss 插件**:这是 rpx 转换的关键
3. **Node.js 版本建议**:使用 Node 16.x 以获得最佳兼容性
## 环境要求
- Node.js: 16.x (推荐 16.20.2)
- npm: 8.x
- postcss: 7.x
- postcss-loader: 3.x
## 相关命令
```bash
# 安装 nvm如果没有
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# 切换到 Node 16
nvm install 16
nvm use 16
# 重新安装依赖
rm -rf node_modules package-lock.json
npm install
# 开发模式
npm run dev:h5
# 生产构建
npm run build:h5
```
## 修复日期
2024-12-17
## 修复分支
`devops`

View File

@@ -3,15 +3,236 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<!-- 关键:使用最严格的 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: 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>
// 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();
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();
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>
<body>
<noscript>
<strong>请开启JavaScript运行本应用</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

1565
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,8 +20,9 @@
"css-loader": "^3.6.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^4.5.2",
"postcss": "^7.0.39",
"postcss-loader": "^3.0.0",
"sass": "^1.96.0",
"sass": "^1.32.13",
"sass-loader": "^10.5.2",
"thread-loader": "^2.1.3",
"url-loader": "^4.1.1",
@@ -43,6 +44,7 @@
"cross-env": "^7.0.3",
"mini-types": "*",
"postcss-comment": "^2.0.0",
"postcss-import": "^12.0.1",
"vue-template-compiler": "^2.6.14"
},
"browserslist": [

View File

@@ -50,7 +50,13 @@
</view>
<view class="score-control">
<view class="control-btn decrease" @click="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" @click="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,7 +140,7 @@ export default {
const globalData = app.globalData || {}
// 获取当前选手信息(从 score-list-multi 页面传递)
const currentAthlete = globalData.currentAthlete || {}
const currentAthlete = globalData.currentAthlete ||
// 获取裁判长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()
@@ -465,8 +636,16 @@ export default {
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 {
@@ -480,6 +659,7 @@ export default {
.btn-symbol {
font-size: 48rpx;
font-weight: 300;
pointer-events: none;
}
.control-btn.decrease .btn-symbol {
@@ -493,6 +673,7 @@ export default {
.btn-value {
font-size: 24rpx;
margin-top: 8rpx;
pointer-events: none;
}
.control-btn.decrease .btn-value {
@@ -521,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

@@ -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>
@@ -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) {
@@ -389,6 +396,8 @@ export default {
position: relative;
white-space: nowrap;
flex-shrink: 0;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.venue-tab.active {
@@ -441,6 +450,8 @@ export default {
color: #666666;
white-space: nowrap;
flex-shrink: 0;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.project-btn.active {
@@ -505,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;
@@ -527,6 +547,8 @@ export default {
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.modify-btn:active {

View File

@@ -62,18 +62,35 @@
<!-- 裁判长显示总分和已评分裁判数 -->
<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">
@@ -634,6 +651,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;

View File

@@ -1,7 +1,13 @@
const autoprefixer = require('autoprefixer')
// 引入 uni-app 的 postcss 插件来处理 rpx 转换
// 使用 postcss.plugin 旧版 API (postcss-loader 3.x 兼容)
const postcss = require('postcss')
const uniappPlugin = require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
module.exports = {
plugins: [
autoprefixer()
uniappPlugin,
autoprefixer
]
}

7
postcss.config.js.bak Normal file
View File

@@ -0,0 +1,7 @@
const autoprefixer = require('autoprefixer')
module.exports = {
plugins: [
autoprefixer()
]
}

View File

@@ -24,9 +24,12 @@ import request from '@/utils/request.js'
*/
export function getMyAthletes(params) {
return request({
url: '/api/mini/athletes',
url: '/mini/score/athletes',
method: 'GET',
params: params, // GET 请求使用 params
params: {
...params,
refereeType: 2 // 普通裁判
},
showLoading: true
})
}
@@ -44,9 +47,12 @@ export function getMyAthletes(params) {
*/
export function getAthletesForAdmin(params) {
return request({
url: '/api/mini/athletes/admin',
url: '/mini/score/athletes',
method: 'GET',
params: params, // GET 请求使用 params
params: {
...params,
refereeType: 1 // 裁判长
},
showLoading: true
})
}

View File

@@ -13,7 +13,7 @@ import request from '@/utils/request.js'
*/
export function getDeductions(params) {
return request({
url: '/martial/deductionItem/list',
url: '/blade-martial/deductionItem/list',
method: 'GET',
params: {
...params,
@@ -35,7 +35,7 @@ export function getDeductions(params) {
*/
export function submitScore(data) {
return request({
url: '/martial/score/submit',
url: '/mini/score/submit',
method: 'POST',
data,
showLoading: true,
@@ -54,7 +54,7 @@ export function submitScore(data) {
*/
export function getScoreDetail(params) {
return request({
url: `/api/mini/score/detail/${params.athleteId}`,
url: `/mini/score/detail/${params.athleteId}`,
method: 'GET',
showLoading: true
})
@@ -74,7 +74,7 @@ export function getScoreDetail(params) {
*/
export function modifyScore(data) {
return request({
url: '/api/mini/score/modify',
url: '/mini/score/modify',
method: 'PUT',
data,
showLoading: true,

View File

@@ -17,11 +17,14 @@ const ENV_CONFIG = {
dataMode: 'api',
// API基础路径dataMode为'api'时使用)
apiBaseURL: 'http://localhost:8123',
// uni.request 不支持 devServer proxy必须用完整地址
apiBaseURL: 'http://142.91.105.230:8123',
// 请求超时时间(毫秒)
timeout: 30000,
// 调试模式
debug: true
},
// 测试环境配置

View File

@@ -11,7 +11,9 @@
"path": "pages/score-list/score-list",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}
},
{

View File

@@ -120,6 +120,7 @@ export default {
// 保存用户信息到全局数据
getApp().globalData = {
token, // Token用于登录状态检查
userRole, // 'pub' 或 'admin'
matchCode: this.matchCode, // 比赛编码
inviteCode: this.inviteCode, // 邀请码重要用于后续API调用

View File

@@ -18,7 +18,7 @@
<view class="player-name">{{ athleteInfo.name }}</view>
<view class="total-score-label">
<text class="label-text">总分</text>
<text class="score-value">{{ athleteInfo.totalScore }}</text>
<text class="score-value">{{ formatScore(athleteInfo.totalScore) }}</text>
</view>
</view>
<view class="player-details">
@@ -46,7 +46,7 @@
<!-- 修改总分区域 -->
<view class="modify-section">
<view class="modify-header">
<text class="modify-label">修改总分+-0.005</text>
<text class="modify-label">修改总分±0.050</text>
</view>
<view class="score-control">
@@ -65,10 +65,6 @@
<text class="btn-value">+0.001</text>
</view>
</view>
<!-- <view class="modify-tip">
裁判长修改保留3位小数点超过上限或下限时按钮置灰
</view> -->
</view>
<!-- 备注 -->
@@ -110,10 +106,10 @@ export default {
judgeScores: [],
modification: null,
modifierId: '',
currentScore: 8.000,
originalScore: 8.000,
currentScore: 0,
originalScore: 0,
note: '',
minScore: 5.0,
minScore: 0,
maxScore: 10.0
}
},
@@ -123,27 +119,101 @@ export default {
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.userRole !== 'admin') {
console.warn('非裁判长用户,无权修改评分')
uni.showToast({
title: '无权限',
icon: 'none',
duration: 1500
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
return
}
// 检查是否有选手信息
if (!globalData.currentAthlete || !globalData.currentAthlete.athleteId) {
console.warn('没有选手信息,返回列表页')
uni.showToast({
title: '请选择选手',
icon: 'none',
duration: 1500
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
return
}
// 获取当前选手信息(从 score-list-multi 页面传递)
const currentAthlete = globalData.currentAthlete || {}
// 获取裁判长ID
this.modifierId = globalData.judgeId
// 🔥 关键修复:先用传递过来的选手数据初始化页面
this.athleteInfo = {
athleteId: currentAthlete.athleteId,
name: currentAthlete.name || '',
idCard: currentAthlete.idCard || '',
team: currentAthlete.team || '',
number: currentAthlete.number || '',
totalScore: currentAthlete.totalScore || 0
}
// 设置初始分数(使用传递过来的总分)
const totalScore = parseFloat(currentAthlete.totalScore) || 0
this.originalScore = totalScore
this.currentScore = totalScore
// 调试信息
if (config.debug) {
console.log('修改评分页加载:', {
currentAthlete: currentAthlete,
athleteId: currentAthlete.athleteId,
totalScore: totalScore,
modifierId: this.modifierId
})
}
// 加载选手评分详情
// 尝试加载选手评分详情(获取各评委的评分)
if (currentAthlete.athleteId) {
await this.loadScoreDetail(currentAthlete.athleteId)
}
},
methods: {
formatScore(score) {
if (score === null || score === undefined || score === -1 || score === '-1') {
return '--'
}
if (typeof score === 'string' && !isNaN(parseFloat(score))) {
return parseFloat(score).toFixed(3)
}
if (typeof score === 'number') {
return score.toFixed(3)
}
return score
},
async loadScoreDetail(athleteId) {
try {
uni.showLoading({
@@ -151,27 +221,35 @@ 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
})
uni.hideLoading()
// 保存选手信息和评分详情
this.athleteInfo = response.data.athleteInfo || {}
this.judgeScores = response.data.judgeScores || []
this.modification = response.data.modification || null
// 如果接口返回了数据,更新页面
if (response && response.data) {
// 更新评委评分列表
this.judgeScores = response.data.judgeScores || []
this.modification = response.data.modification || null
// 设置初始分数
this.originalScore = this.athleteInfo.totalScore || 8.000
this.currentScore = this.originalScore
// 如果接口返回了选手信息,更新(但保留传递过来的数据作为备用)
if (response.data.athleteInfo) {
const apiAthleteInfo = response.data.athleteInfo
this.athleteInfo = {
athleteId: apiAthleteInfo.athleteId || this.athleteInfo.athleteId,
name: apiAthleteInfo.name || this.athleteInfo.name,
idCard: apiAthleteInfo.idCard || this.athleteInfo.idCard,
team: apiAthleteInfo.team || this.athleteInfo.team,
number: apiAthleteInfo.number || this.athleteInfo.number,
totalScore: apiAthleteInfo.totalScore || this.athleteInfo.totalScore
}
// 如果之前已修改过,加载修改后的分数
if (this.modification && this.modification.modifiedScore) {
this.currentScore = this.modification.modifiedScore
// 更新分数
const totalScore = parseFloat(apiAthleteInfo.totalScore) || this.originalScore
this.originalScore = totalScore
this.currentScore = totalScore
}
}
// 调试信息
@@ -188,10 +266,10 @@ export default {
} catch (error) {
uni.hideLoading()
console.error('加载评分详情失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
// 不显示错误提示,因为已经有传递过来的数据可以使用
if (config.debug) {
console.log('使用传递过来的选手数据')
}
}
},
@@ -200,22 +278,28 @@ export default {
},
decreaseScore() {
if (this.currentScore > this.minScore) {
// 限制最小值为原始分数-0.050
const minAllowed = parseFloat((this.originalScore - 0.050).toFixed(3))
if (this.currentScore > minAllowed) {
this.currentScore = parseFloat((this.currentScore - 0.001).toFixed(3))
}
},
increaseScore() {
if (this.currentScore < this.maxScore) {
// 限制最大值为原始分数+0.050
const maxAllowed = parseFloat((this.originalScore + 0.050).toFixed(3))
if (this.currentScore < maxAllowed) {
this.currentScore = parseFloat((this.currentScore + 0.001).toFixed(3))
}
},
async handleModify() {
// 验证评分范围
if (this.currentScore < this.minScore || this.currentScore > this.maxScore) {
// 验证评分范围±0.050
const minAllowed = parseFloat((this.originalScore - 0.050).toFixed(3))
const maxAllowed = parseFloat((this.originalScore + 0.050).toFixed(3))
if (this.currentScore < minAllowed || this.currentScore > maxAllowed) {
uni.showToast({
title: `评分必须在${this.minScore}-${this.maxScore}分之间`,
title: '评分只能在原始分数±0.050范围内',
icon: 'none'
})
return
@@ -236,12 +320,9 @@ export default {
mask: true
})
// 🔥 关键改动:使用 dataAdapter 修改评分
// Mock模式调用 mock/score.js 的 modifyScore 函数
// API模式调用 api/score.js 的 modifyScore 函数PUT /api/mini/score/modify
const response = await dataAdapter.getData('modifyScore', {
athleteId: this.athleteInfo.athleteId,
modifierId: this.modifierId,
athleteId: this.athleteInfo.athleteId,
modifiedScore: this.currentScore,
note: this.note
})
@@ -420,15 +501,21 @@ export default {
}
.judge-score-item {
font-size: 26rpx;
color: #333333;
display: flex;
align-items: center;
padding: 12rpx 20rpx;
background-color: #F5F5F5;
border-radius: 8rpx;
border: 2rpx solid #E5E5E5;
}
.judge-name {
font-size: 24rpx;
color: #666666;
}
.judge-score {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}

View File

@@ -120,6 +120,36 @@ export default {
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 = {
@@ -161,14 +191,22 @@ export default {
try {
// 🔥 关键改动:使用 dataAdapter 获取扣分项列表
// Mock模式调用 mock/score.js 的 getDeductions 函数
// API模式调用 api/score.js 的 getDeductions 函数GET /martial/deductionItem/list
// API模式调用 api/score.js 的 getDeductions 函数GET /blade-martial/deductionItem/list
const response = await dataAdapter.getData('getDeductions', {
projectId: this.projectId
})
// 为每个扣分项添加 checked 状态
this.deductions = (response.data || []).map(item => ({
...item,
// 获取返回数据(兼容分页和非分页格式)
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,
checked: false
}))
@@ -216,10 +254,10 @@ export default {
return
}
// 收集选中的扣分项ID
// 收集选中的扣分项ID(转为数字类型,后端期望 List<Long>
const selectedDeductions = this.deductions
.filter(item => item.checked)
.map(item => item.deductionId)
.map(item => String(item.deductionId))
try {
uni.showLoading({
@@ -229,13 +267,19 @@ export default {
// 🔥 关键改动:使用 dataAdapter 提交评分
// Mock模式调用 mock/score.js 的 submitScore 函数
// API模式调用 api/score.js 的 submitScore 函数POST /martial/score/submit
// API模式调用 api/score.js 的 submitScore 函数POST /mini/score/submit
const app = getApp()
const globalData = app.globalData || {}
const response = await dataAdapter.getData('submitScore', {
athleteId: this.player.athleteId,
judgeId: this.judgeId,
athleteId: String(this.player.athleteId),
judgeId: String(this.judgeId),
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 || ''
})
uni.hideLoading()

View File

@@ -4,57 +4,45 @@
<view class="nav-bar">
<view class="nav-title">评分系统</view>
<view class="nav-right">
<view class="icon-menu">···</view>
<view class="icon-close"></view>
<view class="nav-dots">···</view>
<view class="nav-circle"></view>
</view>
</view>
<!-- 比赛信息 -->
<view class="match-info">
<view class="match-title">{{ matchInfo.name }}</view>
<view class="match-time">比赛时间{{ matchInfo.time }}</view>
<view class="match-time">比赛时间{{ formatDateTime(matchInfo.time) }}</view>
</view>
<!-- 场地和项目选择 -->
<view class="venue-section">
<!-- 场地切换 - 横向滚动 -->
<scroll-view class="venue-scroll" scroll-x="true" show-scrollbar="false">
<view class="venue-tabs">
<view
v-for="venue in venues"
:key="venue.venueId"
:class="['venue-tab', currentVenue === venue.venueId ? 'active' : '']"
@click="switchVenue(venue.venueId)"
>
{{ venue.venueName }}
</view>
</view>
</scroll-view>
<view class="venue-tip">
<!-- <text class="tip-bold">裁判长可看见所有场地和项目</text> -->
<!-- <text class="tip-normal">场地和项目可动态全部可以点击切换</text> -->
<!-- 场地和项目卡片 -->
<view class="venue-card">
<!-- 场地标题行 -->
<view class="venue-header">
<view class="venue-name">{{ venueInfo.name }}</view>
<view class="refresh-link" @click="handleRefresh">刷新</view>
</view>
<!-- 项目选 - 横向滚动 -->
<scroll-view class="project-scroll" scroll-x="true" show-scrollbar="false">
<view class="project-list">
<!-- 项目 -->
<view class="project-row">
<view class="project-grid">
<view
class="project-chip"
:class="{ active: index === currentProjectIndex }"
v-for="(project, index) in projects"
:key="project.projectId"
:class="['project-btn', currentProject === project.projectId ? 'active' : '']"
@click="switchProject(project.projectId)"
@click="switchProject(index)"
>
{{ project.projectName }}
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 已评分统计 -->
<view class="score-stats">
<text class="stats-text">已评分</text>
<text class="stats-number">{{ scoredCount }}/{{ totalCount }}</text>
<text class="stats-label">已评分</text>
<text class="stats-value">{{ scoredCount }}/{{ totalCount }}</text>
</view>
<!-- 选手列表 -->
@@ -65,25 +53,42 @@
v-for="player in players"
:key="player.athleteId"
>
<view class="player-header">
<view class="card-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 class="score-tag">
<text class="tag-label">总分</text>
<text class="tag-value">{{ formatScore(player.totalScore) }}</text>
</view>
<button class="modify-btn" @click="goToModify(player)">修改</button>
</view>
</view>
<view class="player-info">
<view class="info-item">身份证{{ player.idCard }}</view>
<view class="info-item">队伍{{ player.team }}</view>
<view class="info-item">编号{{ player.number }}</view>
<view class="player-details">
<view class="detail-row">
<text class="detail-text">身份证{{ player.idCard }}</text>
</view>
<view class="detail-row">
<text class="detail-text">队伍{{ player.team }}</text>
</view>
<view class="detail-row">
<text class="detail-text">编号{{ player.number }}</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-status" v-if="players.length > 0">
<view v-if="isLoading" class="loading-text">加载中...</view>
<view v-else-if="!hasMore" class="no-more-text"> 没有更多了 </view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!isLoading && players.length === 0">
<text class="empty-text">暂无选手数据</text>
</view>
</view>
</view>
</template>
@@ -96,190 +101,253 @@ export default {
data() {
return {
matchInfo: {
id: '',
name: '',
time: ''
},
competitionId: '',
currentVenue: '',
currentProject: '',
venues: [],
venueInfo: {
id: '',
name: ''
},
projectInfo: {
id: '',
name: ''
},
judgeId: '',
projects: [],
currentProjectIndex: 0,
players: [],
scoredCount: 0,
totalCount: 0
totalCount: 0,
pagination: {
current: 1,
size: 10,
total: 0
},
isLoading: false,
hasMore: true,
isFirstLoad: true
}
},
async onLoad() {
// 获取全局数据
const app = getApp()
const globalData = app.globalData || {}
// 加载比赛信息
this.matchInfo = {
id: globalData.matchId,
name: globalData.matchName || '比赛名称',
time: globalData.matchTime || '比赛时间'
// 检查登录状态
if (!globalData.judgeId || !globalData.token) {
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 1500
})
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' })
}, 1500)
return
}
// 注意:裁判长没有固定场地和项目,需要查看所有
this.competitionId = globalData.matchId
// 检查是否是裁判长
if (globalData.userRole !== 'admin') {
console.warn('非裁判长用户,跳转到普通评分页')
uni.reLaunch({
url: '/pages/score-list/score-list'
})
return
}
// 加载比赛信息
this.matchInfo = {
name: globalData.matchName || '比赛名称',
time: globalData.matchTime || ''
}
// 从 globalData 获取场地信息(与普通裁判相同)
this.venueInfo = {
id: globalData.venueId,
name: globalData.venueName || '场地'
}
// 从 globalData 获取项目列表
this.projects = globalData.projects || []
this.currentProjectIndex = globalData.currentProjectIndex || 0
this.updateCurrentProject()
this.judgeId = globalData.judgeId
// 调试信息
if (config.debug) {
console.log('裁判长列表页加载:', {
userRole: globalData.userRole,
competitionId: this.competitionId
judgeId: this.judgeId,
venueId: this.venueInfo.id,
projectId: this.projectInfo.id
})
}
// 加载场地和项目列表
await this.loadVenuesAndProjects()
await this.loadPlayers(true)
this.isFirstLoad = false
},
async onShow() {
// 从修改评分页返回时刷新数据
if (!this.isFirstLoad) {
if (config.debug) {
console.log('页面显示,刷新数据')
}
await this.loadPlayers(true)
}
},
async onPullDownRefresh() {
await this.loadPlayers(true)
uni.stopPullDownRefresh()
},
async onReachBottom() {
if (this.hasMore && !this.isLoading) {
await this.loadMore()
}
},
methods: {
async loadVenuesAndProjects() {
formatDateTime(dateTimeStr) {
if (!dateTimeStr) return ''
try {
uni.showLoading({
title: '加载中...',
mask: true
})
// 🔥 关键改动:使用 dataAdapter 获取场地列表
// Mock模式调用 mock/athlete.js 的 getVenues 函数
// API模式调用 api/athlete.js 的 getVenues 函数GET /martial/venue/list
const venuesRes = await dataAdapter.getData('getVenues', {
competitionId: this.competitionId
})
// 🔥 关键改动:使用 dataAdapter 获取项目列表
// Mock模式调用 mock/athlete.js 的 getProjects 函数
// API模式调用 api/athlete.js 的 getProjects 函数GET /martial/project/list
const projectsRes = await dataAdapter.getData('getProjects', {
competitionId: this.competitionId
})
this.venues = venuesRes.data || []
this.projects = projectsRes.data || []
// 默认选中第一个场地和项目
if (this.venues.length > 0) {
this.currentVenue = this.venues[0].venueId
}
if (this.projects.length > 0) {
this.currentProject = this.projects[0].projectId
}
uni.hideLoading()
// 调试信息
if (config.debug) {
console.log('场地和项目加载成功:', {
venues: this.venues.length,
projects: this.projects.length,
currentVenue: this.currentVenue,
currentProject: this.currentProject
})
}
// 加载选手列表
if (this.currentVenue && this.currentProject) {
await this.loadPlayers()
}
const date = new Date(dateTimeStr)
if (isNaN(date.getTime())) return dateTimeStr
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hours = date.getHours()
const minutes = date.getMinutes()
const paddedMinutes = minutes < 10 ? '0' + minutes : minutes
return year + '年' + month + '月' + day + '日 ' + hours + ':' + paddedMinutes
} catch (error) {
uni.hideLoading()
console.error('加载场地和项目失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
return dateTimeStr
}
},
async loadPlayers() {
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
},
async handleRefresh() {
if (this.isLoading) return
uni.showToast({ title: '刷新中...', icon: 'loading', duration: 1000 })
await this.loadPlayers(true)
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1000 })
},
async loadPlayers(refresh = false) {
if (this.isLoading) return
try {
uni.showLoading({
title: '加载中...',
mask: true
this.isLoading = true
if (refresh) {
this.pagination.current = 1
this.hasMore = true
}
if (refresh && this.isFirstLoad) {
uni.showLoading({ title: '加载中...', mask: true })
}
const app = getApp()
const globalData = app.globalData || {}
const params = {
matchCode: globalData.matchCode,
judgeId: this.judgeId,
venueId: this.venueInfo.id,
projectId: this.projectInfo.id,
current: this.pagination.current,
size: this.pagination.size
}
Object.keys(params).forEach(key => {
if (params[key] === undefined || params[key] === null || params[key] === '') {
delete params[key]
}
})
// 🔥 关键改动:使用 dataAdapter 获取选手列表(裁判长视图)
// Mock模式调用 mock/athlete.js 的 getAthletesForAdmin 函数
// API模式调用 api/athlete.js 的 getAthletesForAdmin 函数GET /api/mini/athletes/admin
const response = await dataAdapter.getData('getAthletesForAdmin', {
competitionId: this.competitionId,
venueId: this.currentVenue,
projectId: this.currentProject
})
uni.hideLoading()
// 保存选手列表
this.players = response.data || []
// 计算评分统计(裁判长视图:统计有总分的选手)
this.totalCount = this.players.length
this.scoredCount = this.players.filter(p => p.totalScore).length
// 调试信息
if (config.debug) {
console.log('选手列表加载成功:', {
venueId: this.currentVenue,
projectId: this.currentProject,
total: this.totalCount,
console.log('请求选手列表参数:', params)
}
// 裁判长使用 getAthletesForAdmin 接口
const response = await dataAdapter.getData('getAthletesForAdmin', params)
if (config.debug) {
console.log('选手列表响应:', response)
}
if (refresh && this.isFirstLoad) {
uni.hideLoading()
}
const responseData = response.data || {}
const records = responseData.records || response.data || []
const total = responseData.total || records.length
this.pagination.total = total
this.totalCount = total
if (refresh) {
this.players = records
} else {
this.players = [...this.players, ...records]
}
// 裁判长视图:统计有总分的选手
this.scoredCount = this.players.filter(p => p.totalScore).length
this.hasMore = this.players.length < total
if (config.debug) {
console.log('选手列表处理结果:', {
total: total,
loaded: this.players.length,
scored: this.scoredCount,
players: this.players
})
}
} catch (error) {
uni.hideLoading()
console.error('加载选手列表失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
uni.showToast({ title: error.message || '加载失败', icon: 'none' })
} finally {
this.isLoading = false
}
},
async switchVenue(venueId) {
if (this.currentVenue === venueId) return
this.currentVenue = venueId
// 调试信息
if (config.debug) {
console.log('切换场地:', venueId)
}
// 重新加载选手列表
await this.loadPlayers()
},
async switchProject(projectId) {
if (this.currentProject === projectId) return
this.currentProject = projectId
// 调试信息
if (config.debug) {
console.log('切换项目:', projectId)
}
// 重新加载选手列表
await this.loadPlayers()
async loadMore() {
if (!this.hasMore || this.isLoading) return
this.pagination.current++
await this.loadPlayers(false)
},
goToModify(player) {
// 保存当前选手信息到全局数据
const app = getApp()
app.globalData.currentAthlete = player
uni.navigateTo({ url: '/pages/modify-score/modify-score' })
},
uni.navigateTo({
url: '/pages/modify-score/modify-score'
})
updateCurrentProject() {
const currentProject = this.projects[this.currentProjectIndex] || {}
this.projectInfo = {
id: currentProject.projectId,
name: currentProject.projectName || '项目'
}
},
async switchProject(index) {
if (index === this.currentProjectIndex) return
this.currentProjectIndex = index
const app = getApp()
app.globalData.currentProjectIndex = index
this.updateCurrentProject()
await this.loadPlayers(true)
}
}
}
@@ -292,7 +360,7 @@ export default {
padding-bottom: 40rpx;
}
/* 导航栏 */
/* ==================== 导航栏 ==================== */
.nav-bar {
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
@@ -307,7 +375,6 @@ export default {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
letter-spacing: 2rpx;
}
.nav-right {
@@ -315,24 +382,15 @@ export default {
right: 30rpx;
display: flex;
align-items: center;
gap: 30rpx;
gap: 20rpx;
}
.icon-menu,
.icon-close {
width: 60rpx;
height: 60rpx;
background-color: rgba(255, 255, 255, 0.25);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.nav-dots, .nav-circle {
font-size: 32rpx;
color: #FFFFFF;
font-weight: bold;
}
/* 比赛信息 */
/* ==================== 比赛信息 ==================== */
.match-info {
padding: 30rpx;
background-color: #F5F5F5;
@@ -341,132 +399,94 @@ export default {
.match-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
line-height: 1.6;
margin-bottom: 10rpx;
}
.tip-text {
font-size: 24rpx;
color: #FF4D6A;
margin-bottom: 10rpx;
color: #1B7C5E;
line-height: 1.5;
margin-bottom: 8rpx;
}
.match-time {
font-size: 28rpx;
color: #666666;
color: #333333;
}
/* 场地和项目区域 */
.venue-section {
/* ==================== 场地卡片 ==================== */
.venue-card {
margin: 0 30rpx 20rpx;
background-color: #FFFFFF;
margin: 20rpx 30rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
/* 场地滚动容器 */
.venue-scroll {
width: 100%;
white-space: nowrap;
margin-bottom: 20rpx;
.venue-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 24rpx;
border-bottom: 4rpx solid #1B7C5E;
margin-bottom: 24rpx;
}
.venue-tabs {
display: inline-flex;
gap: 30rpx;
padding-bottom: 20rpx;
border-bottom: 4rpx solid #E0E0E0;
position: relative;
}
.venue-tab {
.venue-name {
font-size: 32rpx;
font-weight: 500;
color: #666666;
padding: 0 20rpx;
position: relative;
white-space: nowrap;
flex-shrink: 0;
}
.venue-tab.active {
font-weight: 600;
color: #333333;
}
.venue-tab.active::after {
content: '';
position: absolute;
bottom: -24rpx;
left: 0;
right: 0;
height: 4rpx;
background-color: #1B7C5E;
.refresh-link {
font-size: 26rpx;
color: #4A90D9;
}
.venue-tip {
font-size: 24rpx;
line-height: 1.6;
margin-bottom: 20rpx;
.project-row {
display: flex;
flex-direction: column;
}
.tip-bold {
color: #FF4D6A;
font-weight: 500;
.project-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.tip-normal {
color: #FF4D6A;
}
/* 项目滚动容器 */
.project-scroll {
width: 100%;
white-space: nowrap;
}
.project-list {
display: inline-flex;
gap: 20rpx;
}
.project-btn {
padding: 20rpx 30rpx;
background-color: #FFFFFF;
border: 2rpx solid #CCCCCC;
.project-chip {
padding: 20rpx 12rpx;
border: 2rpx solid #1B7C5E;
border-radius: 8rpx;
font-size: 26rpx;
color: #666666;
color: #1B7C5E;
background-color: #FFFFFF;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
.project-btn.active {
.project-chip.active {
background-color: #1B7C5E;
color: #FFFFFF;
border-color: #1B7C5E;
font-weight: 500;
}
/* 评分统计 */
/* ==================== 评分统计 ==================== */
.score-stats {
padding: 20rpx 30rpx;
display: flex;
align-items: center;
}
.stats-label {
font-size: 28rpx;
color: #333333;
}
.stats-text {
color: #666666;
}
.stats-number {
.stats-value {
font-size: 32rpx;
color: #1B7C5E;
font-weight: 600;
margin-left: 8rpx;
}
/* 选手列表 */
/* ==================== 选手卡片 ==================== */
.player-list {
padding: 0 30rpx;
}
@@ -474,12 +494,12 @@ export default {
.player-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.player-header {
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -492,32 +512,31 @@ export default {
color: #333333;
}
/* ==================== 操作区域 ==================== */
.action-area {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10rpx;
align-items: center;
gap: 16rpx;
}
.total-score {
font-size: 26rpx;
color: #333333;
font-weight: 600;
}
.chief-actions {
.score-tag {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10rpx;
align-items: center;
padding: 12rpx 20rpx;
background-color: #F5F5F5;
border-radius: 8rpx;
border: 2rpx solid #E5E5E5;
}
.chief-hint {
font-size: 22rpx;
color: #FF4D6A;
text-align: right;
line-height: 1.5;
max-width: 400rpx;
.tag-label {
font-size: 24rpx;
color: #666666;
}
.tag-value {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
.modify-btn {
@@ -527,21 +546,50 @@ export default {
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
border: none;
line-height: 1.4;
}
.modify-btn:active {
opacity: 0.9;
}
.player-info {
/* ==================== 选手详情 ==================== */
.player-details {
display: flex;
flex-direction: column;
gap: 12rpx;
gap: 8rpx;
}
.info-item {
.detail-row {
line-height: 1.6;
}
.detail-text {
font-size: 26rpx;
color: #666666;
line-height: 1.5;
}
/* ==================== 加载状态 ==================== */
.loading-status {
padding: 30rpx 0;
text-align: center;
}
.loading-text {
font-size: 26rpx;
color: #1B7C5E;
}
.no-more-text {
font-size: 26rpx;
color: #999999;
}
/* ==================== 空状态 ==================== */
.empty-state {
padding: 100rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
</style>

View File

@@ -4,75 +4,111 @@
<view class="nav-bar">
<view class="nav-title">评分系统</view>
<view class="nav-right">
<view class="icon-menu">···</view>
<view class="icon-close"></view>
<view class="nav-dots">···</view>
<view class="nav-circle"></view>
</view>
</view>
<!-- 比赛信息 -->
<view class="match-info">
<view class="match-title">{{ matchInfo.name }}</view>
<view class="match-time">比赛时间{{ matchInfo.time }}</view>
<view class="match-time">比赛时间{{ formatDateTime(matchInfo.time) }}</view>
</view>
<!-- 场地和项目选择 -->
<view class="venue-section">
<!-- 场地和项目卡片 -->
<view class="venue-card">
<!-- 场地标题行 -->
<view class="venue-header">
<view class="venue-tab active">{{ venueInfo.name }}</view>
<view class="venue-name">{{ venueInfo.name }}</view>
<view class="refresh-link" @click="handleRefresh">刷新</view>
</view>
<view class="project-section">
<view
class="project-btn"
:class="{ active: index === currentProjectIndex }"
v-for="(project, index) in projects"
:key="project.projectId"
@click="switchProject(index)"
>
{{ project.projectName }}
<!-- 项目筛选 -->
<view class="project-row">
<view class="project-grid">
<view
class="project-chip"
:class="{ active: index === currentProjectIndex }"
v-for="(project, index) in projects"
:key="project.projectId"
@click="switchProject(index)"
>
{{ project.projectName }}
</view>
</view>
</view>
</view>
<!-- 评分统计 -->
<!-- 评分统计 -->
<view class="score-stats">
<text class="stats-text">已评分</text>
<text class="stats-number">{{ scoredCount }}/{{ totalCount }}</text>
<text class="stats-label">已评分</text>
<text class="stats-value">{{ scoredCount }}/{{ totalCount }}</text>
</view>
<!-- 选手列表 -->
<view class="player-list">
<!-- 遍历选手列表 -->
<!-- 选手卡片 -->
<view
class="player-card"
v-for="player in players"
:key="player.athleteId"
>
<view class="player-header">
<view class="player-name">{{ player.name }}</view>
<!-- 已评分显示我的评分和总分 -->
<view class="player-scores" v-if="player.scored">
<text class="my-score">我的评分{{ player.myScore }}</text>
<text class="total-score">{{ player.totalScore }}</text>
<!-- 已评分状态 -->
<template v-if="player.scored">
<view class="card-header">
<view class="player-name">{{ player.name }}</view>
<view class="score-tags">
<view class="score-tag">
<text class="tag-label">我的评</text>
<text class="tag-value">{{ player.myScore }}</text>
</view>
<!-- 总分只有所有裁判都评分完成后才显示 -->
<view class="score-tag" v-if="player.scoringComplete">
<text class="tag-label">总分</text>
<text class="tag-value">{{ formatScore(player.totalScore) }}</text>
</view>
<view class="score-tag waiting" v-else>
<text class="tag-label">总分</text>
<text class="tag-value">评分中({{ player.scoredJudgeCount || 0 }}/{{ player.requiredJudgeCount || "?" }})</text>
</view>
</view>
</view>
</template>
<!-- 未评分显示评分按钮 -->
<button
class="score-btn"
v-else
@click="goToScoreDetail(player)"
>
评分
</button>
</view>
<!-- 未评分状态 -->
<template v-else>
<view class="card-header">
<view class="player-name">{{ player.name }}</view>
<view class="action-row">
<button class="score-btn" @click="goToScoreDetail(player)">评分</button>
</view>
</view>
</template>
<view class="player-info">
<view class="info-item">身份证{{ player.idCard }}</view>
<view class="info-item">队伍{{ player.team }}</view>
<view class="info-item">编号{{ player.number }}</view>
<!-- 选手详细信息 -->
<view class="player-details">
<view class="detail-row">
<text class="detail-text">身份证{{ player.idCard }}</text>
</view>
<view class="detail-row">
<text class="detail-text">队伍{{ player.team }}</text>
</view>
<view class="detail-row">
<text class="detail-text">编号{{ player.number }}</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-status" v-if="players.length > 0">
<view v-if="isLoading" class="loading-text">加载中...</view>
<view v-else-if="!hasMore" class="no-more-text"> 没有更多了 </view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!isLoading && players.length === 0">
<text class="empty-text">暂无选手数据</text>
</view>
</view>
</view>
</template>
@@ -97,137 +133,289 @@ export default {
name: ''
},
judgeId: '',
projects: [], // 所有分配的项目列表
currentProjectIndex: 0, // 当前选中的项目索引
projects: [],
currentProjectIndex: 0,
players: [],
scoredCount: 0,
totalCount: 0
totalCount: 0,
pagination: {
current: 1,
size: 10,
total: 0
},
isLoading: false,
hasMore: true,
isFirstLoad: true
}
},
async onLoad() {
// 获取全局数据
const app = getApp()
const globalData = app.globalData || {}
// 加载比赛信息
if (!globalData.judgeId || !globalData.token) {
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 1500
})
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' })
}, 1500)
return
}
this.matchInfo = {
name: globalData.matchName || '比赛名称',
time: globalData.matchTime || '比赛时间'
time: globalData.matchTime || ''
}
// 加载场地信息
this.venueInfo = {
id: globalData.venueId,
name: globalData.venueName || '场地'
name: globalData.venueName || '第一场地'
}
// 加载项目列表
this.projects = globalData.projects || []
this.currentProjectIndex = globalData.currentProjectIndex || 0
// 设置当前项目信息
this.updateCurrentProject()
this.judgeId = globalData.judgeId
// 调试信息
if (config.debug) {
console.log('评分列表页加载:', {
judgeId: this.judgeId,
venueId: this.venueInfo.id,
projectId: this.projectInfo.id,
projectsCount: this.projects.length
projectId: this.projectInfo.id
})
}
// 加载选手列表
await this.loadPlayers()
await this.loadPlayers(true)
this.isFirstLoad = false
},
async onShow() {
// 从评分详情页返回时刷新数据
if (!this.isFirstLoad) {
if (config.debug) {
console.log('页面显示,刷新数据')
}
await this.loadPlayers(true)
}
},
async onPullDownRefresh() {
await this.loadPlayers(true)
uni.stopPullDownRefresh()
},
async onReachBottom() {
if (this.hasMore && !this.isLoading) {
await this.loadMore()
}
},
methods: {
async loadPlayers() {
formatDateTime(dateTimeStr) {
if (!dateTimeStr) return ''
try {
uni.showLoading({
title: '加载中...',
mask: true
})
const date = new Date(dateTimeStr)
if (isNaN(date.getTime())) return dateTimeStr
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hours = date.getHours()
const minutes = date.getMinutes()
const paddedMinutes = minutes < 10 ? '0' + minutes : minutes
return year + '年' + month + '月' + day + '日 ' + hours + ':' + paddedMinutes
} catch (error) {
return dateTimeStr
}
},
// 🔥 关键改动:使用 dataAdapter 获取选手列表
// Mock模式调用 mock/athlete.js 的 getMyAthletes 函数
// API模式调用 api/athlete.js 的 getMyAthletes 函数GET /api/mini/athletes
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
},
// 构建请求参数
// 优先使用 matchCode比赛编码这样后端可以根据邀请码关联查询
/**
* 计算选手总分
* 规则:所有裁判评分完成后,去掉一个最高分和一个最低分,取剩余分数的平均值
* @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
if (totalJudges === 0 || scoredCount < totalJudges) {
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
}
const totalJudges = player.totalJudges || 0
return totalJudges > 0 && player.judgeScores.length >= totalJudges
},
/**
* 获取选手的显示总分
* @param {Object} player - 选手对象
* @returns {String} 格式化后的总分或 '--'
*/
getDisplayTotalScore(player) {
const score = this.calculateTotalScore(player)
if (score === null) {
return '--'
}
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
},
async handleRefresh() {
if (this.isLoading) return
uni.showToast({ title: '刷新中...', icon: 'loading', duration: 1000 })
await this.loadPlayers(true)
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1000 })
},
async loadPlayers(refresh = false) {
if (this.isLoading) return
try {
this.isLoading = true
if (refresh) {
this.pagination.current = 1
this.hasMore = true
}
if (refresh && this.isFirstLoad) {
uni.showLoading({ title: '加载中...', mask: true })
}
const app = getApp()
const globalData = app.globalData || {}
const params = {
// 方案1使用比赛编码推荐后端可以根据邀请码关联
matchCode: globalData.matchCode,
// 方案2使用具体的ID作为备选
judgeId: this.judgeId,
venueId: this.venueInfo.id,
projectId: this.projectInfo.id
projectId: this.projectInfo.id,
current: this.pagination.current,
size: this.pagination.size
}
// 移除无效参数
Object.keys(params).forEach(key => {
if (params[key] === undefined || params[key] === null || params[key] === '') {
delete params[key]
}
})
// 调试信息
if (config.debug) {
console.log('请求运动员列表参数:', params)
console.log('请求选手列表参数:', params)
}
const response = await dataAdapter.getData('getMyAthletes', params)
uni.hideLoading()
// 保存选手列表
this.players = response.data || []
// 计算评分统计
this.totalCount = this.players.length
this.scoredCount = this.players.filter(p => p.scored).length
// 调试信息
if (config.debug) {
console.log('选手列表加载成功:', {
total: this.totalCount,
console.log('选手列表响应:', response)
}
if (refresh && this.isFirstLoad) {
uni.hideLoading()
}
const responseData = response.data || {}
const records = responseData.records || response.data || []
const total = responseData.total || records.length
this.pagination.total = total
this.totalCount = total
if (refresh) {
this.players = records
} else {
this.players = [...this.players, ...records]
}
this.scoredCount = this.players.filter(p => p.scored).length
this.hasMore = this.players.length < total
if (config.debug) {
console.log('选手列表处理结果:', {
total: total,
loaded: this.players.length,
scored: this.scoredCount,
players: this.players
})
}
} catch (error) {
uni.hideLoading()
console.error('加载选手列表失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
uni.showToast({ title: error.message || '加载失败', icon: 'none' })
} finally {
this.isLoading = false
}
},
goToScoreDetail(player) {
// 保存当前选手信息到全局数据
const app = getApp()
app.globalData.currentAthlete = player
uni.navigateTo({
url: '/pages/score-detail/score-detail'
})
async loadMore() {
if (!this.hasMore || this.isLoading) return
this.pagination.current++
await this.loadPlayers(false)
},
goToScoreDetail(player) {
const app = getApp()
app.globalData.currentAthlete = player
uni.navigateTo({ url: '/pages/score-detail/score-detail' })
},
/**
* 更新当前项目信息
*/
updateCurrentProject() {
const currentProject = this.projects[this.currentProjectIndex] || {}
this.projectInfo = {
@@ -236,37 +424,13 @@ export default {
}
},
/**
* 切换项目
* @param {Number} index - 项目索引
*/
async switchProject(index) {
// 如果点击的是当前项目,不做处理
if (index === this.currentProjectIndex) {
return
}
// 更新当前项目索引
if (index === this.currentProjectIndex) return
this.currentProjectIndex = index
// 更新全局数据中的项目索引
const app = getApp()
app.globalData.currentProjectIndex = index
// 更新当前项目信息
this.updateCurrentProject()
// 调试信息
if (config.debug) {
console.log('切换项目:', {
index: index,
projectId: this.projectInfo.id,
projectName: this.projectInfo.name
})
}
// 重新加载选手列表
await this.loadPlayers()
await this.loadPlayers(true)
}
}
}
@@ -279,7 +443,7 @@ export default {
padding-bottom: 40rpx;
}
/* 导航栏 */
/* ==================== 导航栏 ==================== */
.nav-bar {
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
@@ -294,7 +458,6 @@ export default {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
letter-spacing: 2rpx;
}
.nav-right {
@@ -302,24 +465,15 @@ export default {
right: 30rpx;
display: flex;
align-items: center;
gap: 30rpx;
gap: 20rpx;
}
.icon-menu,
.icon-close {
width: 60rpx;
height: 60rpx;
background-color: rgba(255, 255, 255, 0.25);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.nav-dots, .nav-circle {
font-size: 32rpx;
color: #FFFFFF;
font-weight: bold;
}
/* 比赛信息 */
/* ==================== 比赛信息 ==================== */
.match-info {
padding: 30rpx;
background-color: #F5F5F5;
@@ -328,121 +482,94 @@ export default {
.match-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
line-height: 1.6;
margin-bottom: 10rpx;
}
.tip-text {
font-size: 24rpx;
color: #FF4D6A;
margin-bottom: 10rpx;
color: #1B7C5E;
line-height: 1.5;
margin-bottom: 8rpx;
}
.match-time {
font-size: 28rpx;
color: #666666;
color: #333333;
}
/* 场地和项目区域 */
.venue-section {
/* ==================== 场地卡片 ==================== */
.venue-card {
margin: 0 30rpx 20rpx;
background-color: #FFFFFF;
margin: 20rpx 30rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.venue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
padding-bottom: 24rpx;
border-bottom: 4rpx solid #1B7C5E;
margin-bottom: 24rpx;
}
.venue-tab {
.venue-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
position: relative;
}
.venue-tab.active::after {
content: '';
position: absolute;
bottom: -24rpx;
left: 0;
right: 0;
height: 4rpx;
background-color: #1B7C5E;
.refresh-link {
font-size: 26rpx;
color: #4A90D9;
}
.refresh-hint {
font-size: 24rpx;
color: #FF4D6A;
}
.project-section {
.project-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20rpx;
flex-direction: column;
}
.project-btn {
padding: 20rpx 40rpx;
background-color: #FFFFFF;
.project-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.project-chip {
padding: 20rpx 12rpx;
border: 2rpx solid #1B7C5E;
border-radius: 8rpx;
font-size: 28rpx;
font-size: 26rpx;
color: #1B7C5E;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
background-color: #FFFFFF;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-btn:active {
opacity: 0.7;
}
.project-btn.active {
.project-chip.active {
background-color: #1B7C5E;
color: #FFFFFF;
}
.project-tip {
font-size: 22rpx;
color: #FF4D6A;
flex: 1;
margin-left: 20rpx;
line-height: 1.5;
}
/* 评分统计 */
/* ==================== 评分统计 ==================== */
.score-stats {
padding: 20rpx 30rpx;
display: flex;
align-items: center;
}
.stats-label {
font-size: 28rpx;
color: #333333;
}
.stats-text {
color: #666666;
}
.stats-number {
.stats-value {
font-size: 32rpx;
color: #1B7C5E;
font-weight: 600;
margin-left: 8rpx;
}
.warning-tip {
padding: 0 30rpx 20rpx;
font-size: 24rpx;
color: #FF4D6A;
}
/* 选手列表 */
/* ==================== 选手卡片 ==================== */
.player-list {
padding: 0 30rpx;
}
@@ -450,14 +577,14 @@ export default {
.player-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.player-header {
.card-header {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20rpx;
}
@@ -468,34 +595,48 @@ export default {
color: #333333;
}
.player-scores {
/* ==================== 已评分标签 ==================== */
.score-tags {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
gap: 16rpx;
flex-wrap: wrap;
}
.my-score {
font-size: 26rpx;
.score-tag {
display: flex;
align-items: center;
padding: 12rpx 20rpx;
background-color: #F5F5F5;
border-radius: 8rpx;
border: 2rpx solid #E5E5E5;
}
.tag-label {
font-size: 24rpx;
color: #666666;
}
.total-score {
font-size: 26rpx;
.tag-value {
font-size: 28rpx;
color: #333333;
font-weight: 600;
font-weight: 500;
}
.action-area {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
/* ==================== 等待中状态 ==================== */
.score-tag.waiting {
background-color: #FFF7E6;
border-color: #FFD591;
}
.chief-hint {
.score-tag.waiting .tag-value {
color: #FA8C16;
font-size: 24rpx;
color: #FF4D6A;
}
/* ==================== 未评分操作 ==================== */
.action-row {
display: flex;
align-items: center;
}
.score-btn {
@@ -505,21 +646,50 @@ export default {
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
border: none;
line-height: 1.4;
}
.score-btn:active {
opacity: 0.9;
}
.player-info {
/* ==================== 选手详情 ==================== */
.player-details {
display: flex;
flex-direction: column;
gap: 12rpx;
gap: 8rpx;
}
.info-item {
.detail-row {
line-height: 1.6;
}
.detail-text {
font-size: 26rpx;
color: #666666;
line-height: 1.5;
}
/* ==================== 加载状态 ==================== */
.loading-status {
padding: 30rpx 0;
text-align: center;
}
.loading-text {
font-size: 26rpx;
color: #1B7C5E;
}
.no-more-text {
font-size: 26rpx;
color: #999999;
}
/* ==================== 空状态 ==================== */
.empty-state {
padding: 100rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
</style>

View File

@@ -20,10 +20,21 @@ module.exports = {
// 开发服务器配置
devServer: {
port: 8080,
host: '0.0.0.0',
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/mini': {
target: 'http://localhost:8123',
changeOrigin: true
},
'/martial': {
target: 'http://localhost:8123',
changeOrigin: true
}
}
},