Compare commits

...

36 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
6f3b8db273 fix bugs 2025-12-17 09:23:27 +08:00
eaac987a5c Merge branch 'main' of git.waypeak.work:martial/martial-admin-mini
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-14 17:38:44 +08:00
b7b8947939 fix bugs 2025-12-14 17:38:35 +08:00
Developer
76fd02661c chore: 测试修复后的构建
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-13 23:25:23 +08:00
Developer
8abfd386fd chore: 测试 trusted 仓库构建
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-13 23:23:20 +08:00
Developer
eebbb4fbce chore: 重新触发构建
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-13 23:21:36 +08:00
Developer
137139b973 chore: 触发重新构建
Some checks failed
continuous-integration/drone/push Build was killed
2025-12-13 23:20:14 +08:00
Developer
39bc88ce6d fix: 升级 Node 版本到 18,修复 sass 兼容性问题
Some checks failed
continuous-integration/drone/push Build is failing
🤖 Generated with Claude Code
2025-12-13 23:14:54 +08:00
Developer
8c56251d72 fix: add custom index.html template with CSS link
All checks were successful
continuous-integration/drone/push Build is passing
- 添加 public/index.html 模板文件,确保 CSS 正确引入
- 更新 manifest.json 添加 template 配置
- 完善 vue.config.js 配置

参考 martial-mini 项目的修复方案
2025-12-13 13:51:44 +08:00
Developer
cf3f0bc13b chore: trigger CI build
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-13 13:31:50 +08:00
Developer
afaaf09a61 chore: trigger CI
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-13 13:20:41 +08:00
Developer
cb3f70966e chore: trigger CI build
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-13 13:07:52 +08:00
Developer
4deed1199d fix: 修复CSS样式未正确引入的问题
- 添加 vue.config.js 确保 CSS 被正确提取
- 修正 .drone.yml 中构建输出路径从 dist/dev/h5 改为 dist/build/h5

🤖 Generated with Claude Code
2025-12-13 12:45:02 +08:00
Developer
a0f7a6a757 fix: add CSS template and fix drone build config 2025-12-13 11:39:20 +08:00
Developer
6ea1c0ca8e Fix: use local vue-cli-service
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-12 19:40:14 +08:00
Developer
7f304e012a Fix: use npx uni build
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-12 19:36:33 +08:00
Developer
1c3332aea9 Trigger build
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-12 19:32:58 +08:00
Developer
4d492f3fea Fix: use scp instead of docker build
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-12 19:12:33 +08:00
Developer
076ec9b7c3 Retrigger CI/CD
Some checks failed
continuous-integration/drone/push Build encountered an error
2025-12-12 19:07:12 +08:00
Developer
e49f9b3de9 Trigger CI/CD
Some checks failed
continuous-integration/drone/push Build encountered an error
2025-12-12 19:05:59 +08:00
Developer
92aa0cdf11 Add CI/CD config
Some checks failed
continuous-integration/drone/push Build encountered an error
2025-12-12 19:00:43 +08:00
5cc95ec72b fix bugs 2025-12-12 18:28:57 +08:00
51 changed files with 25418 additions and 239 deletions

View File

@@ -9,7 +9,8 @@
"Bash(git checkout:*)",
"Bash(ls:*)",
"Bash(done)",
"Bash(cat:*)"
"Bash(cat:*)",
"Bash(git push:*)"
],
"deny": [],
"ask": []

51
.drone.yml Normal file
View File

@@ -0,0 +1,51 @@
kind: pipeline
type: docker
name: martial-admin-mini
trigger:
branch:
- main
- master
steps:
- name: build
image: node:18-alpine
environment:
NODE_OPTIONS: --max-old-space-size=4096
commands:
- npm install --legacy-peer-deps
- npm run build:h5
- ls -la dist/build/h5/
- name: clean
image: appleboy/drone-ssh
settings:
host: 154.30.6.21
username: root
key:
from_secret: ssh_key
script:
- rm -rf /var/www/martial-admin-mini/*
- name: deploy
image: appleboy/drone-scp
settings:
host: 154.30.6.21
username: root
key:
from_secret: ssh_key
source: dist/build/h5/*
target: /var/www/martial-admin-mini
strip_components: 3
- name: restart
image: appleboy/drone-ssh
settings:
host: 154.30.6.21
username: root
key:
from_secret: ssh_key
script:
- docker stop martial-admin-mini || true
- docker rm martial-admin-mini || true
- docker run -d --name martial-admin-mini -p 8082:80 -v /var/www/martial-admin-mini:/usr/share/nginx/html:ro nginx:alpine

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>

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:16-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build:h5
FROM nginx:alpine
COPY --from=builder /app/dist/build/h5 /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

206
H5部署说明.md Normal file
View File

@@ -0,0 +1,206 @@
# H5 版本部署说明
## 问题描述
编译后的 H5 版本在服务器上显示样式丢失(乱码),但文字内容正常显示。
## 原因分析
1. **静态资源路径问题**CSS 和 JS 文件路径不正确
2. **服务器配置问题**Nginx 或 Apache 配置不正确
3. **MIME 类型问题**CSS 文件被当作文本文件加载
## 解决方案
### 方案1检查文件结构推荐
编译后的文件结构应该是:
```
dist/build/h5/
├── index.html
└── static/
├── index.css
└── js/
├── chunk-vendors.xxx.js
└── index.xxx.js
```
**部署步骤:**
1. 将整个 `dist/build/h5` 目录上传到服务器
2. 确保目录结构完整,不要只上传 `index.html`
3. 访问 `http://your-domain/index.html`
### 方案2Nginx 配置
如果使用 Nginx确保配置正确
```nginx
server {
listen 80;
server_name your-domain.com;
# 根目录指向 h5 目录
root /path/to/dist/build/h5;
index index.html;
# 确保静态资源正确加载
location /static/ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# SPA 路由支持
location / {
try_files $uri $uri/ /index.html;
}
# 确保 CSS 文件 MIME 类型正确
location ~* \.css$ {
add_header Content-Type text/css;
}
# 确保 JS 文件 MIME 类型正确
location ~* \.js$ {
add_header Content-Type application/javascript;
}
}
```
### 方案3Apache 配置
如果使用 Apache`h5` 目录下创建 `.htaccess` 文件:
```apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
# 设置正确的 MIME 类型
<IfModule mod_mime.c>
AddType text/css .css
AddType application/javascript .js
</IfModule>
# 启用 Gzip 压缩
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript
</IfModule>
```
### 方案4修改 publicPath如果部署在子目录
如果部署在子目录(如 `http://your-domain.com/martial/`),需要修改 `vue.config.js`
```javascript
module.exports = {
// 修改为子目录路径
publicPath: process.env.NODE_ENV === 'production' ? '/martial/' : '/',
// 其他配置...
}
```
然后重新编译:
```bash
npm run build:h5
```
### 方案5检查浏览器控制台
打开浏览器开发者工具F12查看
1. **Network 标签页**
- 检查 CSS 文件是否加载成功(状态码应该是 200
- 检查文件路径是否正确
- 检查 Content-Type 是否为 `text/css`
2. **Console 标签页**
- 查看是否有 404 错误
- 查看是否有 CORS 错误
3. **常见错误信息:**
```
Failed to load resource: net::ERR_FILE_NOT_FOUND
→ 文件路径不正确,检查 publicPath 配置
Refused to apply style from '...' because its MIME type ('text/html') is not a supported stylesheet MIME type
→ CSS 文件被当作 HTML 加载,检查服务器配置
```
## 快速诊断步骤
1. **本地测试**
```bash
# 在 dist/build/h5 目录下启动本地服务器
cd dist/build/h5
python -m http.server 8000
# 或使用 Node.js
npx http-server -p 8000
```
访问 `http://localhost:8000`,如果本地正常,说明是服务器配置问题。
2. **检查文件是否上传完整**
```bash
# 在服务器上检查文件
ls -la /path/to/h5/static/
# 应该看到 index.css 和 js 目录
```
3. **检查文件权限**
```bash
# 确保文件可读
chmod -R 755 /path/to/h5/
```
4. **检查 CSS 文件内容**
```bash
# 查看 CSS 文件前几行
head -n 20 /path/to/h5/static/index.css
# 应该看到 CSS 代码,而不是 HTML 或错误信息
```
## 重新编译
如果修改了配置,需要重新编译:
```bash
# 清理旧的编译文件
rm -rf dist/build/h5
# 重新编译
npm run build:h5
# 检查编译结果
ls -la dist/build/h5/static/
```
## 常见问题
### Q1: 样式完全丢失,只显示纯文本
**A:** CSS 文件没有加载,检查:
- 文件路径是否正确
- 服务器配置是否正确
- MIME 类型是否正确
### Q2: 部分样式丢失
**A:** CSS 文件加载了但不完整,检查:
- CSS 文件是否完整上传
- 是否有 CSS 压缩错误
- 浏览器兼容性问题
### Q3: 本地正常,服务器异常
**A:** 服务器配置问题,检查:
- publicPath 配置
- Nginx/Apache 配置
- 文件权限
## 联系支持
如果以上方案都无法解决问题,请提供:
1. 浏览器控制台截图Network 和 Console
2. 服务器配置文件
3. 部署的完整路径
4. 访问的 URL

View File

@@ -6,21 +6,27 @@
import request from '@/utils/request.js'
/**
* 获取我的选手列表(普通评委
* 获取选手列表(根据裁判类型返回不同数据
* @param {Object} params
* @param {String} params.judgeId - 评委ID
* @param {String} params.venueId - 场地ID
* @param {String} params.projectId - 项目ID
* @param {Number} params.refereeType - 裁判类型1-裁判长, 2-普通裁判)
* @param {String} params.venueId - 场地ID可选
* @param {String} params.projectId - 项目ID可选
* @returns {Promise}
*
* 注意:此接口需要后端实现
* 建议路径: GET /api/mini/athletes
* 普通裁判:返回待评分的选手列表
* 裁判长:返回已有评分的选手列表
*
* 后端路径: GET /api/mini/score/athletes
*/
export function getMyAthletes(params) {
return request({
url: '/api/mini/athletes',
url: '/mini/score/athletes',
method: 'GET',
params: params, // GET 请求使用 params
params: {
...params,
size: 200 // 确保获取所有选手
},
showLoading: true
})
}
@@ -33,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: '/api/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
})
}
@@ -87,56 +103,3 @@ export default {
getVenues,
getProjects
}
/**
* 后端接口规范(待实现):
*
* GET /api/mini/athletes
*
* 请求参数:
* {
* "judgeId": "456",
* "venueId": "1",
* "projectId": "5"
* }
*
* 响应:
* {
* "code": 200,
* "success": true,
* "msg": "操作成功",
* "data": [
* {
* "athleteId": "1",
* "name": "张三",
* "idCard": "123456789000000000",
* "team": "少林寺武术大学院",
* "number": "123-4567898275",
* "myScore": 8.906,
* "totalScore": 8.907,
* "scored": true,
* "scoreTime": "2025-06-25 09:15:00"
* }
* ]
* }
*
* SQL示例
* SELECT
* a.id AS athleteId,
* a.player_name AS name,
* a.id_card AS idCard,
* a.team_name AS team,
* a.player_no AS number,
* a.total_score AS totalScore,
* s.score AS myScore,
* CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END AS scored,
* s.score_time AS scoreTime
* FROM martial_athlete a
* LEFT JOIN martial_score s
* ON a.id = s.athlete_id
* AND s.judge_id = #{judgeId}
* WHERE a.venue_id = #{venueId}
* AND a.project_id = #{projectId}
* AND a.is_deleted = 0
* ORDER BY a.order_num ASC
*/

View File

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

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
})
@@ -82,11 +82,33 @@ export function modifyScore(data) {
})
}
/**
* 获取选手列表
* @param {Object} params
* @param {String} params.judgeId - 裁判ID
* @param {Number} params.refereeType - 裁判类型1-裁判长, 2-普通裁判)
* @param {String} params.projectId - 项目ID可选
* @param {String} params.venueId - 场地ID可选
* @returns {Promise}
*
* 注意:此接口需要后端实现
* 建议路径: GET /api/mini/score/athletes
*/
export function getAthletes(params) {
return request({
url: '/api/mini/score/athletes',
method: 'GET',
params,
showLoading: true
})
}
export default {
getDeductions,
submitScore,
getScoreDetail,
modifyScore
modifyScore,
getAthletes
}
/**

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>

View File

@@ -58,6 +58,7 @@
"usingComponents" : true
},
"h5" : {
"template": "index.html",
"title" : "武术评分系统",
"router" : {
"mode" : "hash",

View File

@@ -4,48 +4,77 @@
*/
/**
* 获取我的选手列表(普通评委
* 获取选手列表(根据裁判类型返回不同数据
* @param {Object} params
* @param {String} params.judgeId - 评委ID
* @param {String} params.venueId - 场地ID
* @param {String} params.projectId - 项目ID
* @returns {Array} 选手列表(带评分状态
* @param {Number} params.refereeType - 裁判类型1-裁判长, 2-普通裁判)
* @param {String} params.venueId - 场地ID可选
* @param {String} params.projectId - 项目ID可选
* @returns {Array} 选手列表
*/
export function getMyAthletes(params) {
// 模拟3个选手数据
const { refereeType } = params
// 裁判长:返回已有评分的选手
if (refereeType === 1) {
return [
{
athleteId: '1',
athleteId: 1,
name: '张三',
idCard: '123456789000000000',
team: '少林寺武术大学院',
number: '123-4567898275',
myScore: 8.906, // 我的评分
totalScore: 8.907, // 总分
scored: true, // 已评分
scoreTime: '2025-06-25 09:15:00'
team: '少林寺武术大学院',
projectName: '女子组长拳',
orderNum: 1,
totalScore: 8.907,
scoredJudgeCount: 6,
competitionStatus: 2
},
{
athleteId: '2',
athleteId: 2,
name: '李四',
idCard: '123456789000000001',
team: '武当山武术学院',
number: '123-4567898276',
myScore: 8.901,
team: '武当山武术学院',
projectName: '女子组长拳',
orderNum: 2,
totalScore: 8.902,
scored: true,
scoreTime: '2025-06-25 09:20:00'
scoredJudgeCount: 6,
competitionStatus: 2
},
{
athleteId: '3',
athleteId: 4,
name: '赵六',
number: '123-4567898278',
team: '华山武术学院',
projectName: '女子组长拳',
orderNum: 4,
totalScore: 8.899,
scoredJudgeCount: 5,
competitionStatus: 2
}
]
}
// 普通裁判:返回待评分的选手
return [
{
athleteId: 3,
name: '王五',
idCard: '123456789000000002',
team: '峨眉派武术学校',
number: '123-4567898277',
myScore: null, // 未评分
totalScore: null,
scored: false,
scoreTime: null
projectName: '女子组长拳',
orderNum: 3,
competitionStatus: 0
},
{
athleteId: 5,
name: '孙七',
idCard: '123456789000000004',
team: '崆峒派武术学校',
number: '123-4567898279',
projectName: '女子组长拳',
orderNum: 5,
competitionStatus: 0
}
]
}

View File

@@ -23,8 +23,8 @@ export function login(params) {
// 返回Mock登录数据
return {
token: 'mock_token_' + Date.now(),
userRole: role, // 'pub' 或 'admin'
matchId: '123',
refereeType: role === 'pub' ? 2 : 1, // 1-裁判长, 2-普通裁判
matchId: matchCode || '200', // 使用传入的比赛编码默认200
matchName: '2025年全国武术散打锦标赛暨第十七届世界武术锦标赛选拔赛',
matchTime: '2025年6月25日 9:00',
judgeId: '456',

View File

@@ -7,20 +7,26 @@
* 获取扣分项列表
* @param {Object} params
* @param {String} params.projectId - 项目ID
* @returns {Array} 扣分项列表
* @returns {Object} 扣分项列表包装在records中与后端API格式一致
*/
export function getDeductions(params) {
// 模拟8个扣分项
return [
{ id: '1', text: '扣分项描述', score: -0.1, checked: false },
{ id: '2', text: '扣分项描述', score: -0.1, checked: false },
{ id: '3', text: '扣分项描述', score: -0.1, checked: false },
{ id: '4', text: '扣分项描述', score: -0.1, checked: false },
{ id: '5', text: '扣分项描述', score: -0.1, checked: false },
{ id: '6', text: '扣分项描述', score: -0.1, checked: false },
{ id: '7', text: '扣分项描述', score: -0.1, checked: false },
{ id: '8', text: '扣分项描述', score: -0.1, checked: false }
]
// 模拟8个扣分项字段名与后端API保持一致
return {
records: [
{ id: '1', itemName: '动作不规范', deductionPoint: '0.1' },
{ id: '2', itemName: '节奏不稳', deductionPoint: '0.1' },
{ id: '3', itemName: '力度不足', deductionPoint: '0.1' },
{ id: '4', itemName: '平衡失误', deductionPoint: '0.1' },
{ id: '5', itemName: '器械掉落', deductionPoint: '0.2' },
{ id: '6', itemName: '出界', deductionPoint: '0.1' },
{ id: '7', itemName: '动作遗漏', deductionPoint: '0.2' },
{ id: '8', itemName: '其他失误', deductionPoint: '0.1' }
],
total: 8,
size: 100,
current: 1,
pages: 1
}
}
/**

18803
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,5 +3,52 @@
"name": "martial-admin-mini",
"version": "1.0.0",
"description": "武术比赛评分系统",
"main": "main.js"
"main": "main.js",
"scripts": {
"dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service serve",
"build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service build"
},
"dependencies": {
"@dcloudio/uni-app": "^2.0.2-3081220230817001",
"@dcloudio/uni-cli-i18n": "^2.0.2-3081220230817001",
"@dcloudio/uni-h5": "^2.0.2-3081220230817001",
"@dcloudio/uni-i18n": "^2.0.2-3081220230817001",
"@dcloudio/uni-migration": "^2.0.2-3081220230817001",
"autoprefixer": "^9.8.8",
"cache-loader": "^4.1.0",
"copy-webpack-plugin": "^6.4.1",
"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.32.13",
"sass-loader": "^10.5.2",
"thread-loader": "^2.1.3",
"url-loader": "^4.1.1",
"vue": "^2.6.14",
"vue-loader": "^15.11.1",
"webpack": "^4.47.0"
},
"devDependencies": {
"@dcloudio/uni-cli-shared": "^2.0.2-3081220230817001",
"@dcloudio/uni-template-compiler": "^2.0.2-3081220230817001",
"@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.2-3081220230817001",
"@dcloudio/vue-cli-plugin-uni": "^2.0.2-3081220230817001",
"@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.2-3081220230817001",
"@dcloudio/webpack-uni-mp-loader": "^2.0.2-3081220230817001",
"@dcloudio/webpack-uni-pages-loader": "^2.0.2-3081220230817001",
"@vue/cli-plugin-babel": "~4.5.19",
"@vue/cli-service": "~4.5.19",
"babel-plugin-import": "^1.13.5",
"cross-env": "^7.0.3",
"mini-types": "*",
"postcss-comment": "^2.0.0",
"postcss-import": "^12.0.1",
"vue-template-compiler": "^2.6.14"
},
"browserslist": [
"Android >= 4.4",
"ios >= 9"
]
}

View File

@@ -105,6 +105,7 @@ export default {
const {
token,
userRole,
refereeType,
matchId,
matchName,
matchTime,
@@ -121,6 +122,7 @@ export default {
// 保存用户信息到全局数据
getApp().globalData = {
userRole, // 'pub' 或 'admin'
refereeType, // 1-裁判长, 2-普通裁判
matchCode: this.matchCode,
matchId,
matchName,
@@ -129,7 +131,7 @@ export default {
judgeName,
venueId, // 普通评委有场地裁判长为null
venueName,
projects, // 分配的项目列表
projects, // 分配的项目列表(从登录接口返回)
currentProjectIndex: 0 // 当前选中的项目索引
}

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

@@ -107,6 +107,8 @@ export default {
},
judgeId: '',
projectId: '',
competitionId: '',
venueId: '',
currentScore: 8.000,
note: '',
minScore: 5.0,
@@ -137,10 +139,9 @@ export default {
// 加载评委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) {
@@ -148,6 +149,8 @@ export default {
athlete: this.player,
judgeId: this.judgeId,
projectId: this.projectId,
competitionId: this.competitionId,
venueId: this.venueId,
initialScore: this.currentScore
})
}
@@ -166,9 +169,12 @@ export default {
projectId: this.projectId
})
// 为每个扣分项添加 checked 状态
this.deductions = (response.data || []).map(item => ({
...item,
// 为每个扣分项添加 checked 状态,并映射字段名
const records = response.data?.records || []
this.deductions = records.map(item => ({
deductionId: item.id,
deductionName: item.itemName,
deductionScore: parseFloat(item.deductionPoint || 0),
checked: false
}))
@@ -187,7 +193,20 @@ 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() {
@@ -216,14 +235,27 @@ export default {
return
}
// 收集选中的扣分项
// 验证必需字段
if (!this.competitionId) {
uni.showToast({
title: '缺少比赛ID请重新登录',
icon: 'none'
})
return
}
if (!this.projectId) {
uni.showToast({
title: '缺少项目ID请返回重新选择',
icon: 'none'
})
return
}
// 收集选中的扣分项ID
const selectedDeductions = this.deductions
.filter(item => item.checked)
.map(item => ({
deductionId: item.deductionId,
deductionName: item.deductionName,
deductionScore: item.deductionScore
}))
.map(item => item.deductionId)
try {
uni.showLoading({
@@ -231,16 +263,27 @@ export default {
mask: true
})
// 🔥 关键改动:使用 dataAdapter 提交评分
// Mock模式调用 mock/score.js 的 submitScore 函数
// API模式调用 api/score.js 的 submitScore 函数POST /martial/score/submit
const response = await dataAdapter.getData('submitScore', {
// 准备提交数据
const submitData = {
athleteId: this.player.athleteId,
judgeId: this.judgeId,
projectId: this.projectId,
competitionId: this.competitionId,
venueId: this.venueId,
score: this.currentScore,
deductions: selectedDeductions,
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()
@@ -301,12 +344,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 {

View File

@@ -68,13 +68,20 @@
<view class="player-header">
<view class="player-name">{{ player.name }}</view>
<!-- 动作区域始终显示 -->
<view class="action-area">
<!-- 已评分显示总分和修改按钮 -->
<view class="action-area" v-if="player.totalScore">
<template v-if="player.scoringComplete && player.totalScore > 0">
<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>
</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

@@ -18,11 +18,27 @@
<!-- 场地和项目选择 -->
<view class="venue-section">
<view class="venue-header">
<view class="venue-tab active">{{ venueInfo.name }}</view>
<view
class="venue-tab"
:class="{ active: index === currentVenueIndex }"
v-for="(venue, index) in venues"
:key="venue.id"
@click="switchVenue(index)"
>
{{ venue.venueName }}
</view>
</view>
<view class="project-section">
<view class="project-btn active">{{ projectInfo.name }}</view>
<view
class="project-btn"
:class="{ active: index === currentProjectIndex }"
v-for="(project, index) in projects"
:key="project.id"
@click="switchProject(index)"
>
{{ project.projectName }}
</view>
</view>
</view>
@@ -39,28 +55,46 @@
class="player-card"
v-for="player in players"
:key="player.athleteId"
@click="handlePlayerClick(player)"
>
<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>
<!-- 裁判长显示总分和已评分裁判数 -->
<view class="player-scores" v-if="refereeType === 1">
<text class="total-score">
总分{{ player.scoringComplete ? player.totalScore : '评分中' }}
</text>
<text class="judge-count">
已评分{{ player.scoredJudgeCount || 0 }}/{{ player.requiredJudgeCount || 0 }}
</text>
</view>
<!-- 普通裁判根据评分状态显示不同内容 -->
<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="goToScoreDetail(player)"
@click.stop="goToScoreDetail(player)"
>
评分
</button>
</view>
</view>
<view class="player-info">
<view class="info-item">身份证{{ player.idCard }}</view>
<view class="info-item" v-if="player.idCard">身份证{{ player.idCard }}</view>
<view class="info-item">队伍{{ player.team }}</view>
<view class="info-item">编号{{ player.number }}</view>
</view>
@@ -89,6 +123,12 @@ export default {
name: ''
},
judgeId: '',
matchId: '',
refereeType: 2, // 裁判类型1-裁判长, 2-普通裁判)
venues: [], // 所有场地列表
currentVenueIndex: 0, // 当前选中的场地索引
projects: [], // 所有项目列表
currentProjectIndex: 0, // 当前选中的项目索引
players: [],
scoredCount: 0,
totalCount: 0
@@ -96,6 +136,7 @@ export default {
},
async onLoad() {
try {
// 获取全局数据
const app = getApp()
const globalData = app.globalData || {}
@@ -106,34 +147,85 @@ export default {
time: globalData.matchTime || '比赛时间'
}
// 加载场地信息
this.venueInfo = {
id: globalData.venueId,
name: globalData.venueName || '场地'
}
// 加载项目信息
const projects = globalData.projects || []
const currentIndex = globalData.currentProjectIndex || 0
const currentProject = projects[currentIndex] || {}
this.projectInfo = {
id: currentProject.projectId,
name: currentProject.projectName || '项目'
}
this.judgeId = globalData.judgeId
this.matchId = globalData.matchId || globalData.matchCode
this.refereeType = globalData.refereeType || 2 // 默认为普通裁判
// 调试信息
if (config.debug) {
console.log('初始化数据:', {
judgeId: this.judgeId,
matchId: this.matchId,
matchCode: globalData.matchCode,
refereeType: this.refereeType
})
}
// 检查必要参数
if (!this.matchId) {
throw new Error('缺少比赛ID请重新登录')
}
// 显示加载提示
uni.showLoading({
title: '加载中...',
mask: true
})
// 1. 先获取场地列表
const venuesResponse = await dataAdapter.getData('getVenues', {
competitionId: this.matchId
})
this.venues = venuesResponse.data?.records || []
this.currentVenueIndex = 0
// 设置当前场地信息使用第一条数据的ID
if (this.venues.length > 0) {
this.venueInfo = {
id: this.venues[0].id,
name: this.venues[0].name
}
}
// 2. 再获取项目列表
const projectsResponse = await dataAdapter.getData('getProjects', {
competitionId: this.matchId
})
this.projects = projectsResponse.data?.records || []
this.currentProjectIndex = 0
// 设置当前项目信息使用第一条数据的ID
if (this.projects.length > 0) {
this.projectInfo = {
id: this.projects[0].id,
name: this.projects[0].projectName
}
}
uni.hideLoading()
// 调试信息
if (config.debug) {
console.log('评分列表页加载:', {
judgeId: this.judgeId,
venueId: this.venueInfo.id,
projectId: this.projectInfo.id
projectId: this.projectInfo.id,
venuesCount: this.venues.length,
projectsCount: this.projects.length
})
}
// 加载选手列表
// 3. 最后加载选手列表使用场地和项目的第一条数据ID
await this.loadPlayers()
} catch (error) {
uni.hideLoading()
console.error('页面加载失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
}
},
methods: {
@@ -146,9 +238,10 @@ export default {
// 🔥 关键改动:使用 dataAdapter 获取选手列表
// Mock模式调用 mock/athlete.js 的 getMyAthletes 函数
// API模式调用 api/athlete.js 的 getMyAthletes 函数GET /api/mini/athletes
// API模式调用 api/athlete.js 的 getMyAthletes 函数GET /api/mini/score/athletes
const response = await dataAdapter.getData('getMyAthletes', {
judgeId: this.judgeId,
refereeType: this.refereeType, // 传递裁判类型
venueId: this.venueInfo.id,
projectId: this.projectInfo.id
})
@@ -182,14 +275,108 @@ export default {
}
},
/**
* 处理选手卡片点击
* - 裁判长:跳转到查看详情页面
* - 普通裁判:不处理(通过评分按钮跳转)
*/
handlePlayerClick(player) {
if (this.refereeType === 1) {
// 裁判长:查看评分详情
this.goToScoreDetail(player)
}
// 普通裁判不处理卡片点击,只能通过评分按钮跳转
},
goToScoreDetail(player) {
// 保存当前选手信息到全局数据
// 保存当前选手信息、项目ID和场地ID到全局数据
const app = getApp()
app.globalData.currentAthlete = player
app.globalData.currentProjectId = this.projectInfo.id
app.globalData.currentVenueId = this.venueInfo.id
// 调试信息
if (config.debug) {
console.log('进入评分详情:', {
athleteId: player.athleteId,
athleteName: player.name,
projectId: this.projectInfo.id,
projectName: this.projectInfo.name,
venueId: this.venueInfo.id,
venueName: this.venueInfo.name,
refereeType: this.refereeType
})
}
uni.navigateTo({
url: '/pages/score-detail/score-detail'
})
},
/**
* 切换场地
* @param {Number} index - 场地索引
*/
async switchVenue(index) {
// 如果点击的是当前场地,不做处理
if (index === this.currentVenueIndex) {
return
}
// 更新当前场地索引
this.currentVenueIndex = index
// 更新当前场地信息
const currentVenue = this.venues[index] || {}
this.venueInfo = {
id: currentVenue.id,
name: currentVenue.name
}
// 调试信息
if (config.debug) {
console.log('切换场地:', {
index: index,
venueId: this.venueInfo.id,
venueName: this.venueInfo.name
})
}
// 重新加载选手列表
await this.loadPlayers()
},
/**
* 切换项目
* @param {Number} index - 项目索引
*/
async switchProject(index) {
// 如果点击的是当前项目,不做处理
if (index === this.currentProjectIndex) {
return
}
// 更新当前项目索引
this.currentProjectIndex = index
// 更新当前项目信息
const currentProject = this.projects[index] || {}
this.projectInfo = {
id: currentProject.id,
name: currentProject.projectName
}
// 调试信息
if (config.debug) {
console.log('切换项目:', {
index: index,
projectId: this.projectInfo.id,
projectName: this.projectInfo.name
})
}
// 重新加载选手列表
await this.loadPlayers()
}
}
}
@@ -279,26 +466,41 @@ export default {
.venue-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 4rpx solid #1B7C5E;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.venue-header::-webkit-scrollbar {
display: none;
}
.venue-tab {
font-size: 32rpx;
font-weight: 600;
color: #333333;
position: relative;
padding: 20rpx 40rpx;
font-size: 28rpx;
font-weight: 500;
color: #666666;
background-color: #F5F5F5;
border-radius: 8rpx;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
white-space: nowrap;
}
.venue-tab.active::after {
content: '';
position: absolute;
bottom: -24rpx;
left: 0;
right: 0;
height: 4rpx;
.venue-tab:active {
opacity: 0.7;
}
.venue-tab.active {
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
background-color: #1B7C5E;
}
@@ -310,7 +512,15 @@ export default {
.project-section {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.project-section::-webkit-scrollbar {
display: none;
}
.project-btn {
@@ -321,6 +531,14 @@ export default {
font-size: 28rpx;
color: #1B7C5E;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
white-space: nowrap;
}
.project-btn:active {
opacity: 0.7;
}
.project-btn.active {
@@ -402,6 +620,12 @@ export default {
font-weight: 600;
}
.judge-count {
font-size: 24rpx;
color: #1B7C5E;
font-weight: 500;
}
.action-area {
display: flex;
flex-direction: column;
@@ -427,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;

13
postcss.config.js Normal file
View File

@@ -0,0 +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: [
uniappPlugin,
autoprefixer
]
}

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

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

14
public/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>武术评分系统</title>
<link rel="stylesheet" href="./static/index.css">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

18
src/App.vue Normal file
View File

@@ -0,0 +1,18 @@
<script>
export default {
onLaunch: function() {
console.log('App Launch')
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>
<style>
/* 注意要写在第一行同时给style标签加入lang="scss"属性 */
@import "common/common.css";
</style>

154
src/api/athlete.js Normal file
View File

@@ -0,0 +1,154 @@
/**
* API接口 - 选手模块
* 真实后端接口调用(需要后端实现)
*/
import request from '@/utils/request.js'
/**
* 获取我的选手列表(普通评委)
* @param {Object} params
* @param {String} params.matchCode - 比赛编码(推荐方式)
* @param {String} params.judgeId - 评委ID备选方式
* @param {String} params.venueId - 场地ID备选方式
* @param {String} params.projectId - 项目ID备选方式
* @returns {Promise}
*
* 注意:此接口需要后端实现
* 建议路径: GET /api/mini/athletes
*
* 推荐实现方式:
* 1. 优先从 Token 中解析评委信息(最安全)
* 2. 或使用 matchCode 参数,后端根据 Token 中的邀请码关联查询
* 3. 或使用 judgeId + venueId + projectId 直接查询
*/
export function getMyAthletes(params) {
return request({
url: '/mini/score/athletes',
method: 'GET',
params: {
...params,
refereeType: 2 // 普通裁判
},
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
*/
export function getAthletesForAdmin(params) {
return request({
url: '/mini/score/athletes',
method: 'GET',
params: {
...params,
refereeType: 1 // 裁判长
},
showLoading: true
})
}
/**
* 获取场地列表
* @param {Object} params
* @param {String} params.competitionId - 比赛ID
* @returns {Promise}
*/
export function getVenues(params) {
return request({
url: '/martial/venue/list',
method: 'GET',
params: {
...params,
current: 1,
size: 100
}
})
}
/**
* 获取项目列表
* @param {Object} params
* @param {String} params.competitionId - 比赛ID
* @returns {Promise}
*/
export function getProjects(params) {
return request({
url: '/martial/project/list',
method: 'GET',
params: {
...params,
current: 1,
size: 100
}
})
}
export default {
getMyAthletes,
getAthletesForAdmin,
getVenues,
getProjects
}
/**
* 后端接口规范(待实现):
*
* GET /api/mini/athletes
*
* 请求参数:
* {
* "judgeId": "456",
* "venueId": "1",
* "projectId": "5"
* }
*
* 响应:
* {
* "code": 200,
* "success": true,
* "msg": "操作成功",
* "data": [
* {
* "athleteId": "1",
* "name": "张三",
* "idCard": "123456789000000000",
* "team": "少林寺武术大学院",
* "number": "123-4567898275",
* "myScore": 8.906,
* "totalScore": 8.907,
* "scored": true,
* "scoreTime": "2025-06-25 09:15:00"
* }
* ]
* }
*
* SQL示例
* SELECT
* a.id AS athleteId,
* a.player_name AS name,
* a.id_card AS idCard,
* a.team_name AS team,
* a.player_no AS number,
* a.total_score AS totalScore,
* s.score AS myScore,
* CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END AS scored,
* s.score_time AS scoreTime
* FROM martial_athlete a
* LEFT JOIN martial_score s
* ON a.id = s.athlete_id
* AND s.judge_id = #{judgeId}
* WHERE a.venue_id = #{venueId}
* AND a.project_id = #{projectId}
* AND a.is_deleted = 0
* ORDER BY a.order_num ASC
*/

85
src/api/auth.js Normal file
View File

@@ -0,0 +1,85 @@
/**
* API接口 - 认证模块
* 真实后端接口调用(需要后端实现)
*/
import request from '@/utils/request.js'
/**
* 登录验证
* @param {Object} data
* @param {String} data.matchCode - 比赛编码
* @param {String} data.inviteCode - 邀请码
* @returns {Promise}
*
* 注意:此接口需要后端实现
* 建议路径: POST /api/mini/login
*/
export function login(data) {
return request({
url: '/mini/login',
method: 'POST',
data,
showLoading: true,
loadingText: '登录中...'
})
}
/**
* 退出登录
* @returns {Promise}
*/
export function logout() {
return request({
url: '/mini/logout',
method: 'POST'
})
}
/**
* Token验证
* @returns {Promise}
*/
export function verifyToken() {
return request({
url: '/mini/verify',
method: 'GET'
})
}
export default {
login,
logout,
verifyToken
}
/**
* 后端接口规范(待实现):
*
* POST /api/mini/login
*
* 请求:
* {
* "matchCode": "123",
* "inviteCode": "pub"
* }
*
* 响应:
* {
* "code": 200,
* "success": true,
* "msg": "登录成功",
* "data": {
* "token": "xxx",
* "userRole": "pub",
* "matchId": "123",
* "matchName": "2025年全国武术散打锦标赛...",
* "matchTime": "2025年6月25日 9:00",
* "judgeId": "456",
* "judgeName": "欧阳丽娜",
* "venueId": "1",
* "venueName": "第一场地",
* "projects": ["女子组长拳", "男子组陈氏太极拳"]
* }
* }
*/

158
src/api/index.js Normal file
View File

@@ -0,0 +1,158 @@
/**
* API接口中心
* 所有API接口的统一入口
*
* 这个文件汇总了所有业务模块的API接口函数
* 提供给 dataAdapter.js 调用
*/
import authApi from './auth.js'
import athleteApi from './athlete.js'
import scoreApi from './score.js'
/**
* 导出所有API接口函数
*
* 资源名称key对应 dataAdapter.getData() 的第一个参数
* 例如dataAdapter.getData('login', params) 会调用 authApi.login(params)
*/
export default {
// ==================== 认证模块 ====================
/**
* 登录验证
* @param {Object} data - { matchCode, inviteCode }
* @returns {Promise}
*/
login: authApi.login,
/**
* 退出登录
* @returns {Promise}
*/
logout: authApi.logout,
/**
* Token验证
* @returns {Promise}
*/
verifyToken: authApi.verifyToken,
// ==================== 选手模块 ====================
/**
* 获取我的选手列表(普通评委)
* @param {Object} params - { judgeId, venueId, projectId }
* @returns {Promise}
*/
getMyAthletes: athleteApi.getMyAthletes,
/**
* 获取选手列表(裁判长)
* @param {Object} params - { competitionId, venueId, projectId }
* @returns {Promise}
*/
getAthletesForAdmin: athleteApi.getAthletesForAdmin,
/**
* 获取场地列表
* @param {Object} params - { competitionId }
* @returns {Promise}
*/
getVenues: athleteApi.getVenues,
/**
* 获取项目列表
* @param {Object} params - { competitionId }
* @returns {Promise}
*/
getProjects: athleteApi.getProjects,
// ==================== 评分模块 ====================
/**
* 获取扣分项列表
* @param {Object} params - { projectId }
* @returns {Promise}
*/
getDeductions: scoreApi.getDeductions,
/**
* 提交评分
* @param {Object} data - { athleteId, judgeId, score, deductions, note }
* @returns {Promise}
*/
submitScore: scoreApi.submitScore,
/**
* 获取评分详情(裁判长查看)
* @param {Object} params - { athleteId }
* @returns {Promise}
*/
getScoreDetail: scoreApi.getScoreDetail,
/**
* 修改评分(裁判长)
* @param {Object} data - { athleteId, modifierId, modifiedScore, note }
* @returns {Promise}
*/
modifyScore: scoreApi.modifyScore
}
/**
* 使用说明:
*
* 这个文件不直接在页面中使用,而是通过 dataAdapter.js 间接调用。
*
* 当 config/env.config.js 中 dataMode 设置为 'api' 时,
* dataAdapter.getData() 会自动调用这里的API函数。
*
* 页面使用示例:
*
* import dataAdapter from '@/utils/dataAdapter.js'
*
* // 配置 dataMode: 'api' 时以下代码会调用真实API
* const res = await dataAdapter.getData('login', {
* matchCode: '123',
* inviteCode: 'pub'
* })
* // 实际调用: authApi.login({ matchCode, inviteCode })
* // 请求: POST /api/mini/login
*
* // 配置 dataMode: 'mock' 时同样的代码会使用Mock数据
* // 实际调用: mockData.login({ matchCode, inviteCode })
* // 无网络请求返回本地Mock数据
*/
/**
* 后端开发者注意事项:
*
* 1. 需要实现的新接口(小程序专用):
* - POST /api/mini/login # 登录验证
* - GET /api/mini/athletes # 普通评委选手列表
* - GET /api/mini/athletes/admin # 裁判长选手列表
* - GET /api/mini/score/detail/{athleteId} # 评分详情
* - PUT /api/mini/score/modify # 修改评分
*
* 2. 可以复用的现有接口:
* - POST /martial/score/submit # 提交评分
* - GET /martial/venue/list # 场地列表
* - GET /martial/project/list # 项目列表
* - GET /martial/deductionItem/list # 扣分项列表
*
* 3. 响应格式统一为 BladeX 标准格式:
* {
* "code": 200,
* "success": true,
* "msg": "操作成功",
* "data": { ... }
* }
*
* 4. 请求头要求:
* - Content-Type: application/json
* - Blade-Auth: Bearer {token}
*
* 5. 建议创建专门的Controller
* @RestController
* @RequestMapping("/api/mini")
* public class MartialMiniController {
* // 实现上述5个专用接口
* }
*/

165
src/api/score.js Normal file
View File

@@ -0,0 +1,165 @@
/**
* API接口 - 评分模块
* 真实后端接口调用(需要后端实现)
*/
import request from '@/utils/request.js'
/**
* 获取扣分项列表
* @param {Object} params
* @param {String} params.projectId - 项目ID
* @returns {Promise}
*/
export function getDeductions(params) {
return request({
url: '/blade-martial/deductionItem/list',
method: 'GET',
params: {
...params,
current: 1,
size: 100
}
})
}
/**
* 提交评分
* @param {Object} data
* @param {String} data.athleteId - 选手ID
* @param {String} data.judgeId - 评委ID
* @param {Number} data.score - 评分
* @param {Array} data.deductions - 扣分项
* @param {String} data.note - 备注
* @returns {Promise}
*/
export function submitScore(data) {
return request({
url: '/mini/score/submit',
method: 'POST',
data,
showLoading: true,
loadingText: '提交中...'
})
}
/**
* 获取评分详情(裁判长查看)
* @param {Object} params
* @param {String} params.athleteId - 选手ID
* @returns {Promise}
*
* 注意:此接口需要后端实现
* 建议路径: GET /api/mini/score/detail/{athleteId}
*/
export function getScoreDetail(params) {
return request({
url: `/mini/score/detail/${params.athleteId}`,
method: 'GET',
showLoading: true
})
}
/**
* 修改评分(裁判长)
* @param {Object} data
* @param {String} data.athleteId - 选手ID
* @param {String} data.modifierId - 修改人ID
* @param {Number} data.modifiedScore - 修改后的分数
* @param {String} data.note - 修改原因
* @returns {Promise}
*
* 注意:此接口需要后端实现
* 建议路径: PUT /api/mini/score/modify
*/
export function modifyScore(data) {
return request({
url: '/mini/score/modify',
method: 'PUT',
data,
showLoading: true,
loadingText: '修改中...'
})
}
export default {
getDeductions,
submitScore,
getScoreDetail,
modifyScore
}
/**
* 后端接口规范(待实现):
*
* 1. GET /api/mini/score/detail/{athleteId}
*
* 响应:
* {
* "code": 200,
* "success": true,
* "msg": "操作成功",
* "data": {
* "athleteInfo": {
* "athleteId": "1",
* "name": "张三",
* "idCard": "123456789000000000",
* "team": "少林寺武术大学院",
* "number": "123-4567898275",
* "totalScore": 8.907
* },
* "judgeScores": [
* {
* "judgeId": "1",
* "judgeName": "欧阳丽娜",
* "score": 8.907,
* "scoreTime": "2025-06-25 09:15:00",
* "note": ""
* }
* ],
* "modification": null
* }
* }
*
* SQL示例
* SELECT
* s.judge_id AS judgeId,
* s.judge_name AS judgeName,
* s.score,
* s.score_time AS scoreTime,
* s.note
* FROM martial_score s
* WHERE s.athlete_id = #{athleteId}
* ORDER BY s.score_time ASC
*
* ---
*
* 2. PUT /api/mini/score/modify
*
* 请求:
* {
* "athleteId": "1",
* "modifierId": "789",
* "modifiedScore": 8.910,
* "note": "修改原因"
* }
*
* 响应:
* {
* "code": 200,
* "success": true,
* "msg": "修改成功",
* "data": {
* "athleteId": "1",
* "originalScore": 8.907,
* "modifiedScore": 8.910,
* "modifyTime": "2025-06-25 10:00:00"
* }
* }
*
* 实现逻辑:
* 1. 验证权限(只有裁判长可以修改)
* 2. 保存 originalScore如果是第一次修改
* 3. 更新 totalScore
* 4. 记录 modifyReason 和 modifyTime
*/

30
src/common/common.css Normal file
View File

@@ -0,0 +1,30 @@
/* 全局样式 */
page {
background-color: #F5F5F5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
/* 防止系统样式影响 */
* {
box-sizing: border-box;
}
/* 清除默认样式 */
view, text, button, input {
margin: 0;
padding: 0;
}
button {
border: none;
outline: none;
}
button::after {
border: none;
}
input {
outline: none;
}

73
src/config/env.config.js Normal file
View File

@@ -0,0 +1,73 @@
/**
* 环境配置文件
* 控制应用的数据源模式Mock数据 或 真实API
*
* 使用说明:
* 1. Mock模式UI演示、前端独立开发设置 dataMode: 'mock'
* 2. API模式真实数据对接设置 dataMode: 'api'
* 3. 可在代码中动态切换模式
*/
const ENV_CONFIG = {
// 开发环境配置
development: {
// 数据模式: 'mock' | 'api'
// mock - 使用本地Mock数据保护UI版本
// api - 调用真实后端接口
dataMode: 'api',
// API基础路径dataMode为'api'时使用)
// uni.request 不支持 devServer proxy必须用完整地址
apiBaseURL: 'http://142.91.105.230:8123',
// 请求超时时间(毫秒)
timeout: 30000,
// 调试模式
debug: true
},
// 测试环境配置
test: {
dataMode: 'api',
apiBaseURL: 'http://test-api.yourdomain.com',
debug: true,
timeout: 30000,
mockDelay: 0
},
// 生产环境配置
production: {
dataMode: 'api',
apiBaseURL: 'https://api.yourdomain.com',
debug: false,
timeout: 30000,
mockDelay: 0
}
}
// 获取当前环境(开发/测试/生产)
const env = process.env.NODE_ENV || 'development'
// 导出当前环境的配置
export default {
...ENV_CONFIG[env],
env
}
/**
* 快速切换数据模式示例:
*
* // 在代码中使用
* import config from '@/config/env.config.js'
*
* if (config.dataMode === 'mock') {
* console.log('当前使用Mock数据')
* } else {
* console.log('当前使用真实API')
* }
*
* // 查看当前环境
* console.log('当前环境:', config.env)
* console.log('数据模式:', config.dataMode)
*/

11
src/main.js Normal file
View File

@@ -0,0 +1,11 @@
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()

67
src/manifest.json Normal file
View File

@@ -0,0 +1,67 @@
{
"name" : "武术评分系统",
"appid" : "",
"description" : "武术比赛评分系统",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {},
"sdkConfigs" : {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"h5" : {
"title" : "武术评分系统",
"router" : {
"mode" : "hash",
"base" : "./"
}
}
}

162
src/mock/athlete.js Normal file
View File

@@ -0,0 +1,162 @@
/**
* Mock 数据 - 选手模块
* 模拟选手列表数据
*/
/**
* 获取我的选手列表(普通评委)
* @param {Object} params
* @param {String} params.judgeId - 评委ID
* @param {String} params.venueId - 场地ID
* @param {String} params.projectId - 项目ID
* @returns {Array} 选手列表(带评分状态)
*/
export function getMyAthletes(params) {
// 模拟3个选手数据
return [
{
athleteId: '1',
name: '张三',
idCard: '123456789000000000',
team: '少林寺武术大学院',
number: '123-4567898275',
myScore: 8.906, // 我的评分
totalScore: 8.907, // 总分
scored: true, // 已评分
scoreTime: '2025-06-25 09:15:00'
},
{
athleteId: '2',
name: '李四',
idCard: '123456789000000001',
team: '武当山武术学院',
number: '123-4567898276',
myScore: 8.901,
totalScore: 8.902,
scored: true,
scoreTime: '2025-06-25 09:20:00'
},
{
athleteId: '3',
name: '王五',
idCard: '123456789000000002',
team: '峨眉派武术学校',
number: '123-4567898277',
myScore: null, // 未评分
totalScore: null,
scored: false,
scoreTime: null
}
]
}
/**
* 获取选手列表(裁判长)
* @param {Object} params
* @param {String} params.competitionId - 比赛ID
* @param {String} params.venueId - 场地ID
* @param {String} params.projectId - 项目ID
* @returns {Array} 选手列表(带评分统计)
*/
export function getAthletesForAdmin(params) {
// 模拟5个选手数据
return [
{
athleteId: '1',
name: '张三',
idCard: '123456789000000000',
team: '少林寺武术大学院',
number: '123-4567898275',
totalScore: 8.907,
judgeCount: 6, // 已评分评委数
totalJudges: 6, // 总评委数
canModify: true // 可以修改(所有评委已评分)
},
{
athleteId: '2',
name: '李四',
idCard: '123456789000000001',
team: '武当山武术学院',
number: '123-4567898276',
totalScore: 8.902,
judgeCount: 6,
totalJudges: 6,
canModify: true
},
{
athleteId: '3',
name: '王五',
idCard: '123456789000000002',
team: '峨眉派武术学校',
number: '123-4567898277',
totalScore: null,
judgeCount: 3, // 只有3位评委评分
totalJudges: 6,
canModify: false // 不能修改(未全部评分)
},
{
athleteId: '4',
name: '赵六',
idCard: '123456789000000003',
team: '华山武术学院',
number: '123-4567898278',
totalScore: 8.899,
judgeCount: 6,
totalJudges: 6,
canModify: true
},
{
athleteId: '5',
name: '孙七',
idCard: '123456789000000004',
team: '崆峒派武术学校',
number: '123-4567898279',
totalScore: 8.912,
judgeCount: 6,
totalJudges: 6,
canModify: true
}
]
}
/**
* 获取场地列表
* @param {Object} params
* @param {String} params.competitionId - 比赛ID
* @returns {Array} 场地列表
*/
export function getVenues(params) {
return [
{ id: '1', name: '第一场地' },
{ id: '2', name: '第二场地' },
{ id: '3', name: '第三场地' },
{ id: '4', name: '第四场地' },
{ id: '5', name: '第五场地' }
]
}
/**
* 获取项目列表
* @param {Object} params
* @param {String} params.competitionId - 比赛ID
* @returns {Array} 项目列表对象数组与API格式一致
*/
export function getProjects(params) {
return [
{ id: '5', name: '女子组长拳' },
{ id: '6', name: '男子组陈氏太极拳' },
{ id: '7', name: '女子组双剑(含长穗双剑)' },
{ id: '8', name: '男子组杨氏太极拳' },
{ id: '9', name: '女子组刀术' },
{ id: '10', name: '男子组棍术' },
{ id: '11', name: '女子组枪术' },
{ id: '12', name: '男子组剑术' }
]
}
export default {
getMyAthletes,
getAthletesForAdmin,
getVenues,
getProjects
}

117
src/mock/index.js Normal file
View File

@@ -0,0 +1,117 @@
/**
* Mock数据中心
* 所有Mock数据的统一入口
*
* 这个文件汇总了所有业务模块的Mock数据函数
* 提供给 dataAdapter.js 调用
*/
import loginMock from './login.js'
import athleteMock from './athlete.js'
import scoreMock from './score.js'
/**
* 导出所有Mock数据函数
*
* 资源名称key对应 dataAdapter.getData() 的第一个参数
* 例如dataAdapter.getData('login', params) 会调用 loginMock.login(params)
*/
export default {
// ==================== 认证模块 ====================
/**
* 登录验证
* @param {Object} params - { matchCode, inviteCode }
* @returns {Object} 用户信息和Token
*/
login: loginMock.login,
// ==================== 选手模块 ====================
/**
* 获取我的选手列表(普通评委)
* @param {Object} params - { judgeId, venueId, projectId }
* @returns {Array} 选手列表(带评分状态)
*/
getMyAthletes: athleteMock.getMyAthletes,
/**
* 获取选手列表(裁判长)
* @param {Object} params - { competitionId, venueId, projectId }
* @returns {Array} 选手列表(带评分统计)
*/
getAthletesForAdmin: athleteMock.getAthletesForAdmin,
/**
* 获取场地列表
* @param {Object} params - { competitionId }
* @returns {Array} 场地列表
*/
getVenues: athleteMock.getVenues,
/**
* 获取项目列表
* @param {Object} params - { competitionId }
* @returns {Array} 项目列表
*/
getProjects: athleteMock.getProjects,
// ==================== 评分模块 ====================
/**
* 获取扣分项列表
* @param {Object} params - { projectId }
* @returns {Array} 扣分项列表
*/
getDeductions: scoreMock.getDeductions,
/**
* 提交评分
* @param {Object} params - { athleteId, judgeId, score, deductions, note }
* @returns {Object} 提交结果
*/
submitScore: scoreMock.submitScore,
/**
* 获取评分详情(裁判长查看)
* @param {Object} params - { athleteId }
* @returns {Object} 评分详情(选手信息+评委评分)
*/
getScoreDetail: scoreMock.getScoreDetail,
/**
* 修改评分(裁判长)
* @param {Object} params - { athleteId, modifierId, modifiedScore, note }
* @returns {Object} 修改结果
*/
modifyScore: scoreMock.modifyScore
}
/**
* 使用说明:
*
* 这个文件不直接在页面中使用,而是通过 dataAdapter.js 间接调用。
*
* 页面使用示例:
*
* import dataAdapter from '@/utils/dataAdapter.js'
*
* // 登录
* const res = await dataAdapter.getData('login', {
* matchCode: '123',
* inviteCode: 'pub'
* })
*
* // 获取选手列表
* const res = await dataAdapter.getData('getMyAthletes', {
* judgeId: '456',
* venueId: '1',
* projectId: '5'
* })
*
* // 提交评分
* const res = await dataAdapter.getData('submitScore', {
* athleteId: '1',
* judgeId: '456',
* score: 8.907,
* deductions: [...],
* note: '表现优秀'
* })
*/

56
src/mock/login.js Normal file
View File

@@ -0,0 +1,56 @@
/**
* Mock 数据 - 登录模块
* 模拟登录验证和用户信息返回
*/
/**
* 登录验证
* @param {Object} params
* @param {String} params.matchCode - 比赛编码
* @param {String} params.inviteCode - 邀请码pub 或 admin
* @returns {Object} 用户信息和Token
*/
export function login(params) {
const { matchCode, inviteCode } = params
// 模拟验证逻辑
const role = inviteCode.toLowerCase()
if (role !== 'pub' && role !== 'admin') {
throw new Error('邀请码错误,请使用 pub 或 admin')
}
// 返回Mock登录数据
return {
token: 'mock_token_' + Date.now(),
userRole: role, // 'pub' 或 'admin'
matchId: '123',
matchName: '2025年全国武术散打锦标赛暨第十七届世界武术锦标赛选拔赛',
matchTime: '2025年6月25日 9:00',
judgeId: '456',
judgeName: '欧阳丽娜',
// 普通评委有固定场地,裁判长可以查看所有场地
venueId: role === 'pub' ? '1' : null,
venueName: role === 'pub' ? '第一场地' : null,
// 分配的项目列表(对象数组格式)
projects: role === 'pub'
? [
{ projectId: 1, projectName: '女子组长拳' },
{ projectId: 2, projectName: '男子组陈氏太极拳' }
]
: [
{ projectId: 1, projectName: '女子组长拳' },
{ projectId: 2, projectName: '男子组陈氏太极拳' },
{ projectId: 3, projectName: '女子组双剑(含长穗双剑)' },
{ projectId: 4, projectName: '男子组杨氏太极拳' },
{ projectId: 5, projectName: '女子组刀术' },
{ projectId: 6, projectName: '男子组棍术' },
{ projectId: 7, projectName: '女子组枪术' },
{ projectId: 8, projectName: '男子组剑术' }
]
}
}
export default {
login
}

162
src/mock/score.js Normal file
View File

@@ -0,0 +1,162 @@
/**
* Mock 数据 - 评分模块
* 模拟评分相关数据
*/
/**
* 获取扣分项列表
* @param {Object} params
* @param {String} params.projectId - 项目ID
* @returns {Array} 扣分项列表
*/
export function getDeductions(params) {
// 模拟8个扣分项
return [
{ id: '1', text: '扣分项描述', score: -0.1, checked: false },
{ id: '2', text: '扣分项描述', score: -0.1, checked: false },
{ id: '3', text: '扣分项描述', score: -0.1, checked: false },
{ id: '4', text: '扣分项描述', score: -0.1, checked: false },
{ id: '5', text: '扣分项描述', score: -0.1, checked: false },
{ id: '6', text: '扣分项描述', score: -0.1, checked: false },
{ id: '7', text: '扣分项描述', score: -0.1, checked: false },
{ id: '8', text: '扣分项描述', score: -0.1, checked: false }
]
}
/**
* 提交评分
* @param {Object} params
* @param {String} params.athleteId - 选手ID
* @param {String} params.judgeId - 评委ID
* @param {Number} params.score - 评分
* @param {Array} params.deductions - 扣分项
* @param {String} params.note - 备注
* @returns {Object} 提交结果
*/
export function submitScore(params) {
const { athleteId, judgeId, score, deductions, note } = params
// 模拟提交成功
console.log('Mock提交评分:', {
athleteId,
judgeId,
score,
deductions: deductions.filter(d => d.checked).length + '项',
note
})
return {
scoreId: 'score_' + Date.now(),
athleteId,
judgeId,
score,
submitTime: new Date().toISOString(),
message: '评分提交成功'
}
}
/**
* 获取评分详情(裁判长查看)
* @param {Object} params
* @param {String} params.athleteId - 选手ID
* @returns {Object} 评分详情
*/
export function getScoreDetail(params) {
const { athleteId } = params
// 模拟选手信息和评委评分
return {
athleteInfo: {
athleteId,
name: '张三',
idCard: '123456789000000000',
team: '少林寺武术大学院',
number: '123-4567898275',
totalScore: 8.907
},
// 6位评委的评分
judgeScores: [
{
judgeId: '1',
judgeName: '欧阳丽娜',
score: 8.907,
scoreTime: '2025-06-25 09:15:00',
note: ''
},
{
judgeId: '2',
judgeName: '张三',
score: 8.901,
scoreTime: '2025-06-25 09:15:30',
note: ''
},
{
judgeId: '3',
judgeName: '裁判姓名',
score: 8.902,
scoreTime: '2025-06-25 09:16:00',
note: ''
},
{
judgeId: '4',
judgeName: '裁判姓名',
score: 8.907,
scoreTime: '2025-06-25 09:16:30',
note: ''
},
{
judgeId: '5',
judgeName: '裁判姓名',
score: 8.905,
scoreTime: '2025-06-25 09:17:00',
note: ''
},
{
judgeId: '6',
judgeName: '裁判姓名',
score: 8.904,
scoreTime: '2025-06-25 09:17:30',
note: ''
}
],
// 修改记录(如果有)
modification: null
}
}
/**
* 修改评分(裁判长)
* @param {Object} params
* @param {String} params.athleteId - 选手ID
* @param {String} params.modifierId - 修改人ID裁判长
* @param {Number} params.modifiedScore - 修改后的分数
* @param {String} params.note - 修改原因
* @returns {Object} 修改结果
*/
export function modifyScore(params) {
const { athleteId, modifierId, modifiedScore, note } = params
// 模拟修改成功
console.log('Mock修改评分:', {
athleteId,
modifierId,
originalScore: 8.907,
modifiedScore,
note
})
return {
athleteId,
originalScore: 8.907,
modifiedScore,
modifyTime: new Date().toISOString(),
message: '评分修改成功'
}
}
export default {
getDeductions,
submitScore,
getScoreDetail,
modifyScore
}

47
src/pages.json Normal file
View File

@@ -0,0 +1,47 @@
{
"pages": [
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "pages/score-list/score-list",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}
},
{
"path": "pages/modify-score/modify-score",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "pages/score-list-multi/score-list-multi",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "pages/score-detail/score-detail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "评分系统",
"navigationBarBackgroundColor": "#1B7C5E",
"backgroundColor": "#F5F5F5"
}
}

305
src/pages/login/login.vue Normal file
View File

@@ -0,0 +1,305 @@
<template>
<view class="container">
<!-- 自定义导航栏 -->
<view class="nav-bar">
<view class="nav-title">评分系统</view>
<view class="nav-right">
<view class="icon-menu">···</view>
<view class="icon-close"></view>
</view>
</view>
<!-- 主体内容 -->
<view class="content">
<view class="page-title">进入评分</view>
<!-- 比赛编码输入 -->
<view class="input-group">
<view class="input-label">比赛编码</view>
<view class="input-wrapper">
<input
class="input-field"
type="text"
placeholder="请输入比赛编码"
v-model="matchCode"
/>
</view>
</view>
<!-- 评委邀请码输入 -->
<view class="input-group">
<view class="input-label">评委邀请码</view>
<view class="input-wrapper">
<input
class="input-field"
type="text"
placeholder="请输入评委邀请码"
v-model="inviteCode"
/>
</view>
</view>
<!-- 立即评分按钮 -->
<button class="submit-btn" @click="handleSubmit">立即评分</button>
</view>
</view>
</template>
<script>
import dataAdapter from '@/utils/dataAdapter.js'
import config from '@/config/env.config.js'
export default {
data() {
return {
matchCode: '',
inviteCode: ''
}
},
onLoad() {
// 开发环境显示当前数据模式
if (config.debug) {
console.log('='.repeat(50))
console.log('当前数据模式:', config.dataMode)
console.log('Mock模式:', dataAdapter.isMockMode() ? '是' : '否')
console.log('API模式:', dataAdapter.isApiMode() ? '是' : '否')
console.log('='.repeat(50))
}
},
methods: {
async handleSubmit() {
// 表单验证
if (!this.matchCode) {
uni.showToast({
title: '请输入比赛编码',
icon: 'none'
})
return
}
if (!this.inviteCode) {
uni.showToast({
title: '请输入评委邀请码',
icon: 'none'
})
return
}
try {
// 显示加载
uni.showLoading({
title: '登录中...',
mask: true
})
// 🔥 关键改动:使用 dataAdapter 进行登录
// Mock模式调用 mock/login.js 的 login 函数
// API模式调用 api/auth.js 的 login 函数POST /api/mini/login
const response = await dataAdapter.getData('login', {
matchCode: this.matchCode,
inviteCode: this.inviteCode
})
uni.hideLoading()
// 处理登录响应Mock和API返回格式相同
const {
token,
userRole,
matchId,
matchName,
matchTime,
judgeId,
judgeName,
venueId,
venueName,
projects
} = response.data
// 保存Token到本地存储
uni.setStorageSync('token', token)
// 保存用户信息到全局数据
getApp().globalData = {
token, // Token用于登录状态检查
userRole, // 'pub' 或 'admin'
matchCode: this.matchCode, // 比赛编码
inviteCode: this.inviteCode, // 邀请码重要用于后续API调用
matchId,
matchName,
matchTime,
judgeId,
judgeName,
venueId, // 普通评委有场地裁判长为null
venueName,
projects, // 分配的项目列表
currentProjectIndex: 0 // 当前选中的项目索引
}
// 调试信息
if (config.debug) {
console.log('登录成功:', {
userRole,
judgeName,
venueId: venueId || '全部场地',
projects: projects.length + '个项目'
})
}
// 显示登录成功提示
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
})
// 根据角色跳转到不同页面
setTimeout(() => {
if (userRole === 'admin') {
// 裁判长跳转到多场地列表页(可以修改评分)
uni.navigateTo({
url: '/pages/score-list-multi/score-list-multi'
})
} else {
// 普通裁判跳转到评分列表页(可以评分)
uni.navigateTo({
url: '/pages/score-list/score-list'
})
}
}, 1500)
} catch (error) {
uni.hideLoading()
// 错误处理
console.error('登录失败:', error)
uni.showToast({
title: error.message || '登录失败,请重试',
icon: 'none',
duration: 2000
})
}
}
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #F5F5F5;
}
/* 导航栏 */
.nav-bar {
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 0 30rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
letter-spacing: 2rpx;
}
.nav-right {
position: absolute;
right: 30rpx;
display: flex;
align-items: center;
gap: 30rpx;
}
.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;
font-size: 32rpx;
color: #FFFFFF;
font-weight: bold;
}
/* 主体内容 */
.content {
padding: 60rpx 40rpx;
}
.page-title {
font-size: 40rpx;
font-weight: 600;
color: #333333;
margin-bottom: 60rpx;
text-align: center;
}
/* 输入组 */
.input-group {
margin-bottom: 40rpx;
}
.input-label {
font-size: 32rpx;
font-weight: 500;
color: #333333;
margin-bottom: 20rpx;
}
.input-wrapper {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
position: relative;
}
.input-field {
width: 100%;
font-size: 28rpx;
color: #333333;
border: none;
}
.input-field::placeholder {
color: #CCCCCC;
}
.input-tip {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
font-size: 24rpx;
color: #FF4D6A;
}
/* 提交按钮 */
.submit-btn {
width: 100%;
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
margin-top: 80rpx;
box-shadow: 0 8rpx 20rpx rgba(27, 124, 94, 0.3);
}
.submit-btn:active {
opacity: 0.9;
}
</style>

View File

@@ -0,0 +1,675 @@
<template>
<view class="container">
<!-- 自定义导航栏 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="back-icon"></text>
</view>
<view class="nav-title">修改评分</view>
<view class="nav-right">
<view class="icon-menu">···</view>
<view class="icon-close"></view>
</view>
</view>
<!-- 选手信息 -->
<view class="player-info-section">
<view class="player-header">
<view class="player-name">{{ athleteInfo.name }}</view>
<view class="total-score-label">
<text class="label-text">总分</text>
<text class="score-value">{{ formatScore(athleteInfo.totalScore) }}</text>
</view>
</view>
<view class="player-details">
<view class="detail-item">身份证{{ athleteInfo.idCard }}</view>
<view class="detail-item">队伍{{ athleteInfo.team }}</view>
<view class="detail-item">编号{{ athleteInfo.number }}</view>
</view>
</view>
<!-- 评委评分统计 -->
<view class="judges-section">
<view class="section-title">共有{{ judgeScores.length }}位评委完成评分</view>
<view class="judges-scores">
<view
class="judge-score-item"
v-for="judge in judgeScores"
:key="judge.judgeId"
>
<text class="judge-name">{{ judge.judgeName }}</text>
<text class="judge-score">{{ judge.score }}</text>
</view>
</view>
</view>
<!-- 修改总分区域 -->
<view class="modify-section">
<view class="modify-header">
<text class="modify-label">修改总分±0.050</text>
</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>
<text class="no-modify-text">可不改</text>
</view>
<view class="control-btn increase" @click="increaseScore">
<text class="btn-symbol"></text>
<text class="btn-value">+0.001</text>
</view>
</view>
</view>
<!-- 备注 -->
<view class="note-section">
<view class="note-label">
<text>备注</text>
</view>
<view class="note-input-wrapper">
<textarea
class="note-input"
placeholder="请输入修改备注"
v-model="note"
maxlength="200"
/>
<text class="optional-text">可不填</text>
</view>
</view>
<!-- 修改按钮 -->
<button class="modify-btn" @click="handleModify">修改</button>
</view>
</template>
<script>
import dataAdapter from '@/utils/dataAdapter.js'
import config from '@/config/env.config.js'
export default {
data() {
return {
athleteInfo: {
athleteId: '',
name: '',
idCard: '',
team: '',
number: '',
totalScore: 0
},
judgeScores: [],
modification: null,
modifierId: '',
currentScore: 0,
originalScore: 0,
note: '',
minScore: 0,
maxScore: 10.0
}
},
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.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({
title: '加载中...',
mask: true
})
const response = await dataAdapter.getData('getScoreDetail', {
athleteId: athleteId
})
uni.hideLoading()
// 如果接口返回了数据,更新页面
if (response && response.data) {
// 更新评委评分列表
this.judgeScores = response.data.judgeScores || []
this.modification = response.data.modification || null
// 如果接口返回了选手信息,更新(但保留传递过来的数据作为备用)
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
}
// 更新分数
const totalScore = parseFloat(apiAthleteInfo.totalScore) || this.originalScore
this.originalScore = totalScore
this.currentScore = totalScore
}
}
// 调试信息
if (config.debug) {
console.log('评分详情加载成功:', {
athlete: this.athleteInfo,
judges: this.judgeScores.length,
originalScore: this.originalScore,
currentScore: this.currentScore,
modification: this.modification
})
}
} catch (error) {
uni.hideLoading()
console.error('加载评分详情失败:', error)
// 不显示错误提示,因为已经有传递过来的数据可以使用
if (config.debug) {
console.log('使用传递过来的选手数据')
}
}
},
goBack() {
uni.navigateBack()
},
decreaseScore() {
// 限制最小值为原始分数-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() {
// 限制最大值为原始分数+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() {
// 验证评分范围±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: '评分只能在原始分数±0.050范围内',
icon: 'none'
})
return
}
// 检查是否有修改
if (this.currentScore === this.originalScore && !this.note) {
uni.showToast({
title: '请修改分数或填写备注',
icon: 'none'
})
return
}
try {
uni.showLoading({
title: '提交中...',
mask: true
})
const response = await dataAdapter.getData('modifyScore', {
modifierId: this.modifierId,
athleteId: this.athleteInfo.athleteId,
modifiedScore: this.currentScore,
note: this.note
})
uni.hideLoading()
// 调试信息
if (config.debug) {
console.log('修改评分成功:', {
athleteId: this.athleteInfo.athleteId,
originalScore: this.originalScore,
modifiedScore: this.currentScore,
note: this.note,
response: response
})
}
// 显示成功提示
uni.showToast({
title: '修改成功',
icon: 'success',
duration: 1500
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
uni.hideLoading()
console.error('修改评分失败:', error)
uni.showToast({
title: error.message || '修改失败,请重试',
icon: 'none',
duration: 2000
})
}
}
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 40rpx;
}
/* 导航栏 */
.nav-bar {
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 0 30rpx;
}
.nav-left {
position: absolute;
left: 30rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 60rpx;
color: #FFFFFF;
font-weight: 300;
line-height: 1;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
letter-spacing: 2rpx;
}
.nav-right {
position: absolute;
right: 30rpx;
display: flex;
align-items: center;
gap: 30rpx;
}
.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;
font-size: 32rpx;
color: #FFFFFF;
font-weight: bold;
}
/* 选手信息 */
.player-info-section {
margin: 30rpx;
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.player-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.player-name {
font-size: 34rpx;
font-weight: 600;
color: #333333;
}
.total-score-label {
display: flex;
align-items: baseline;
}
.label-text {
font-size: 26rpx;
color: #666666;
}
.score-value {
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-left: 8rpx;
}
.player-details {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.detail-item {
font-size: 26rpx;
color: #666666;
line-height: 1.5;
}
/* 评委评分统计 */
.judges-section {
margin: 30rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333333;
margin-bottom: 20rpx;
}
.judges-scores {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.judge-score-item {
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;
}
/* 修改总分区域 */
.modify-section {
margin: 30rpx;
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 40rpx 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.modify-header {
margin-bottom: 30rpx;
}
.modify-label {
font-size: 28rpx;
color: #666666;
}
.score-control {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.control-btn {
width: 140rpx;
height: 140rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #F5F5F5;
border-radius: 12rpx;
}
.control-btn.decrease {
background-color: #FFE5E5;
}
.control-btn.increase {
background-color: #E5F5F0;
}
.btn-symbol {
font-size: 48rpx;
font-weight: 300;
}
.control-btn.decrease .btn-symbol {
color: #FF4D6A;
}
.control-btn.increase .btn-symbol {
color: #1B7C5E;
}
.btn-value {
font-size: 24rpx;
margin-top: 8rpx;
}
.control-btn.decrease .btn-value {
color: #FF4D6A;
}
.control-btn.increase .btn-value {
color: #1B7C5E;
}
.score-display {
display: flex;
flex-direction: column;
align-items: center;
}
.current-score {
font-size: 60rpx;
font-weight: 600;
color: #1B7C5E;
}
.no-modify-text {
font-size: 24rpx;
color: #FF4D6A;
margin-top: 8rpx;
}
.modify-tip {
font-size: 24rpx;
color: #FF4D6A;
line-height: 1.6;
text-align: center;
}
/* 备注 */
.note-section {
margin: 30rpx;
}
.note-label {
font-size: 28rpx;
color: #333333;
margin-bottom: 20rpx;
}
.note-input-wrapper {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
position: relative;
}
.note-input {
width: 100%;
min-height: 120rpx;
font-size: 28rpx;
color: #333333;
line-height: 1.6;
}
.note-input::placeholder {
color: #CCCCCC;
}
.optional-text {
position: absolute;
right: 30rpx;
bottom: 30rpx;
font-size: 24rpx;
color: #FF4D6A;
}
/* 修改按钮 */
.modify-btn {
margin: 30rpx;
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(27, 124, 94, 0.3);
}
.modify-btn:active {
opacity: 0.9;
}
</style>

View File

@@ -0,0 +1,621 @@
<template>
<view class="container">
<!-- 自定义导航栏 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="back-icon"></text>
</view>
<view class="nav-title">评分详情</view>
<view class="nav-right">
<view class="icon-menu">···</view>
<view class="icon-close"></view>
</view>
</view>
<!-- 选手信息 -->
<view class="player-info-section">
<view class="player-name">{{ player.name }}</view>
<view class="player-details">
<view class="detail-item">身份证{{ player.idCard }}</view>
<view class="detail-item">队伍{{ player.team }}</view>
<view class="detail-item">编号{{ player.number }}</view>
</view>
</view>
<!-- 评分提示 -->
<view class="score-tip">
点击分数填写或拖动滑块打分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>
</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">
<view
v-for="(item, index) in deductions"
:key="item.deductionId"
class="deduction-item"
@click="toggleDeduction(index)"
>
<view :class="['checkbox', item.checked ? 'checked' : '']">
<text v-if="item.checked" class="check-icon"></text>
</view>
<text class="deduction-text">{{ item.deductionName }}</text>
</view>
</view>
</view>
<!-- 备注 -->
<view class="note-section">
<view class="note-label">
<text>备注</text>
</view>
<view class="note-input-wrapper">
<textarea
class="note-input"
placeholder="请输入修改备注"
v-model="note"
maxlength="200"
/>
<!-- <text class="optional-text">可不填</text> -->
</view>
</view>
<!-- 提交按钮 -->
<button class="submit-btn" @click="handleSubmit">提交</button>
</view>
</template>
<script>
import dataAdapter from '@/utils/dataAdapter.js'
import config from '@/config/env.config.js'
export default {
data() {
return {
player: {
athleteId: '',
name: '',
idCard: '',
team: '',
number: ''
},
judgeId: '',
projectId: '',
currentScore: 8.000,
note: '',
minScore: 5.0,
maxScore: 10.0,
deductions: []
}
},
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 || '',
name: currentAthlete.name || '选手姓名',
idCard: currentAthlete.idCard || '',
team: currentAthlete.team || '',
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
// 调试信息
if (config.debug) {
console.log('评分详情页加载:', {
athlete: this.player,
judgeId: this.judgeId,
projectId: this.projectId,
initialScore: this.currentScore
})
}
// 加载扣分项列表
await this.loadDeductions()
},
methods: {
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,
checked: false
}))
// 调试信息
if (config.debug) {
console.log('扣分项加载成功:', this.deductions)
}
} catch (error) {
console.error('加载扣分项失败:', error)
uni.showToast({
title: '加载扣分项失败',
icon: 'none'
})
}
},
goBack() {
uni.navigateBack()
},
decreaseScore() {
if (this.currentScore > this.minScore) {
this.currentScore = parseFloat((this.currentScore - 0.001).toFixed(3))
}
},
increaseScore() {
if (this.currentScore < this.maxScore) {
this.currentScore = parseFloat((this.currentScore + 0.001).toFixed(3))
}
},
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}分之间`,
icon: 'none'
})
return
}
// 收集选中的扣分项ID转为数字类型后端期望 List<Long>
const selectedDeductions = this.deductions
.filter(item => item.checked)
.map(item => String(item.deductionId))
try {
uni.showLoading({
title: '提交中...',
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),
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 || ''
})
uni.hideLoading()
// 调试信息
if (config.debug) {
console.log('评分提交成功:', {
athleteId: this.player.athleteId,
score: this.currentScore,
deductions: selectedDeductions,
response: response
})
}
// 显示成功提示
uni.showToast({
title: '提交成功',
icon: 'success',
duration: 1500
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
uni.hideLoading()
console.error('提交评分失败:', error)
uni.showToast({
title: error.message || '提交失败,请重试',
icon: 'none',
duration: 2000
})
}
}
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 40rpx;
}
/* 导航栏 */
.nav-bar {
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 0 30rpx;
}
.nav-left {
position: absolute;
left: 30rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 60rpx;
color: #FFFFFF;
font-weight: 300;
line-height: 1;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
letter-spacing: 2rpx;
}
.nav-right {
position: absolute;
right: 30rpx;
display: flex;
align-items: center;
gap: 30rpx;
}
.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;
font-size: 32rpx;
color: #FFFFFF;
font-weight: bold;
}
/* 选手信息 */
.player-info-section {
margin: 30rpx;
}
.player-name {
font-size: 34rpx;
font-weight: 600;
color: #333333;
margin-bottom: 20rpx;
}
.player-details {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.detail-item {
font-size: 26rpx;
color: #CD8B6F;
line-height: 1.5;
}
/* 评分提示 */
.score-tip {
padding: 0 30rpx;
font-size: 26rpx;
color: #666666;
margin-bottom: 30rpx;
}
/* 分数控制 */
.score-control {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 60rpx;
margin-bottom: 20rpx;
}
.control-btn {
width: 140rpx;
height: 140rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #F5F5F5;
border-radius: 12rpx;
}
.control-btn.decrease {
background-color: #FFE5E5;
}
.control-btn.increase {
background-color: #E5F5F0;
}
.btn-symbol {
font-size: 48rpx;
font-weight: 300;
}
.control-btn.decrease .btn-symbol {
color: #FF4D6A;
}
.control-btn.increase .btn-symbol {
color: #1B7C5E;
}
.btn-value {
font-size: 24rpx;
margin-top: 8rpx;
}
.control-btn.decrease .btn-value {
color: #FF4D6A;
}
.control-btn.increase .btn-value {
color: #1B7C5E;
}
.score-display {
display: flex;
flex-direction: column;
align-items: center;
}
.current-score {
font-size: 80rpx;
font-weight: 600;
color: #1B7C5E;
}
.judge-tip {
padding: 0 30rpx;
font-size: 24rpx;
color: #FF4D6A;
text-align: center;
line-height: 1.6;
margin-bottom: 30rpx;
}
/* 扣分项 */
.deduction-section {
margin: 30rpx;
}
.deduction-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.deduction-label {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
.deduction-hint {
font-size: 24rpx;
color: #FF4D6A;
}
.deduction-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.deduction-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #FFFFFF;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #CCCCCC;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
background-color: #F5F5F5;
}
.checkbox.checked {
background-color: #1B7C5E;
border-color: #1B7C5E;
}
.check-icon {
color: #FFFFFF;
font-size: 28rpx;
font-weight: bold;
}
.deduction-text {
font-size: 26rpx;
color: #333333;
line-height: 1.4;
flex: 1;
}
/* 备注 */
.note-section {
margin: 30rpx;
}
.note-label {
font-size: 28rpx;
color: #333333;
margin-bottom: 20rpx;
}
.note-input-wrapper {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
position: relative;
}
.note-input {
width: 100%;
min-height: 120rpx;
font-size: 28rpx;
color: #333333;
line-height: 1.6;
}
.note-input::placeholder {
color: #CCCCCC;
}
.optional-text {
position: absolute;
right: 30rpx;
bottom: 30rpx;
font-size: 24rpx;
color: #FF4D6A;
}
/* 提交按钮 */
.submit-btn {
margin: 30rpx;
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(27, 124, 94, 0.3);
}
.submit-btn:active {
opacity: 0.9;
}
</style>

View File

@@ -0,0 +1,595 @@
<template>
<view class="container">
<!-- 自定义导航栏 -->
<view class="nav-bar">
<view class="nav-title">评分系统</view>
<view class="nav-right">
<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">比赛时间{{ formatDateTime(matchInfo.time) }}</view>
</view>
<!-- 场地和项目卡片 -->
<view class="venue-card">
<!-- 场地标题行 -->
<view class="venue-header">
<view class="venue-name">{{ venueInfo.name }}</view>
<view class="refresh-link" @click="handleRefresh">刷新</view>
</view>
<!-- 项目筛选 -->
<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-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="card-header">
<view class="player-name">{{ player.name }}</view>
<!-- 已评分显示总分和修改按钮 -->
<view class="action-area" v-if="player.totalScore">
<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-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>
<script>
import dataAdapter from '@/utils/dataAdapter.js'
import config from '@/config/env.config.js'
export default {
data() {
return {
matchInfo: {
name: '',
time: ''
},
venueInfo: {
id: '',
name: ''
},
projectInfo: {
id: '',
name: ''
},
judgeId: '',
projects: [],
currentProjectIndex: 0,
players: [],
scoredCount: 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
}
// 检查是否是裁判长
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,
judgeId: this.judgeId,
venueId: this.venueInfo.id,
projectId: this.projectInfo.id
})
}
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: {
formatDateTime(dateTimeStr) {
if (!dateTimeStr) return ''
try {
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
}
},
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 {
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]
}
})
if (config.debug) {
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' })
} finally {
this.isLoading = false
}
},
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' })
},
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)
}
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 40rpx;
}
/* ==================== 导航栏 ==================== */
.nav-bar {
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 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;
display: flex;
align-items: center;
gap: 20rpx;
}
.nav-dots, .nav-circle {
font-size: 32rpx;
color: #FFFFFF;
}
/* ==================== 比赛信息 ==================== */
.match-info {
padding: 30rpx;
background-color: #F5F5F5;
}
.match-title {
font-size: 32rpx;
font-weight: 600;
color: #1B7C5E;
line-height: 1.5;
margin-bottom: 8rpx;
}
.match-time {
font-size: 28rpx;
color: #333333;
}
/* ==================== 场地卡片 ==================== */
.venue-card {
margin: 0 30rpx 20rpx;
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.venue-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 24rpx;
border-bottom: 4rpx solid #1B7C5E;
margin-bottom: 24rpx;
}
.venue-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.refresh-link {
font-size: 26rpx;
color: #4A90D9;
}
.project-row {
display: flex;
flex-direction: column;
}
.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: 26rpx;
color: #1B7C5E;
background-color: #FFFFFF;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-chip.active {
background-color: #1B7C5E;
color: #FFFFFF;
}
/* ==================== 评分统计 ==================== */
.score-stats {
padding: 20rpx 30rpx;
display: flex;
align-items: center;
}
.stats-label {
font-size: 28rpx;
color: #333333;
}
.stats-value {
font-size: 32rpx;
color: #1B7C5E;
font-weight: 600;
margin-left: 8rpx;
}
/* ==================== 选手卡片 ==================== */
.player-list {
padding: 0 30rpx;
}
.player-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20rpx;
}
.player-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
/* ==================== 操作区域 ==================== */
.action-area {
display: flex;
align-items: center;
gap: 16rpx;
}
.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;
}
.tag-value {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
.modify-btn {
padding: 12rpx 40rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
border-radius: 8rpx;
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
border: none;
line-height: 1.4;
}
/* ==================== 选手详情 ==================== */
.player-details {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.detail-row {
line-height: 1.6;
}
.detail-text {
font-size: 26rpx;
color: #666666;
}
/* ==================== 加载状态 ==================== */
.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

@@ -0,0 +1,695 @@
<template>
<view class="container">
<!-- 自定义导航栏 -->
<view class="nav-bar">
<view class="nav-title">评分系统</view>
<view class="nav-right">
<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">比赛时间{{ formatDateTime(matchInfo.time) }}</view>
</view>
<!-- 场地和项目卡片 -->
<view class="venue-card">
<!-- 场地标题行 -->
<view class="venue-header">
<view class="venue-name">{{ venueInfo.name }}</view>
<view class="refresh-link" @click="handleRefresh">刷新</view>
</view>
<!-- 项目筛选 -->
<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-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"
>
<!-- 已评分状态 -->
<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>
<!-- 未评分状态 -->
<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-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>
<script>
import dataAdapter from '@/utils/dataAdapter.js'
import config from '@/config/env.config.js'
export default {
data() {
return {
matchInfo: {
name: '',
time: ''
},
venueInfo: {
id: '',
name: ''
},
projectInfo: {
id: '',
name: ''
},
judgeId: '',
projects: [],
currentProjectIndex: 0,
players: [],
scoredCount: 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 || ''
}
this.venueInfo = {
id: globalData.venueId,
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
})
}
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: {
formatDateTime(dateTimeStr) {
if (!dateTimeStr) return ''
try {
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
}
},
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
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 = {
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]
}
})
if (config.debug) {
console.log('请求选手列表参数:', params)
}
const response = await dataAdapter.getData('getMyAthletes', 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.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' })
} finally {
this.isLoading = false
}
},
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 = {
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)
}
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 40rpx;
}
/* ==================== 导航栏 ==================== */
.nav-bar {
height: 90rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 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;
display: flex;
align-items: center;
gap: 20rpx;
}
.nav-dots, .nav-circle {
font-size: 32rpx;
color: #FFFFFF;
}
/* ==================== 比赛信息 ==================== */
.match-info {
padding: 30rpx;
background-color: #F5F5F5;
}
.match-title {
font-size: 32rpx;
font-weight: 600;
color: #1B7C5E;
line-height: 1.5;
margin-bottom: 8rpx;
}
.match-time {
font-size: 28rpx;
color: #333333;
}
/* ==================== 场地卡片 ==================== */
.venue-card {
margin: 0 30rpx 20rpx;
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.venue-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 24rpx;
border-bottom: 4rpx solid #1B7C5E;
margin-bottom: 24rpx;
}
.venue-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.refresh-link {
font-size: 26rpx;
color: #4A90D9;
}
.project-row {
display: flex;
flex-direction: column;
}
.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: 26rpx;
color: #1B7C5E;
background-color: #FFFFFF;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-chip.active {
background-color: #1B7C5E;
color: #FFFFFF;
}
/* ==================== 评分统计 ==================== */
.score-stats {
padding: 20rpx 30rpx;
display: flex;
align-items: center;
}
.stats-label {
font-size: 28rpx;
color: #333333;
}
.stats-value {
font-size: 32rpx;
color: #1B7C5E;
font-weight: 600;
margin-left: 8rpx;
}
/* ==================== 选手卡片 ==================== */
.player-list {
padding: 0 30rpx;
}
.player-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20rpx;
}
.player-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
/* ==================== 已评分标签 ==================== */
.score-tags {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.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;
}
.tag-value {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
/* ==================== 等待中状态 ==================== */
.score-tag.waiting {
background-color: #FFF7E6;
border-color: #FFD591;
}
.score-tag.waiting .tag-value {
color: #FA8C16;
font-size: 24rpx;
}
/* ==================== 未评分操作 ==================== */
.action-row {
display: flex;
align-items: center;
}
.score-btn {
padding: 12rpx 40rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
border-radius: 8rpx;
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
border: none;
line-height: 1.4;
}
/* ==================== 选手详情 ==================== */
.player-details {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.detail-row {
line-height: 1.6;
}
.detail-text {
font-size: 26rpx;
color: #666666;
}
/* ==================== 加载状态 ==================== */
.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>

0
src/static/.gitkeep Normal file
View File

73
src/uni.scss Normal file
View File

@@ -0,0 +1,73 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 主色 */
$uni-color-primary: #1B7C5E;
$uni-color-success: #2A9D7E;
$uni-color-warning: #f0ad4e;
$uni-color-error: #FF4D6A;
/* 文字基本颜色 */
$uni-text-color: #333;
$uni-text-color-inverse: #fff;
$uni-text-color-grey: #999;
$uni-text-color-placeholder: #CCCCCC;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #F5F5F5;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1;
$uni-bg-color-mask: rgba(0, 0, 0, 0.4);
/* 边框颜色 */
$uni-border-color: #e5e5e5;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm: 24rpx;
$uni-font-size-base: 28rpx;
$uni-font-size-lg: 32rpx;
/* 图片尺寸 */
$uni-img-size-sm: 40rpx;
$uni-img-size-base: 52rpx;
$uni-img-size-lg: 80rpx;
/* Border Radius */
$uni-border-radius-sm: 4rpx;
$uni-border-radius-base: 8rpx;
$uni-border-radius-lg: 12rpx;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 10rpx;
$uni-spacing-row-base: 20rpx;
$uni-spacing-row-lg: 30rpx;
/* 垂直间距 */
$uni-spacing-col-sm: 8rpx;
$uni-spacing-col-base: 16rpx;
$uni-spacing-col-lg: 24rpx;
/* 透明度 */
$uni-opacity-disabled: 0.3;
/* 文章场景相关 */
$uni-color-title: #2c405a;
$uni-color-subtitle: #555555;
$uni-color-paragraph: #3f536e;

257
src/utils/dataAdapter.js Normal file
View File

@@ -0,0 +1,257 @@
/**
* 数据源适配器(核心文件)
* 根据配置动态选择 Mock数据 或 真实API数据
*
* 这是保护Mock版本UI的核心机制
* - Mock模式使用本地Mock数据不依赖后端UI功能完整
* - API模式调用真实后端接口获取数据库数据
*
* 通过修改 config/env.config.js 中的 dataMode 即可切换模式
*/
import config from '@/config/env.config.js'
/**
* DataAdapter 类
* 单例模式,全局统一管理数据源
*/
class DataAdapter {
constructor() {
this.mode = config.dataMode // 'mock' 或 'api'
this.debug = config.debug
this.mockDelay = config.mockDelay
// 延迟加载,避免循环依赖
this.mockData = null
this.apiService = null
if (this.debug) {
console.log(`[DataAdapter] 初始化完成,当前模式: ${this.mode}`)
}
}
/**
* 延迟加载 Mock 数据模块
*/
async _loadMockData() {
if (!this.mockData) {
const mockModule = await import('@/mock/index.js')
this.mockData = mockModule.default
}
return this.mockData
}
/**
* 延迟加载 API 服务模块
*/
async _loadApiService() {
if (!this.apiService) {
const apiModule = await import('@/api/index.js')
this.apiService = apiModule.default
}
return this.apiService
}
/**
* 统一数据获取接口
* @param {String} resource - 资源名称(如 'login', 'getMyAthletes'
* @param {Object} params - 请求参数
* @returns {Promise} 返回统一格式的响应
*/
async getData(resource, params = {}) {
if (this.mode === 'mock') {
return this._getMockData(resource, params)
} else {
return this._getApiData(resource, params)
}
}
/**
* 获取 Mock 数据
* @private
*/
async _getMockData(resource, params) {
if (this.debug) {
console.log(`[Mock数据] 请求: ${resource}`, params)
}
try {
// 模拟网络延迟
if (this.mockDelay > 0) {
await this._delay(this.mockDelay)
}
// 加载Mock数据模块
const mockData = await this._loadMockData()
// 检查资源是否存在
if (!mockData[resource]) {
throw new Error(`Mock数据中未找到资源: ${resource}`)
}
// 调用Mock数据函数
const data = mockData[resource](params)
if (this.debug) {
console.log(`[Mock数据] 响应: ${resource}`, data)
}
// 返回统一格式
return {
code: 200,
message: '成功',
data,
success: true
}
} catch (error) {
console.error(`[Mock数据] 错误: ${resource}`, error)
throw {
code: 500,
message: error.message || 'Mock数据获取失败',
data: null
}
}
}
/**
* 获取 API 数据
* @private
*/
async _getApiData(resource, params) {
if (this.debug) {
console.log(`[API请求] 请求: ${resource}`, params)
}
try {
// 加载API服务模块
const apiService = await this._loadApiService()
// 检查接口是否存在
if (!apiService[resource]) {
throw new Error(`API服务中未找到接口: ${resource}`)
}
// 调用API接口
const response = await apiService[resource](params)
if (this.debug) {
console.log(`[API请求] 响应: ${resource}`, response)
}
// API响应已经是统一格式由 request.js 处理)
return response
} catch (error) {
console.error(`[API请求] 错误: ${resource}`, error)
// 重新抛出,由调用方处理
throw error
}
}
/**
* 延迟函数(模拟网络请求)
* @private
*/
_delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* 切换数据模式
* @param {String} mode - 'mock' 或 'api'
*/
switchMode(mode) {
if (mode === 'mock' || mode === 'api') {
this.mode = mode
console.log(`[DataAdapter] 数据模式已切换为: ${mode}`)
} else {
console.error('[DataAdapter] 无效的数据模式,只能是 "mock" 或 "api"')
}
}
/**
* 获取当前模式
* @returns {String} 'mock' 或 'api'
*/
getMode() {
return this.mode
}
/**
* 检查是否为Mock模式
* @returns {Boolean}
*/
isMockMode() {
return this.mode === 'mock'
}
/**
* 检查是否为API模式
* @returns {Boolean}
*/
isApiMode() {
return this.mode === 'api'
}
}
// 导出单例
export default new DataAdapter()
/**
* 使用示例:
*
* // 在页面中使用
* import dataAdapter from '@/utils/dataAdapter.js'
*
* export default {
* data() {
* return {
* players: []
* }
* },
*
* async onLoad() {
* try {
* // 获取数据自动根据配置选择Mock或API
* const response = await dataAdapter.getData('getMyAthletes', {
* judgeId: 123,
* venueId: 1,
* projectId: 5
* })
*
* this.players = response.data
* } catch (error) {
* console.error('数据加载失败:', error.message)
* }
* },
*
* methods: {
* // 查看当前模式
* checkMode() {
* console.log('当前数据模式:', dataAdapter.getMode())
* console.log('是否Mock模式:', dataAdapter.isMockMode())
* },
*
* // 动态切换模式(开发调试用)
* toggleMode() {
* const newMode = dataAdapter.isMockMode() ? 'api' : 'mock'
* dataAdapter.switchMode(newMode)
* }
* }
* }
*
* ---
*
* 资源名称resource与Mock/API的映射关系
*
* | resource | Mock函数 | API函数 | 说明 |
* |---------------------|----------------------|---------------------|---------------|
* | login | mockData.login | apiService.login | 登录验证 |
* | getMyAthletes | mockData.getMyAthletes | apiService.getMyAthletes | 选手列表(评委) |
* | getAthletesForAdmin | mockData.getAthletesForAdmin | apiService.getAthletesForAdmin | 选手列表(裁判长) |
* | submitScore | mockData.submitScore | apiService.submitScore | 提交评分 |
* | getScoreDetail | mockData.getScoreDetail | apiService.getScoreDetail | 评分详情 |
* | modifyScore | mockData.modifyScore | apiService.modifyScore | 修改评分 |
* | getDeductions | mockData.getDeductions | apiService.getDeductions | 扣分项列表 |
* | getVenues | mockData.getVenues | apiService.getVenues | 场地列表 |
* | getProjects | mockData.getProjects | apiService.getProjects | 项目列表 |
*/

279
src/utils/request.js Normal file
View File

@@ -0,0 +1,279 @@
/**
* 网络请求封装
* 统一处理HTTP请求、响应、错误、Token等
*
* 特性:
* - 自动添加TokenBlade-Auth格式
* - 统一错误处理
* - 请求/响应拦截
* - 超时控制
* - Loading状态管理
*/
import config from '@/config/env.config.js'
/**
* 构建请求头
* @param {Object} customHeader 自定义头部
* @returns {Object} 完整的请求头
*/
function getHeaders(customHeader = {}) {
const token = uni.getStorageSync('token') || ''
return {
'Content-Type': 'application/json',
// 重要:后端使用 Blade-Auth 而不是 Authorization
'Blade-Auth': token ? `Bearer ${token}` : '',
...customHeader
}
}
/**
* 统一请求方法
* @param {Object} options 请求配置
* @param {String} options.url 请求路径不含baseURL
* @param {String} options.method 请求方法GET/POST/PUT/DELETE
* @param {Object} options.data 请求数据POST/PUT使用
* @param {Object} options.params 查询参数GET使用
* @param {Object} options.header 自定义请求头
* @param {Boolean} options.showLoading 是否显示Loading
* @param {String} options.loadingText Loading文本
* @returns {Promise}
*/
function request(options = {}) {
const {
url = '',
method = 'GET',
data = {},
params = {},
header = {},
showLoading = false,
loadingText = '加载中...'
} = options
// 显示Loading
if (showLoading) {
uni.showLoading({
title: loadingText,
mask: true
})
}
// 打印调试信息
if (config.debug) {
console.log(`[API请求] ${method} ${url}`, method === 'GET' ? params : data)
}
// 构建完整URLGET请求需要拼接查询参数
let fullUrl = config.apiBaseURL + url
let requestData = data
// GET请求将params拼接到URL
if (method === 'GET' && params && Object.keys(params).length > 0) {
// 过滤掉 undefined、null、空字符串的参数
const validParams = Object.keys(params).filter(key => {
const value = params[key]
return value !== undefined && value !== null && value !== ''
})
if (validParams.length > 0) {
const queryString = validParams
.map(key => {
const value = params[key]
// 确保值不是 undefined 字符串
if (typeof value === 'string' && value === 'undefined') {
return null
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
})
.filter(item => item !== null)
.join('&')
if (queryString) {
fullUrl += (url.includes('?') ? '&' : '?') + queryString
}
}
requestData = undefined // GET请求不使用data字段
}
return new Promise((resolve, reject) => {
uni.request({
url: fullUrl,
method,
data: requestData,
header: getHeaders(header),
timeout: config.timeout,
success: (res) => {
if (config.debug) {
console.log(`[API响应] ${method} ${url}`, res.data)
}
// 隐藏Loading
if (showLoading) {
uni.hideLoading()
}
// BladeX框架标准响应格式
// { code: 200, success: true, data: {}, msg: "操作成功" }
if (res.statusCode === 200) {
const response = res.data
// 业务成功
if (response.code === 200 || response.success) {
resolve({
code: 200,
message: response.msg || response.message || '成功',
data: response.data,
success: true
})
} else {
// 业务失败
const errorMsg = response.msg || response.message || '请求失败'
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
})
reject({
code: response.code,
message: errorMsg,
data: response.data
})
}
} else if (res.statusCode === 401) {
// Token过期或未登录
uni.showToast({
title: 'Token已过期请重新登录',
icon: 'none'
})
// 清除Token
uni.removeStorageSync('token')
// 跳转到登录页
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/login'
})
}, 1500)
reject({
code: 401,
message: 'Token已过期'
})
} else {
// HTTP错误
const errorMsg = `请求失败 (${res.statusCode})`
uni.showToast({
title: errorMsg,
icon: 'none'
})
reject({
code: res.statusCode,
message: errorMsg
})
}
},
fail: (err) => {
if (config.debug) {
console.error(`[API错误] ${method} ${url}`, err)
}
// 隐藏Loading
if (showLoading) {
uni.hideLoading()
}
// 网络错误
const errorMsg = err.errMsg || '网络错误,请检查网络连接'
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
})
reject({
code: -1,
message: errorMsg,
error: err
})
}
})
})
}
/**
* GET 请求
*/
export function get(url, params = {}, options = {}) {
return request({
url,
method: 'GET',
params,
...options
})
}
/**
* POST 请求
*/
export function post(url, data = {}, options = {}) {
return request({
url,
method: 'POST',
data,
...options
})
}
/**
* PUT 请求
*/
export function put(url, data = {}, options = {}) {
return request({
url,
method: 'PUT',
data,
...options
})
}
/**
* DELETE 请求
*/
export function del(url, data = {}, options = {}) {
return request({
url,
method: 'DELETE',
data,
...options
})
}
// 默认导出
export default request
/**
* 使用示例:
*
* // 方式1直接使用 request
* import request from '@/utils/request.js'
*
* request({
* url: '/martial/score/list',
* method: 'GET',
* data: { page: 1, size: 10 },
* showLoading: true
* }).then(res => {
* console.log(res.data)
* }).catch(err => {
* console.error(err.message)
* })
*
* // 方式2使用快捷方法
* import { get, post, put, del } from '@/utils/request.js'
*
* // GET请求
* get('/martial/athlete/list', { venueId: 1 })
* .then(res => console.log(res.data))
*
* // POST请求
* post('/martial/score/submit', { athleteId: 1, score: 8.907 })
* .then(res => console.log(res.data))
*/

223
test-h5.html Normal file
View File

@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>H5 部署测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.test-card {
background: white;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.test-title {
font-size: 20px;
font-weight: bold;
color: #1B7C5E;
margin-bottom: 10px;
}
.test-result {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
button {
background-color: #1B7C5E;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #156650;
}
pre {
background-color: #f4f4f4;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
</style>
</head>
<body>
<h1>武术评分系统 H5 部署测试</h1>
<div class="test-card">
<div class="test-title">1. 当前页面信息</div>
<div id="pageInfo"></div>
</div>
<div class="test-card">
<div class="test-title">2. 静态资源测试</div>
<button onclick="testResources()">测试资源加载</button>
<div id="resourceTest"></div>
</div>
<div class="test-card">
<div class="test-title">3. 路径配置检查</div>
<div id="pathCheck"></div>
</div>
<div class="test-card">
<div class="test-title">4. 解决方案</div>
<div id="solutions"></div>
</div>
<script>
// 显示页面信息
function showPageInfo() {
const info = {
'URL': window.location.href,
'协议': window.location.protocol,
'主机': window.location.host,
'路径': window.location.pathname,
'基础路径': document.baseURI
};
let html = '<div class="test-result info">';
for (let key in info) {
html += `<strong>${key}:</strong> ${info[key]}<br>`;
}
html += '</div>';
document.getElementById('pageInfo').innerHTML = html;
}
// 测试资源加载
async function testResources() {
const resultDiv = document.getElementById('resourceTest');
resultDiv.innerHTML = '<div class="test-result info">正在测试...</div>';
const resources = [
'./static/index.css',
'./static/js/chunk-vendors.js',
'./static/js/index.js'
];
let html = '';
let allSuccess = true;
for (let resource of resources) {
try {
const response = await fetch(resource, { method: 'HEAD' });
const contentType = response.headers.get('Content-Type');
if (response.ok) {
html += `<div class="test-result success">
${resource}<br>
状态: ${response.status}<br>
类型: ${contentType}
</div>`;
} else {
html += `<div class="test-result error">
${resource}<br>
状态: ${response.status} ${response.statusText}
</div>`;
allSuccess = false;
}
} catch (error) {
html += `<div class="test-result error">
${resource}<br>
错误: ${error.message}
</div>`;
allSuccess = false;
}
}
if (allSuccess) {
html += '<div class="test-result success"><strong>所有资源加载正常!</strong></div>';
} else {
html += '<div class="test-result error"><strong>部分资源加载失败,请检查文件路径和服务器配置</strong></div>';
}
resultDiv.innerHTML = html;
}
// 检查路径配置
function checkPaths() {
const currentPath = window.location.pathname;
const isSubDir = currentPath.includes('/') && currentPath !== '/';
let html = '<div class="test-result info">';
html += `<strong>当前路径:</strong> ${currentPath}<br>`;
html += `<strong>是否在子目录:</strong> ${isSubDir ? '是' : '否'}<br>`;
if (isSubDir) {
html += '<br><strong>⚠️ 检测到部署在子目录</strong><br>';
html += '需要修改 vue.config.js 中的 publicPath 配置';
} else {
html += '<br><strong>✓ 部署在根目录</strong>';
}
html += '</div>';
document.getElementById('pathCheck').innerHTML = html;
}
// 显示解决方案
function showSolutions() {
const html = `
<div class="test-result info">
<strong>常见问题解决方案:</strong><br><br>
<strong>1. 样式完全丢失</strong><br>
• 检查 static/index.css 文件是否存在<br>
• 检查服务器 MIME 类型配置<br>
• 打开浏览器控制台查看 Network 标签<br><br>
<strong>2. 部署在子目录</strong><br>
• 修改 vue.config.js 的 publicPath<br>
• 重新编译: npm run build:h5<br><br>
<strong>3. Nginx 配置</strong><br>
<pre>location /static/ {
expires 30d;
}
location ~* \\.css$ {
add_header Content-Type text/css;
}</pre><br>
<strong>4. 本地测试</strong><br>
• cd dist/build/h5<br>
• python -m http.server 8000<br>
• 访问 http://localhost:8000<br>
</div>
`;
document.getElementById('solutions').innerHTML = html;
}
// 页面加载时执行
window.onload = function() {
showPageInfo();
checkPaths();
showSolutions();
};
</script>
</body>
</html>

55
vue.config.js Normal file
View File

@@ -0,0 +1,55 @@
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,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/mini': {
target: 'http://localhost:8123',
changeOrigin: true
},
'/martial': {
target: 'http://localhost:8123',
changeOrigin: true
}
}
},
chainWebpack: config => {
// 禁用 gzip 大小报告
if (process.env.NODE_ENV === 'production') {
config.performance.hints(false)
}
// 确保 CSS 文件正确处理
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return options
})
}
}