Compare commits

...

22 Commits

Author SHA1 Message Date
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
43 changed files with 25675 additions and 170 deletions

View File

@@ -9,7 +9,8 @@
"Bash(git checkout:*)", "Bash(git checkout:*)",
"Bash(ls:*)", "Bash(ls:*)",
"Bash(done)", "Bash(done)",
"Bash(cat:*)" "Bash(cat:*)",
"Bash(git push:*)"
], ],
"deny": [], "deny": [],
"ask": [] "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

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,19 +6,22 @@
import request from '@/utils/request.js' import request from '@/utils/request.js'
/** /**
* 获取我的选手列表(普通评委 * 获取选手列表(根据裁判类型返回不同数据
* @param {Object} params * @param {Object} params
* @param {String} params.judgeId - 评委ID * @param {String} params.judgeId - 评委ID
* @param {String} params.venueId - 场地ID * @param {Number} params.refereeType - 裁判类型1-裁判长, 2-普通裁判)
* @param {String} params.projectId - 项目ID * @param {String} params.venueId - 场地ID可选
* @param {String} params.projectId - 项目ID可选
* @returns {Promise} * @returns {Promise}
* *
* 注意:此接口需要后端实现 * 普通裁判:返回待评分的选手列表
* 建议路径: GET /api/mini/athletes * 裁判长:返回已有评分的选手列表
*
* 后端路径: GET /api/mini/score/athletes
*/ */
export function getMyAthletes(params) { export function getMyAthletes(params) {
return request({ return request({
url: '/api/mini/athletes', url: '/mini/score/athletes',
method: 'GET', method: 'GET',
params: params, // GET 请求使用 params params: params, // GET 请求使用 params
showLoading: true showLoading: true
@@ -38,7 +41,7 @@ export function getMyAthletes(params) {
*/ */
export function getAthletesForAdmin(params) { export function getAthletesForAdmin(params) {
return request({ return request({
url: '/api/mini/athletes/admin', url: '/mini/athletes/admin',
method: 'GET', method: 'GET',
params: params, // GET 请求使用 params params: params, // GET 请求使用 params
showLoading: true showLoading: true
@@ -89,54 +92,53 @@ export default {
} }
/** /**
* 后端接口规范(待实现) * 后端接口规范:
* *
* GET /api/mini/athletes * GET /api/mini/score/athletes
* *
* 请求参数: * 请求参数:
* { * {
* "judgeId": "456", * "judgeId": "456",
* "venueId": "1", * "refereeType": 2, // 1-裁判长, 2-普通裁判
* "projectId": "5" * "venueId": "1", // 可选
* "projectId": "5" // 可选
* } * }
* *
* 响应: * 响应(普通裁判 - 待评分选手)
* { * {
* "code": 200, * "code": 200,
* "success": true, * "success": true,
* "msg": "操作成功", * "msg": "操作成功",
* "data": [ * "data": [
* { * {
* "athleteId": "1", * "athleteId": 1,
* "name": "张三", * "name": "张三",
* "idCard": "123456789000000000",
* "team": "少林寺武术大学院",
* "number": "123-4567898275", * "number": "123-4567898275",
* "myScore": 8.906, * "team": "少林寺武术大学院",
* "totalScore": 8.907, * "projectName": "女子组长拳",
* "scored": true, * "orderNum": 1,
* "scoreTime": "2025-06-25 09:15:00" * "competitionStatus": 0
* } * }
* ] * ]
* } * }
* *
* SQL示例 * 响应(裁判长 - 已有评分选手)
* SELECT * {
* a.id AS athleteId, * "code": 200,
* a.player_name AS name, * "success": true,
* a.id_card AS idCard, * "msg": "操作成功",
* a.team_name AS team, * "data": [
* a.player_no AS number, * {
* a.total_score AS totalScore, * "athleteId": 1,
* s.score AS myScore, * "name": "张三",
* CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END AS scored, * "number": "123-4567898275",
* s.score_time AS scoreTime * "team": "少林寺武术大学院",
* FROM martial_athlete a * "projectName": "女子组长拳",
* LEFT JOIN martial_score s * "orderNum": 1,
* ON a.id = s.athlete_id * "totalScore": 8.907,
* AND s.judge_id = #{judgeId} * "scoredJudgeCount": 3,
* WHERE a.venue_id = #{venueId} * "competitionStatus": 2
* 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": "登录成功", * "msg": "登录成功",
* "data": { * "data": {
* "token": "xxx", * "token": "xxx",
* "userRole": "pub", * "refereeType": 2, // 1-裁判长, 2-普通裁判
* "matchId": "123", * "matchId": "123",
* "matchName": "2025年全国武术散打锦标赛...", * "matchName": "2025年全国武术散打锦标赛...",
* "matchTime": "2025年6月25日 9:00", * "matchTime": "2025年6月25日 9:00",

View File

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

View File

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

View File

@@ -4,48 +4,77 @@
*/ */
/** /**
* 获取我的选手列表(普通评委 * 获取选手列表(根据裁判类型返回不同数据
* @param {Object} params * @param {Object} params
* @param {String} params.judgeId - 评委ID * @param {String} params.judgeId - 评委ID
* @param {String} params.venueId - 场地ID * @param {Number} params.refereeType - 裁判类型1-裁判长, 2-普通裁判)
* @param {String} params.projectId - 项目ID * @param {String} params.venueId - 场地ID可选
* @returns {Array} 选手列表(带评分状态 * @param {String} params.projectId - 项目ID可选
* @returns {Array} 选手列表
*/ */
export function getMyAthletes(params) { export function getMyAthletes(params) {
// 模拟3个选手数据 const { refereeType } = params
// 裁判长:返回已有评分的选手
if (refereeType === 1) {
return [
{
athleteId: 1,
name: '张三',
number: '123-4567898275',
team: '少林寺武术大学院',
projectName: '女子组长拳',
orderNum: 1,
totalScore: 8.907,
scoredJudgeCount: 6,
competitionStatus: 2
},
{
athleteId: 2,
name: '李四',
number: '123-4567898276',
team: '武当山武术学院',
projectName: '女子组长拳',
orderNum: 2,
totalScore: 8.902,
scoredJudgeCount: 6,
competitionStatus: 2
},
{
athleteId: 4,
name: '赵六',
number: '123-4567898278',
team: '华山武术学院',
projectName: '女子组长拳',
orderNum: 4,
totalScore: 8.899,
scoredJudgeCount: 5,
competitionStatus: 2
}
]
}
// 普通裁判:返回待评分的选手
return [ return [
{ {
athleteId: '1', athleteId: 3,
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: '王五', name: '王五',
idCard: '123456789000000002', idCard: '123456789000000002',
team: '峨眉派武术学校', team: '峨眉派武术学校',
number: '123-4567898277', number: '123-4567898277',
myScore: null, // 未评分 projectName: '女子组长拳',
totalScore: null, orderNum: 3,
scored: false, competitionStatus: 0
scoreTime: null },
{
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登录数据 // 返回Mock登录数据
return { return {
token: 'mock_token_' + Date.now(), token: 'mock_token_' + Date.now(),
userRole: role, // 'pub' 或 'admin' refereeType: role === 'pub' ? 2 : 1, // 1-裁判长, 2-普通裁判
matchId: '123', matchId: matchCode || '200', // 使用传入的比赛编码默认200
matchName: '2025年全国武术散打锦标赛暨第十七届世界武术锦标赛选拔赛', matchName: '2025年全国武术散打锦标赛暨第十七届世界武术锦标赛选拔赛',
matchTime: '2025年6月25日 9:00', matchTime: '2025年6月25日 9:00',
judgeId: '456', judgeId: '456',

View File

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

20142
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,5 +3,50 @@
"name": "martial-admin-mini", "name": "martial-admin-mini",
"version": "1.0.0", "version": "1.0.0",
"description": "武术比赛评分系统", "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-loader": "^3.0.0",
"sass": "^1.96.0",
"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",
"vue-template-compiler": "^2.6.14"
},
"browserslist": [
"Android >= 4.4",
"ios >= 9"
]
} }

View File

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

View File

@@ -107,6 +107,8 @@ export default {
}, },
judgeId: '', judgeId: '',
projectId: '', projectId: '',
competitionId: '',
venueId: '',
currentScore: 8.000, currentScore: 8.000,
note: '', note: '',
minScore: 5.0, minScore: 5.0,
@@ -137,10 +139,9 @@ export default {
// 加载评委ID和项目ID // 加载评委ID和项目ID
this.judgeId = globalData.judgeId this.judgeId = globalData.judgeId
const projects = globalData.projects || [] this.projectId = globalData.currentProjectId || ''
const currentIndex = globalData.currentProjectIndex || 0 this.competitionId = globalData.matchId || globalData.matchCode || ''
const currentProject = projects[currentIndex] || {} this.venueId = globalData.currentVenueId || globalData.venueId || ''
this.projectId = currentProject.projectId
// 调试信息 // 调试信息
if (config.debug) { if (config.debug) {
@@ -148,6 +149,8 @@ export default {
athlete: this.player, athlete: this.player,
judgeId: this.judgeId, judgeId: this.judgeId,
projectId: this.projectId, projectId: this.projectId,
competitionId: this.competitionId,
venueId: this.venueId,
initialScore: this.currentScore initialScore: this.currentScore
}) })
} }
@@ -166,9 +169,12 @@ export default {
projectId: this.projectId projectId: this.projectId
}) })
// 为每个扣分项添加 checked 状态 // 为每个扣分项添加 checked 状态,并映射字段名
this.deductions = (response.data || []).map(item => ({ const records = response.data?.records || []
...item, this.deductions = records.map(item => ({
deductionId: item.id,
deductionName: item.itemName,
deductionScore: parseFloat(item.deductionPoint || 0),
checked: false checked: false
})) }))
@@ -187,7 +193,20 @@ export default {
}, },
goBack() { goBack() {
uni.navigateBack() if (config.debug) {
console.log('返回上一页')
}
uni.navigateBack({
delta: 1,
fail: (err) => {
console.error('返回失败:', err)
// 如果返回失败,尝试跳转到评分列表页
uni.redirectTo({
url: '/pages/score-list/score-list'
})
}
})
}, },
decreaseScore() { decreaseScore() {
@@ -216,14 +235,27 @@ export default {
return 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 const selectedDeductions = this.deductions
.filter(item => item.checked) .filter(item => item.checked)
.map(item => ({ .map(item => item.deductionId)
deductionId: item.deductionId,
deductionName: item.deductionName,
deductionScore: item.deductionScore
}))
try { try {
uni.showLoading({ uni.showLoading({
@@ -231,16 +263,27 @@ export default {
mask: true mask: true
}) })
// 🔥 关键改动:使用 dataAdapter 提交评分 // 准备提交数据
// Mock模式调用 mock/score.js 的 submitScore 函数 const submitData = {
// API模式调用 api/score.js 的 submitScore 函数POST /martial/score/submit
const response = await dataAdapter.getData('submitScore', {
athleteId: this.player.athleteId, athleteId: this.player.athleteId,
judgeId: this.judgeId, judgeId: this.judgeId,
projectId: this.projectId,
competitionId: this.competitionId,
venueId: this.venueId,
score: this.currentScore, score: this.currentScore,
deductions: selectedDeductions, deductions: selectedDeductions,
note: this.note 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() uni.hideLoading()
@@ -301,12 +344,19 @@ export default {
.nav-left { .nav-left {
position: absolute; position: absolute;
left: 30rpx; left: 0;
width: 60rpx; top: 0;
height: 60rpx; width: 120rpx;
height: 90rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 10;
cursor: pointer;
}
.nav-left:active {
opacity: 0.6;
} }
.back-icon { .back-icon {

View File

@@ -18,11 +18,27 @@
<!-- 场地和项目选择 --> <!-- 场地和项目选择 -->
<view class="venue-section"> <view class="venue-section">
<view class="venue-header"> <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>
<view class="project-section"> <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>
</view> </view>
@@ -39,28 +55,29 @@
class="player-card" class="player-card"
v-for="player in players" v-for="player in players"
:key="player.athleteId" :key="player.athleteId"
@click="handlePlayerClick(player)"
> >
<view class="player-header"> <view class="player-header">
<view class="player-name">{{ player.name }}</view> <view class="player-name">{{ player.name }}</view>
<!-- 已评分显示我的评分和总分 --> <!-- 裁判长显示总分和已评分裁判数 -->
<view class="player-scores" v-if="player.scored"> <view class="player-scores" v-if="refereeType === 1">
<text class="my-score">我的评{{ player.myScore }}</text> <text class="total-score">{{ player.totalScore || '未评分' }}</text>
<text class="total-score">{{ player.totalScore }}</text> <text class="judge-count">已评{{ player.scoredJudgeCount || 0 }}</text>
</view> </view>
<!-- 未评分显示评分按钮 --> <!-- 普通裁判显示评分按钮 -->
<button <button
class="score-btn" class="score-btn"
v-else v-else
@click="goToScoreDetail(player)" @click.stop="goToScoreDetail(player)"
> >
评分 评分
</button> </button>
</view> </view>
<view class="player-info"> <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.team }}</view>
<view class="info-item">编号{{ player.number }}</view> <view class="info-item">编号{{ player.number }}</view>
</view> </view>
@@ -89,6 +106,12 @@ export default {
name: '' name: ''
}, },
judgeId: '', judgeId: '',
matchId: '',
refereeType: 2, // 裁判类型1-裁判长, 2-普通裁判)
venues: [], // 所有场地列表
currentVenueIndex: 0, // 当前选中的场地索引
projects: [], // 所有项目列表
currentProjectIndex: 0, // 当前选中的项目索引
players: [], players: [],
scoredCount: 0, scoredCount: 0,
totalCount: 0 totalCount: 0
@@ -96,44 +119,96 @@ export default {
}, },
async onLoad() { async onLoad() {
// 获取全局数据 try {
const app = getApp() // 获取全局数据
const globalData = app.globalData || {} const app = getApp()
const globalData = app.globalData || {}
// 加载比赛信息 // 加载比赛信息
this.matchInfo = { this.matchInfo = {
name: globalData.matchName || '比赛名称', name: globalData.matchName || '比赛名称',
time: globalData.matchTime || '比赛时间' time: globalData.matchTime || '比赛时间'
} }
// 加载场地信息 this.judgeId = globalData.judgeId
this.venueInfo = { this.matchId = globalData.matchId || globalData.matchCode
id: globalData.venueId, this.refereeType = globalData.refereeType || 2 // 默认为普通裁判
name: globalData.venueName || '场地'
}
// 加载项目信息 // 调试信息
const projects = globalData.projects || [] if (config.debug) {
const currentIndex = globalData.currentProjectIndex || 0 console.log('初始化数据:', {
const currentProject = projects[currentIndex] || {} judgeId: this.judgeId,
this.projectInfo = { matchId: this.matchId,
id: currentProject.projectId, matchCode: globalData.matchCode,
name: currentProject.projectName || '项目' refereeType: this.refereeType
} })
}
this.judgeId = globalData.judgeId // 检查必要参数
if (!this.matchId) {
throw new Error('缺少比赛ID请重新登录')
}
// 调试信息 // 显示加载提示
if (config.debug) { uni.showLoading({
console.log('评分列表页加载:', { title: '加载中...',
judgeId: this.judgeId, mask: true
venueId: this.venueInfo.id, })
projectId: this.projectInfo.id
// 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,
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'
}) })
} }
// 加载选手列表
await this.loadPlayers()
}, },
methods: { methods: {
@@ -146,9 +221,10 @@ export default {
// 🔥 关键改动:使用 dataAdapter 获取选手列表 // 🔥 关键改动:使用 dataAdapter 获取选手列表
// Mock模式调用 mock/athlete.js 的 getMyAthletes 函数 // 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', { const response = await dataAdapter.getData('getMyAthletes', {
judgeId: this.judgeId, judgeId: this.judgeId,
refereeType: this.refereeType, // 传递裁判类型
venueId: this.venueInfo.id, venueId: this.venueInfo.id,
projectId: this.projectInfo.id projectId: this.projectInfo.id
}) })
@@ -182,14 +258,108 @@ export default {
} }
}, },
/**
* 处理选手卡片点击
* - 裁判长:跳转到查看详情页面
* - 普通裁判:不处理(通过评分按钮跳转)
*/
handlePlayerClick(player) {
if (this.refereeType === 1) {
// 裁判长:查看评分详情
this.goToScoreDetail(player)
}
// 普通裁判不处理卡片点击,只能通过评分按钮跳转
},
goToScoreDetail(player) { goToScoreDetail(player) {
// 保存当前选手信息到全局数据 // 保存当前选手信息、项目ID和场地ID到全局数据
const app = getApp() const app = getApp()
app.globalData.currentAthlete = player 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({ uni.navigateTo({
url: '/pages/score-detail/score-detail' 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 +449,41 @@ export default {
.venue-header { .venue-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 20rpx;
margin-bottom: 30rpx; margin-bottom: 30rpx;
padding-bottom: 20rpx; padding-bottom: 20rpx;
border-bottom: 4rpx solid #1B7C5E; 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 { .venue-tab {
font-size: 32rpx; padding: 20rpx 40rpx;
font-weight: 600; font-size: 28rpx;
color: #333333; font-weight: 500;
position: relative; 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 { .venue-tab:active {
content: ''; opacity: 0.7;
position: absolute; }
bottom: -24rpx;
left: 0; .venue-tab.active {
right: 0; font-size: 32rpx;
height: 4rpx; font-weight: 600;
color: #FFFFFF;
background-color: #1B7C5E; background-color: #1B7C5E;
} }
@@ -310,7 +495,15 @@ export default {
.project-section { .project-section {
display: flex; display: flex;
align-items: center; 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 { .project-btn {
@@ -321,6 +514,14 @@ export default {
font-size: 28rpx; font-size: 28rpx;
color: #1B7C5E; color: #1B7C5E;
font-weight: 500; 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 { .project-btn.active {
@@ -402,6 +603,12 @@ export default {
font-weight: 600; font-weight: 600;
} }
.judge-count {
font-size: 24rpx;
color: #1B7C5E;
font-weight: 500;
}
.action-area { .action-area {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

7
postcss.config.js 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>

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

@@ -0,0 +1,148 @@
/**
* 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: '/api/mini/athletes',
method: 'GET',
params: params, // GET 请求使用 params
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: '/api/mini/athletes/admin',
method: 'GET',
params: params, // GET 请求使用 params
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: '/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: '/martial/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: `/api/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: '/api/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;
}

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

@@ -0,0 +1,70 @@
/**
* 环境配置文件
* 控制应用的数据源模式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'时使用)
apiBaseURL: 'http://localhost:8123',
// 请求超时时间(毫秒)
timeout: 30000,
},
// 测试环境配置
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
}

45
src/pages.json Normal file
View File

@@ -0,0 +1,45 @@
{
"pages": [
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "pages/score-list/score-list",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"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"
}
}

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

@@ -0,0 +1,304 @@
<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 = {
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,588 @@
<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">{{ 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.005</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 class="modify-tip">
裁判长修改保留3位小数点超过上限或下限时按钮置灰
</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: 8.000,
originalScore: 8.000,
note: '',
minScore: 5.0,
maxScore: 10.0
}
},
async onLoad() {
// 获取全局数据
const app = getApp()
const globalData = app.globalData || {}
// 获取当前选手信息(从 score-list-multi 页面传递)
const currentAthlete = globalData.currentAthlete || {}
// 获取裁判长ID
this.modifierId = globalData.judgeId
// 调试信息
if (config.debug) {
console.log('修改评分页加载:', {
athleteId: currentAthlete.athleteId,
modifierId: this.modifierId
})
}
// 加载选手评分详情
if (currentAthlete.athleteId) {
await this.loadScoreDetail(currentAthlete.athleteId)
}
},
methods: {
async loadScoreDetail(athleteId) {
try {
uni.showLoading({
title: '加载中...',
mask: true
})
// 🔥 关键改动:使用 dataAdapter 获取评分详情
// Mock模式调用 mock/score.js 的 getScoreDetail 函数
// API模式调用 api/score.js 的 getScoreDetail 函数GET /api/mini/score/detail/{athleteId}
const response = await dataAdapter.getData('getScoreDetail', {
athleteId: athleteId
})
uni.hideLoading()
// 保存选手信息和评分详情
this.athleteInfo = response.data.athleteInfo || {}
this.judgeScores = response.data.judgeScores || []
this.modification = response.data.modification || null
// 设置初始分数
this.originalScore = this.athleteInfo.totalScore || 8.000
this.currentScore = this.originalScore
// 如果之前已修改过,加载修改后的分数
if (this.modification && this.modification.modifiedScore) {
this.currentScore = this.modification.modifiedScore
}
// 调试信息
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)
uni.showToast({
title: error.message || '加载失败',
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))
}
},
async handleModify() {
// 验证评分范围
if (this.currentScore < this.minScore || this.currentScore > this.maxScore) {
uni.showToast({
title: `评分必须在${this.minScore}-${this.maxScore}分之间`,
icon: 'none'
})
return
}
// 检查是否有修改
if (this.currentScore === this.originalScore && !this.note) {
uni.showToast({
title: '请修改分数或填写备注',
icon: 'none'
})
return
}
try {
uni.showLoading({
title: '提交中...',
mask: true
})
// 🔥 关键改动:使用 dataAdapter 修改评分
// Mock模式调用 mock/score.js 的 modifyScore 函数
// API模式调用 api/score.js 的 modifyScore 函数PUT /api/mini/score/modify
const response = await dataAdapter.getData('modifyScore', {
athleteId: this.athleteInfo.athleteId,
modifierId: this.modifierId,
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 {
font-size: 26rpx;
color: #333333;
}
.judge-name {
color: #666666;
}
.judge-score {
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,577 @@
<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 || {}
// 加载当前选手信息(从 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 /martial/deductionItem/list
const response = await dataAdapter.getData('getDeductions', {
projectId: this.projectId
})
// 为每个扣分项添加 checked 状态
this.deductions = (response.data || []).map(item => ({
...item,
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
const selectedDeductions = this.deductions
.filter(item => item.checked)
.map(item => item.deductionId)
try {
uni.showLoading({
title: '提交中...',
mask: true
})
// 🔥 关键改动:使用 dataAdapter 提交评分
// Mock模式调用 mock/score.js 的 submitScore 函数
// API模式调用 api/score.js 的 submitScore 函数POST /martial/score/submit
const response = await dataAdapter.getData('submitScore', {
athleteId: this.player.athleteId,
judgeId: this.judgeId,
score: this.currentScore,
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,547 @@
<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="match-info">
<view class="match-title">{{ matchInfo.name }}</view>
<view class="match-time">比赛时间{{ matchInfo.time }}</view>
</view>
<!-- 场地和项目选择 -->
<view class="venue-section">
<!-- 场地切换 - 横向滚动 -->
<scroll-view class="venue-scroll" scroll-x="true" show-scrollbar="false">
<view class="venue-tabs">
<view
v-for="venue in venues"
:key="venue.venueId"
:class="['venue-tab', currentVenue === venue.venueId ? 'active' : '']"
@click="switchVenue(venue.venueId)"
>
{{ venue.venueName }}
</view>
</view>
</scroll-view>
<view class="venue-tip">
<!-- <text class="tip-bold">裁判长可看见所有场地和项目</text> -->
<!-- <text class="tip-normal">场地和项目可动态全部可以点击切换</text> -->
</view>
<!-- 项目选择 - 横向滚动 -->
<scroll-view class="project-scroll" scroll-x="true" show-scrollbar="false">
<view class="project-list">
<view
v-for="(project, index) in projects"
:key="project.projectId"
:class="['project-btn', currentProject === project.projectId ? 'active' : '']"
@click="switchProject(project.projectId)"
>
{{ project.projectName }}
</view>
</view>
</scroll-view>
</view>
<!-- 已评分统计 -->
<view class="score-stats">
<text class="stats-text">已评分</text>
<text class="stats-number">{{ scoredCount }}/{{ totalCount }}</text>
</view>
<!-- 选手列表 -->
<view class="player-list">
<!-- 遍历选手列表 -->
<view
class="player-card"
v-for="player in players"
:key="player.athleteId"
>
<view class="player-header">
<view class="player-name">{{ player.name }}</view>
<!-- 已评分显示总分和修改按钮 -->
<view class="action-area" v-if="player.totalScore">
<text class="total-score">总分{{ player.totalScore }}</text>
<view class="chief-actions">
<!-- <text class="chief-hint">裁判长功能修改评分修改按钮需等总分出来才出现</text> -->
<button class="modify-btn" @click="goToModify(player)">修改</button>
</view>
</view>
</view>
<view class="player-info">
<view class="info-item">身份证{{ player.idCard }}</view>
<view class="info-item">队伍{{ player.team }}</view>
<view class="info-item">编号{{ player.number }}</view>
</view>
</view>
</view>
</view>
</template>
<script>
import dataAdapter from '@/utils/dataAdapter.js'
import config from '@/config/env.config.js'
export default {
data() {
return {
matchInfo: {
id: '',
name: '',
time: ''
},
competitionId: '',
currentVenue: '',
currentProject: '',
venues: [],
projects: [],
players: [],
scoredCount: 0,
totalCount: 0
}
},
async onLoad() {
// 获取全局数据
const app = getApp()
const globalData = app.globalData || {}
// 加载比赛信息
this.matchInfo = {
id: globalData.matchId,
name: globalData.matchName || '比赛名称',
time: globalData.matchTime || '比赛时间'
}
// 注意:裁判长没有固定场地和项目,需要查看所有
this.competitionId = globalData.matchId
// 调试信息
if (config.debug) {
console.log('裁判长列表页加载:', {
userRole: globalData.userRole,
competitionId: this.competitionId
})
}
// 加载场地和项目列表
await this.loadVenuesAndProjects()
},
methods: {
async loadVenuesAndProjects() {
try {
uni.showLoading({
title: '加载中...',
mask: true
})
// 🔥 关键改动:使用 dataAdapter 获取场地列表
// Mock模式调用 mock/athlete.js 的 getVenues 函数
// API模式调用 api/athlete.js 的 getVenues 函数GET /martial/venue/list
const venuesRes = await dataAdapter.getData('getVenues', {
competitionId: this.competitionId
})
// 🔥 关键改动:使用 dataAdapter 获取项目列表
// Mock模式调用 mock/athlete.js 的 getProjects 函数
// API模式调用 api/athlete.js 的 getProjects 函数GET /martial/project/list
const projectsRes = await dataAdapter.getData('getProjects', {
competitionId: this.competitionId
})
this.venues = venuesRes.data || []
this.projects = projectsRes.data || []
// 默认选中第一个场地和项目
if (this.venues.length > 0) {
this.currentVenue = this.venues[0].venueId
}
if (this.projects.length > 0) {
this.currentProject = this.projects[0].projectId
}
uni.hideLoading()
// 调试信息
if (config.debug) {
console.log('场地和项目加载成功:', {
venues: this.venues.length,
projects: this.projects.length,
currentVenue: this.currentVenue,
currentProject: this.currentProject
})
}
// 加载选手列表
if (this.currentVenue && this.currentProject) {
await this.loadPlayers()
}
} catch (error) {
uni.hideLoading()
console.error('加载场地和项目失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
}
},
async loadPlayers() {
try {
uni.showLoading({
title: '加载中...',
mask: true
})
// 🔥 关键改动:使用 dataAdapter 获取选手列表(裁判长视图)
// Mock模式调用 mock/athlete.js 的 getAthletesForAdmin 函数
// API模式调用 api/athlete.js 的 getAthletesForAdmin 函数GET /api/mini/athletes/admin
const response = await dataAdapter.getData('getAthletesForAdmin', {
competitionId: this.competitionId,
venueId: this.currentVenue,
projectId: this.currentProject
})
uni.hideLoading()
// 保存选手列表
this.players = response.data || []
// 计算评分统计(裁判长视图:统计有总分的选手)
this.totalCount = this.players.length
this.scoredCount = this.players.filter(p => p.totalScore).length
// 调试信息
if (config.debug) {
console.log('选手列表加载成功:', {
venueId: this.currentVenue,
projectId: this.currentProject,
total: this.totalCount,
scored: this.scoredCount,
players: this.players
})
}
} catch (error) {
uni.hideLoading()
console.error('加载选手列表失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
}
},
async switchVenue(venueId) {
if (this.currentVenue === venueId) return
this.currentVenue = venueId
// 调试信息
if (config.debug) {
console.log('切换场地:', venueId)
}
// 重新加载选手列表
await this.loadPlayers()
},
async switchProject(projectId) {
if (this.currentProject === projectId) return
this.currentProject = projectId
// 调试信息
if (config.debug) {
console.log('切换项目:', projectId)
}
// 重新加载选手列表
await this.loadPlayers()
},
goToModify(player) {
// 保存当前选手信息到全局数据
const app = getApp()
app.globalData.currentAthlete = player
uni.navigateTo({
url: '/pages/modify-score/modify-score'
})
}
}
}
</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;
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;
}
/* 比赛信息 */
.match-info {
padding: 30rpx;
background-color: #F5F5F5;
}
.match-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
line-height: 1.6;
margin-bottom: 10rpx;
}
.tip-text {
font-size: 24rpx;
color: #FF4D6A;
margin-bottom: 10rpx;
}
.match-time {
font-size: 28rpx;
color: #666666;
}
/* 场地和项目区域 */
.venue-section {
background-color: #FFFFFF;
margin: 20rpx 30rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
/* 场地滚动容器 */
.venue-scroll {
width: 100%;
white-space: nowrap;
margin-bottom: 20rpx;
}
.venue-tabs {
display: inline-flex;
gap: 30rpx;
padding-bottom: 20rpx;
border-bottom: 4rpx solid #E0E0E0;
position: relative;
}
.venue-tab {
font-size: 32rpx;
font-weight: 500;
color: #666666;
padding: 0 20rpx;
position: relative;
white-space: nowrap;
flex-shrink: 0;
}
.venue-tab.active {
font-weight: 600;
color: #333333;
}
.venue-tab.active::after {
content: '';
position: absolute;
bottom: -24rpx;
left: 0;
right: 0;
height: 4rpx;
background-color: #1B7C5E;
}
.venue-tip {
font-size: 24rpx;
line-height: 1.6;
margin-bottom: 20rpx;
}
.tip-bold {
color: #FF4D6A;
font-weight: 500;
}
.tip-normal {
color: #FF4D6A;
}
/* 项目滚动容器 */
.project-scroll {
width: 100%;
white-space: nowrap;
}
.project-list {
display: inline-flex;
gap: 20rpx;
}
.project-btn {
padding: 20rpx 30rpx;
background-color: #FFFFFF;
border: 2rpx solid #CCCCCC;
border-radius: 8rpx;
font-size: 26rpx;
color: #666666;
white-space: nowrap;
flex-shrink: 0;
}
.project-btn.active {
background-color: #1B7C5E;
color: #FFFFFF;
border-color: #1B7C5E;
font-weight: 500;
}
/* 评分统计 */
.score-stats {
padding: 20rpx 30rpx;
font-size: 28rpx;
color: #333333;
}
.stats-text {
color: #666666;
}
.stats-number {
color: #1B7C5E;
font-weight: 600;
}
/* 选手列表 */
.player-list {
padding: 0 30rpx;
}
.player-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.player-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;
flex-direction: column;
align-items: flex-end;
gap: 10rpx;
}
.total-score {
font-size: 26rpx;
color: #333333;
font-weight: 600;
}
.chief-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10rpx;
}
.chief-hint {
font-size: 22rpx;
color: #FF4D6A;
text-align: right;
line-height: 1.5;
max-width: 400rpx;
}
.modify-btn {
padding: 12rpx 40rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
border-radius: 8rpx;
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
}
.modify-btn:active {
opacity: 0.9;
}
.player-info {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.info-item {
font-size: 26rpx;
color: #666666;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,525 @@
<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="match-info">
<view class="match-title">{{ matchInfo.name }}</view>
<view class="match-time">比赛时间{{ matchInfo.time }}</view>
</view>
<!-- 场地和项目选择 -->
<view class="venue-section">
<view class="venue-header">
<view class="venue-tab active">{{ venueInfo.name }}</view>
</view>
<view class="project-section">
<view
class="project-btn"
:class="{ active: index === currentProjectIndex }"
v-for="(project, index) in projects"
:key="project.projectId"
@click="switchProject(index)"
>
{{ project.projectName }}
</view>
</view>
</view>
<!-- 已评分统计 -->
<view class="score-stats">
<text class="stats-text">已评分</text>
<text class="stats-number">{{ scoredCount }}/{{ totalCount }}</text>
</view>
<!-- 选手列表 -->
<view class="player-list">
<!-- 遍历选手列表 -->
<view
class="player-card"
v-for="player in players"
:key="player.athleteId"
>
<view class="player-header">
<view class="player-name">{{ player.name }}</view>
<!-- 已评分显示我的评分和总分 -->
<view class="player-scores" v-if="player.scored">
<text class="my-score">我的评分{{ player.myScore }}</text>
<text class="total-score">总分{{ player.totalScore }}</text>
</view>
<!-- 未评分显示评分按钮 -->
<button
class="score-btn"
v-else
@click="goToScoreDetail(player)"
>
评分
</button>
</view>
<view class="player-info">
<view class="info-item">身份证{{ player.idCard }}</view>
<view class="info-item">队伍{{ player.team }}</view>
<view class="info-item">编号{{ player.number }}</view>
</view>
</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
}
},
async onLoad() {
// 获取全局数据
const app = getApp()
const globalData = app.globalData || {}
// 加载比赛信息
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,
projectsCount: this.projects.length
})
}
// 加载选手列表
await this.loadPlayers()
},
methods: {
async loadPlayers() {
try {
uni.showLoading({
title: '加载中...',
mask: true
})
// 🔥 关键改动:使用 dataAdapter 获取选手列表
// Mock模式调用 mock/athlete.js 的 getMyAthletes 函数
// API模式调用 api/athlete.js 的 getMyAthletes 函数GET /api/mini/athletes
// 构建请求参数
// 优先使用 matchCode比赛编码这样后端可以根据邀请码关联查询
const app = getApp()
const globalData = app.globalData || {}
const params = {
// 方案1使用比赛编码推荐后端可以根据邀请码关联
matchCode: globalData.matchCode,
// 方案2使用具体的ID作为备选
judgeId: this.judgeId,
venueId: this.venueInfo.id,
projectId: this.projectInfo.id
}
// 移除无效参数
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)
uni.hideLoading()
// 保存选手列表
this.players = response.data || []
// 计算评分统计
this.totalCount = this.players.length
this.scoredCount = this.players.filter(p => p.scored).length
// 调试信息
if (config.debug) {
console.log('选手列表加载成功:', {
total: this.totalCount,
scored: this.scoredCount,
players: this.players
})
}
} catch (error) {
uni.hideLoading()
console.error('加载选手列表失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
}
},
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 || '项目'
}
},
/**
* 切换项目
* @param {Number} index - 项目索引
*/
async switchProject(index) {
// 如果点击的是当前项目,不做处理
if (index === this.currentProjectIndex) {
return
}
// 更新当前项目索引
this.currentProjectIndex = index
// 更新全局数据中的项目索引
const app = getApp()
app.globalData.currentProjectIndex = index
// 更新当前项目信息
this.updateCurrentProject()
// 调试信息
if (config.debug) {
console.log('切换项目:', {
index: index,
projectId: this.projectInfo.id,
projectName: this.projectInfo.name
})
}
// 重新加载选手列表
await this.loadPlayers()
}
}
}
</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;
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;
}
/* 比赛信息 */
.match-info {
padding: 30rpx;
background-color: #F5F5F5;
}
.match-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
line-height: 1.6;
margin-bottom: 10rpx;
}
.tip-text {
font-size: 24rpx;
color: #FF4D6A;
margin-bottom: 10rpx;
}
.match-time {
font-size: 28rpx;
color: #666666;
}
/* 场地和项目区域 */
.venue-section {
background-color: #FFFFFF;
margin: 20rpx 30rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.venue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 4rpx solid #1B7C5E;
}
.venue-tab {
font-size: 32rpx;
font-weight: 600;
color: #333333;
position: relative;
}
.venue-tab.active::after {
content: '';
position: absolute;
bottom: -24rpx;
left: 0;
right: 0;
height: 4rpx;
background-color: #1B7C5E;
}
.refresh-hint {
font-size: 24rpx;
color: #FF4D6A;
}
.project-section {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20rpx;
}
.project-btn {
padding: 20rpx 40rpx;
background-color: #FFFFFF;
border: 2rpx solid #1B7C5E;
border-radius: 8rpx;
font-size: 28rpx;
color: #1B7C5E;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.project-btn:active {
opacity: 0.7;
}
.project-btn.active {
background-color: #1B7C5E;
color: #FFFFFF;
}
.project-tip {
font-size: 22rpx;
color: #FF4D6A;
flex: 1;
margin-left: 20rpx;
line-height: 1.5;
}
/* 评分统计 */
.score-stats {
padding: 20rpx 30rpx;
font-size: 28rpx;
color: #333333;
}
.stats-text {
color: #666666;
}
.stats-number {
color: #1B7C5E;
font-weight: 600;
}
.warning-tip {
padding: 0 30rpx 20rpx;
font-size: 24rpx;
color: #FF4D6A;
}
/* 选手列表 */
.player-list {
padding: 0 30rpx;
}
.player-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
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: 32rpx;
font-weight: 600;
color: #333333;
}
.player-scores {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
}
.my-score {
font-size: 26rpx;
color: #666666;
}
.total-score {
font-size: 26rpx;
color: #333333;
font-weight: 600;
}
.action-area {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
}
.chief-hint {
font-size: 24rpx;
color: #FF4D6A;
}
.score-btn {
padding: 12rpx 40rpx;
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
border-radius: 8rpx;
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
}
.score-btn:active {
opacity: 0.9;
}
.player-info {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.info-item {
font-size: 26rpx;
color: #666666;
line-height: 1.5;
}
</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>

44
vue.config.js Normal file
View File

@@ -0,0 +1,44 @@
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,
open: true,
overlay: {
warnings: false,
errors: 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
})
}
}