Compare commits
47 Commits
feature/ap
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11eb5f2db8 | ||
|
|
56a33707d5 | ||
|
|
1b305fc2bd | ||
|
|
a780ee6b2c | ||
|
|
90ee38a57b | ||
|
|
84b84dd951 | ||
|
|
941112dd4c | ||
|
|
314b507748 | ||
|
|
a3680f7d3e | ||
|
|
711779dc57 | ||
|
|
edd64cda47 | ||
|
|
88a931976d | ||
|
|
96bc2d92a2 | ||
|
|
a9c5c4a904 | ||
|
|
5349b80cf8 | ||
|
|
56c1320e40 | ||
|
|
c5c31e8088 | ||
|
|
c978a5bf64 | ||
|
|
f9efd8baa8 | ||
|
|
53c865a076 | ||
|
|
569f8a14d1 | ||
|
|
bcf040bb15 | ||
|
|
7620d9bf96 | ||
|
|
736aa08fba | ||
|
|
e8a2a5cef6 | ||
| 6f3b8db273 | |||
| eaac987a5c | |||
| b7b8947939 | |||
|
|
76fd02661c | ||
|
|
8abfd386fd | ||
|
|
eebbb4fbce | ||
|
|
137139b973 | ||
|
|
39bc88ce6d | ||
|
|
8c56251d72 | ||
|
|
cf3f0bc13b | ||
|
|
afaaf09a61 | ||
|
|
cb3f70966e | ||
|
|
4deed1199d | ||
|
|
a0f7a6a757 | ||
|
|
6ea1c0ca8e | ||
|
|
7f304e012a | ||
|
|
1c3332aea9 | ||
|
|
4d492f3fea | ||
|
|
076ec9b7c3 | ||
|
|
e49f9b3de9 | ||
|
|
92aa0cdf11 | ||
| 5cc95ec72b |
@@ -9,7 +9,8 @@
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(done)",
|
||||
"Bash(cat:*)"
|
||||
"Bash(cat:*)",
|
||||
"Bash(git push:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
51
.drone.yml
Normal file
51
.drone.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: martial-admin-mini
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
- master
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: node:18-alpine
|
||||
environment:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
commands:
|
||||
- npm install --legacy-peer-deps
|
||||
- npm run build:h5
|
||||
- ls -la dist/build/h5/
|
||||
|
||||
- name: clean
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host: 154.30.6.21
|
||||
username: root
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
script:
|
||||
- rm -rf /var/www/martial-admin-mini/*
|
||||
|
||||
- name: deploy
|
||||
image: appleboy/drone-scp
|
||||
settings:
|
||||
host: 154.30.6.21
|
||||
username: root
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
source: dist/build/h5/*
|
||||
target: /var/www/martial-admin-mini
|
||||
strip_components: 3
|
||||
|
||||
- name: restart
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host: 154.30.6.21
|
||||
username: root
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
script:
|
||||
- docker stop martial-admin-mini || true
|
||||
- docker rm martial-admin-mini || true
|
||||
- docker run -d --name martial-admin-mini -p 8082:80 -v /var/www/martial-admin-mini:/usr/share/nginx/html:ro nginx:alpine
|
||||
31
App.vue
31
App.vue
@@ -2,17 +2,44 @@
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
// 禁用 iOS Safari 双击缩放
|
||||
this.disableDoubleTapZoom()
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
},
|
||||
methods: {
|
||||
disableDoubleTapZoom() {
|
||||
// #ifdef H5
|
||||
let lastTouchEnd = 0
|
||||
document.documentElement.addEventListener('touchstart', function(event) {
|
||||
if (event.touches.length > 1) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}, { passive: false })
|
||||
|
||||
document.documentElement.addEventListener('touchend', function(event) {
|
||||
const now = Date.now()
|
||||
if (now - lastTouchEnd <= 300) {
|
||||
event.preventDefault()
|
||||
}
|
||||
lastTouchEnd = now
|
||||
}, { passive: false })
|
||||
|
||||
// 禁用手势缩放
|
||||
document.documentElement.addEventListener('gesturestart', function(event) {
|
||||
event.preventDefault()
|
||||
}, { passive: false })
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
|
||||
@import "common/common.css";
|
||||
/* 注意要写在第一行,同时给style标签加入lang=scss属性 */
|
||||
@import common/common.css;
|
||||
</style>
|
||||
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal 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
206
H5部署说明.md
Normal 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`
|
||||
|
||||
### 方案2:Nginx 配置
|
||||
|
||||
如果使用 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方案3:Apache 配置
|
||||
|
||||
如果使用 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
|
||||
311
README.md
311
README.md
@@ -1,296 +1,77 @@
|
||||
# 武术评分系统 - UniApp版本
|
||||
# 武术赛事管理系统 - 裁判端
|
||||
|
||||
这是一个基于UniApp开发的武术比赛评分系统,支持H5和小程序平台。
|
||||
基于 UniApp 开发的武术比赛评分小程序,支持 H5 和微信小程序。
|
||||
|
||||
## 项目简介
|
||||
## 在线访问
|
||||
|
||||
本项目是一个完整的武术比赛评分管理系统,包含评委打分、裁判长修改评分、多场地管理等功能。
|
||||
| 服务 | 地址 |
|
||||
|------|------|
|
||||
| 裁判端 H5 | https://martial-mini.aitisai.com |
|
||||
| 后端 API | https://martial-api.aitisai.com |
|
||||
|
||||
## 功能特点
|
||||
|
||||
- ✅ 支持H5和微信小程序双平台
|
||||
- ✅ 评委登录与评分功能
|
||||
- ✅ 裁判长评分修改功能
|
||||
- ✅ 多场地、多项目切换
|
||||
- ✅ 扣分项多选功能
|
||||
- ✅ 精确到0.001分的评分控制
|
||||
- ✅ 实时评分统计
|
||||
- 裁判员登录与评分
|
||||
- 主裁判评分审核
|
||||
- 总裁最终确认
|
||||
- 多场地切换
|
||||
- 扣分项管理
|
||||
- 精确到 0.001 分的评分控制
|
||||
|
||||
## 页面结构
|
||||
## 三级裁判系统
|
||||
|
||||
### 1. 登录页面 (`pages/login/login.vue`)
|
||||
- 输入比赛编码
|
||||
- 输入评委邀请码
|
||||
- 验证登录
|
||||
|
||||
### 2. 评分列表页 (`pages/score-list/score-list.vue`)
|
||||
- 显示比赛信息
|
||||
- 查看选手列表
|
||||
- 评委评分入口
|
||||
- 显示我的评分和总分
|
||||
|
||||
### 3. 修改评分页 (`pages/modify-score/modify-score.vue`)
|
||||
- 裁判长专用功能
|
||||
- 查看所有评委评分
|
||||
- 修改总分(±0.001分)
|
||||
- 添加修改备注
|
||||
|
||||
### 4. 多场地评分列表页 (`pages/score-list-multi/score-list-multi.vue`)
|
||||
- 场地切换功能
|
||||
- 项目切换功能
|
||||
- 裁判长可查看所有场地
|
||||
- 普通评委仅看自己的场地
|
||||
|
||||
### 5. 评分详情页 (`pages/score-detail/score-detail.vue`)
|
||||
- 评委打分界面
|
||||
- 分数精确调整(±0.001分)
|
||||
- 扣分项多选
|
||||
- 添加评分备注
|
||||
```
|
||||
裁判员 → 主裁判 → 总裁
|
||||
↓ ↓ ↓
|
||||
打分 审核修改 最终确认
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: UniApp
|
||||
- **前端**: Vue.js 2.x
|
||||
- **样式**: CSS3 (支持rpx单位)
|
||||
- **平台支持**: H5、微信小程序
|
||||
- **平台**: H5、微信小程序
|
||||
- **样式**: SCSS
|
||||
|
||||
## 目录结构
|
||||
## 快速开始
|
||||
|
||||
```
|
||||
martial-admin-mini/
|
||||
├── common/ # 公共资源
|
||||
│ └── common.css # 全局样式
|
||||
├── pages/ # 页面目录
|
||||
│ ├── login/ # 登录页面
|
||||
│ ├── score-list/ # 评分列表页
|
||||
│ ├── modify-score/ # 修改评分页
|
||||
│ ├── score-list-multi/ # 多场地列表页
|
||||
│ └── score-detail/ # 评分详情页
|
||||
├── image/ # 设计图片
|
||||
├── App.vue # 应用配置
|
||||
├── manifest.json # 应用配置清单
|
||||
├── pages.json # 页面配置
|
||||
└── package.json # 项目依赖
|
||||
```
|
||||
|
||||
## 快速开始(推荐方式)
|
||||
|
||||
### 使用HBuilderX运行(最简单)
|
||||
|
||||
1. **下载并安装HBuilderX**
|
||||
- 官网下载:https://www.dcloud.io/hbuilderx.html
|
||||
- 建议下载标准版或App开发版
|
||||
|
||||
2. **打开项目**
|
||||
- 打开HBuilderX
|
||||
- 点击菜单:文件 -> 打开目录
|
||||
- 选择本项目的根目录:`martial-admin-mini`
|
||||
|
||||
3. **运行到H5**
|
||||
- 在项目管理器中,右键点击项目根目录
|
||||
- 选择:运行 -> 运行到浏览器 -> Chrome(或其他浏览器)
|
||||
- 浏览器会自动打开并显示项目
|
||||
|
||||
4. **运行到微信小程序**
|
||||
- 首先安装并打开微信开发者工具
|
||||
- 在HBuilderX中,右键点击项目根目录
|
||||
- 选择:运行 -> 运行到小程序模拟器 -> 微信开发者工具
|
||||
- 首次运行会提示配置小程序工具路径,按提示配置即可
|
||||
|
||||
### 常见问题解决
|
||||
|
||||
#### 问题1:HBuilderX提示"未安装依赖"或"缺少插件"
|
||||
**解决方案:**
|
||||
- 点击HBuilderX顶部菜单:工具 -> 插件安装
|
||||
- 安装以下插件:
|
||||
- uni-app编译器(必需)
|
||||
- App真机运行(如需真机调试)
|
||||
- 微信小程序支持(如需开发小程序)
|
||||
|
||||
#### 问题2:运行时报错"Cannot find module"
|
||||
**解决方案:**
|
||||
```bash
|
||||
# 在项目根目录执行
|
||||
# 安装依赖
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 问题3:小程序无法运行
|
||||
**解决方案:**
|
||||
1. 确保已安装微信开发者工具
|
||||
2. 在HBuilderX中配置微信开发者工具路径:
|
||||
- 工具 -> 设置 -> 运行配置 -> 小程序运行配置
|
||||
- 配置微信开发者工具的安装路径
|
||||
3. 在manifest.json中配置小程序appid(或使用测试号)
|
||||
|
||||
#### 问题4:H5运行时样式异常
|
||||
**解决方案:**
|
||||
- 清除浏览器缓存后重新运行
|
||||
- 或使用Ctrl+F5强制刷新
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 环境要求
|
||||
|
||||
- HBuilderX 3.0+ (推荐)
|
||||
- 或 Node.js 12+ + Vue CLI
|
||||
- 微信开发者工具 (小程序开发需要)
|
||||
|
||||
### 方式一:使用HBuilderX(推荐)
|
||||
|
||||
1. 使用HBuilderX打开项目根目录
|
||||
2. 点击运行 -> 运行到浏览器 (H5开发)
|
||||
3. 或点击运行 -> 运行到小程序模拟器 -> 微信开发者工具
|
||||
|
||||
### 方式二:使用命令行
|
||||
|
||||
#### 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 运行项目
|
||||
|
||||
**H5开发**
|
||||
```bash
|
||||
# H5 开发
|
||||
npm run dev:h5
|
||||
```
|
||||
|
||||
**微信小程序开发**
|
||||
```bash
|
||||
# 微信小程序开发
|
||||
npm run dev:mp-weixin
|
||||
```
|
||||
|
||||
**构建H5**
|
||||
```bash
|
||||
# 构建 H5
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
**构建微信小程序**
|
||||
```bash
|
||||
npm run build:mp-weixin
|
||||
```
|
||||
|
||||
## 设计还原说明
|
||||
|
||||
本项目严格按照提供的5张设计图进行一比一还原:
|
||||
|
||||
1. **颜色方案**
|
||||
- 主色调:绿色 (#1B7C5E - #2A9D7E 渐变)
|
||||
- 强调色:红色 (#FF4D6A) - 用于提示信息
|
||||
- 背景色:浅灰 (#F5F5F5)
|
||||
|
||||
2. **字体大小**
|
||||
- 导航标题:36rpx
|
||||
- 页面标题:40rpx
|
||||
- 正文内容:26-32rpx
|
||||
- 提示文字:22-24rpx
|
||||
|
||||
3. **间距与圆角**
|
||||
- 卡片圆角:16rpx
|
||||
- 按钮圆角:8-16rpx
|
||||
- 标准间距:30rpx
|
||||
- 内边距:20-40rpx
|
||||
|
||||
4. **交互效果**
|
||||
- 按钮点击反馈
|
||||
- 分数增减控制
|
||||
- 多选框交互
|
||||
- 页面跳转动画
|
||||
|
||||
## 功能说明
|
||||
|
||||
### 评分规则
|
||||
|
||||
- 分数范围:5.0 - 10.0 分
|
||||
- 精度:0.001 分
|
||||
- 评委评分保留3位小数
|
||||
- 裁判长可修改总分 ±0.005 分
|
||||
|
||||
### 权限区分
|
||||
|
||||
- **普通评委**:仅能查看和评分自己负责的场地和项目
|
||||
- **裁判长**:可查看所有场地和项目,可修改评分
|
||||
|
||||
### 扣分项
|
||||
|
||||
- 支持多选
|
||||
- 每个项目可配置不同的扣分项
|
||||
- 扣分项选择后自动计入总分
|
||||
|
||||
## 🚀 API对接状态
|
||||
|
||||
### ✅ 前端已完全准备就绪(100%)
|
||||
|
||||
本项目已完成API对接准备工作,可以立即开始后端对接:
|
||||
|
||||
- ✅ **dataAdapter架构** - 支持Mock/API双模式无缝切换
|
||||
- ✅ **API接口定义** - 9个接口全部定义完成
|
||||
- ✅ **网络请求封装** - 统一的错误处理和Token管理
|
||||
- ✅ **Mock数据完整** - 可独立演示所有功能
|
||||
- ✅ **文档体系完善** - 21个文档,约25,000+行
|
||||
|
||||
### 📋 快速开始API对接
|
||||
|
||||
#### 1. 配置后端地址(30秒)
|
||||
|
||||
编辑 `config/env.config.js`:
|
||||
```javascript
|
||||
apiBaseURL: 'http://localhost:8080' // 修改为实际后端地址
|
||||
```
|
||||
|
||||
#### 2. 切换数据模式
|
||||
|
||||
```javascript
|
||||
// Mock模式(后端未就绪时)
|
||||
dataMode: 'mock'
|
||||
|
||||
// API模式(后端就绪后)
|
||||
dataMode: 'api'
|
||||
```
|
||||
|
||||
#### 3. 查看文档
|
||||
|
||||
- **快速上手**: [API对接快速启动指南.md](doc/API对接快速启动指南.md) - 5分钟快速上手
|
||||
- **后端开发**: [后端接口开发清单.md](doc/后端接口开发清单.md) - 详细的开发规范
|
||||
- **前端联调**: [前端API对接指南.md](doc/前端API对接指南.md) - 前端联调指南
|
||||
- **快速参考**: [快速参考.md](快速参考.md) - 一页纸快速参考
|
||||
|
||||
### ⚠️ 后端待开发接口(5个)
|
||||
|
||||
| 接口 | 路径 | 优先级 | 工作量 |
|
||||
|------|------|--------|--------|
|
||||
| 登录验证 | `POST /api/mini/login` | 🔴 高 | 2天 |
|
||||
| 普通评委选手列表 | `GET /api/mini/athletes` | 🔴 高 | 1天 |
|
||||
| 裁判长选手列表 | `GET /api/mini/athletes/admin` | 🟡 中 | 1天 |
|
||||
| 评分详情 | `GET /api/mini/score/detail/{id}` | 🟡 中 | 1天 |
|
||||
| 修改评分 | `PUT /api/mini/score/modify` | 🟡 中 | 1天 |
|
||||
|
||||
**预计总工作量**: 6人天(约1周)
|
||||
|
||||
详细规范请查看:[后端接口开发清单.md](doc/后端接口开发清单.md)
|
||||
|
||||
### 📊 项目状态
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
前端开发: ████████████████████ 100% ✅
|
||||
后端开发: ████████░░░░░░░░░░░░ 44% ⚠️
|
||||
文档完成: ████████████████████ 100% ✅
|
||||
martial-admin-mini/
|
||||
├── pages/
|
||||
│ ├── login/ # 登录页
|
||||
│ ├── score-list/ # 评分列表
|
||||
│ ├── score-list-multi/ # 多场地评分
|
||||
│ ├── score-detail/ # 评分详情
|
||||
│ └── modify-score/ # 修改评分(裁判长)
|
||||
├── components/ # 公共组件
|
||||
├── static/ # 静态资源
|
||||
├── pages.json # 页面配置
|
||||
└── manifest.json # 应用配置
|
||||
```
|
||||
|
||||
查看实时状态:[项目状态看板.md](项目状态看板.md)
|
||||
## 相关仓库
|
||||
|
||||
## 注意事项
|
||||
| 仓库 | 说明 |
|
||||
|------|------|
|
||||
| [martial-master](https://git.waypeak.work/martial/martial-master) | 后端 API |
|
||||
| [martial-web](https://git.waypeak.work/martial/martial-web) | 管理后台 |
|
||||
| [martial-mini](https://git.waypeak.work/martial/martial-mini) | 用户端小程序 |
|
||||
|
||||
1. ✅ 本项目已完成API对接准备,支持Mock/API双模式
|
||||
2. ✅ Mock模式下所有功能可独立演示
|
||||
3. ✅ API模式下需要后端实现5个专用接口
|
||||
4. ✅ 页面跳转已配置,可直接运行演示
|
||||
5. ✅ 适配了主流手机屏幕尺寸
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题或建议,请联系项目负责人。
|
||||
**最后更新**: 2024-12-29
|
||||
|
||||
@@ -6,41 +6,57 @@
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
/**
|
||||
* 获取我的选手列表(普通评委)
|
||||
* 获取选手列表(根据裁判类型返回不同数据)
|
||||
* @param {Object} params
|
||||
* @param {String} params.judgeId - 评委ID
|
||||
* @param {String} params.venueId - 场地ID
|
||||
* @param {String} params.projectId - 项目ID
|
||||
* @param {Number} params.refereeType - 裁判类型(1-主裁判, 2-裁判员)
|
||||
* @param {String} params.venueId - 场地ID(可选)
|
||||
* @param {String} params.projectId - 项目ID(可选)
|
||||
* @returns {Promise}
|
||||
*
|
||||
* 注意:此接口需要后端实现
|
||||
* 建议路径: GET /api/mini/athletes
|
||||
* 裁判员:返回待评分的选手列表
|
||||
* 主裁判:返回已有评分的选手列表
|
||||
*
|
||||
* 后端路径: GET /api/mini/score/athletes
|
||||
*/
|
||||
export function getMyAthletes(params) {
|
||||
return request({
|
||||
url: '/api/mini/athletes',
|
||||
url: '/mini/score/athletes',
|
||||
method: 'GET',
|
||||
params: params, // GET 请求使用 params
|
||||
params: {
|
||||
...params,
|
||||
size: 200 // 确保获取所有选手
|
||||
},
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选手列表(裁判长)
|
||||
* 获取选手列表(主裁判)
|
||||
* @param {Object} params
|
||||
* @param {String} params.competitionId - 比赛ID
|
||||
* @param {String} params.venueId - 场地ID
|
||||
* @param {String} params.projectId - 项目ID
|
||||
* @returns {Promise}
|
||||
*
|
||||
* 注意:此接口需要后端实现
|
||||
* 建议路径: GET /api/mini/athletes/admin
|
||||
* 实际调用 /mini/score/athletes 接口,传递 refereeType=1
|
||||
*/
|
||||
export function getAthletesForAdmin(params) {
|
||||
// 从 globalData 获取 judgeId
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
const judgeId = globalData.judgeId
|
||||
|
||||
return request({
|
||||
url: '/api/mini/athletes/admin',
|
||||
url: '/mini/score/athletes',
|
||||
method: 'GET',
|
||||
params: params, // GET 请求使用 params
|
||||
params: {
|
||||
judgeId: judgeId,
|
||||
refereeType: 1, // 主裁判
|
||||
venueId: params.venueId,
|
||||
projectId: params.projectId,
|
||||
size: 200 // 确保获取所有选手
|
||||
},
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
@@ -87,56 +103,3 @@ export default {
|
||||
getVenues,
|
||||
getProjects
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端接口规范(待实现):
|
||||
*
|
||||
* GET /api/mini/athletes
|
||||
*
|
||||
* 请求参数:
|
||||
* {
|
||||
* "judgeId": "456",
|
||||
* "venueId": "1",
|
||||
* "projectId": "5"
|
||||
* }
|
||||
*
|
||||
* 响应:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "success": true,
|
||||
* "msg": "操作成功",
|
||||
* "data": [
|
||||
* {
|
||||
* "athleteId": "1",
|
||||
* "name": "张三",
|
||||
* "idCard": "123456789000000000",
|
||||
* "team": "少林寺武术大学院",
|
||||
* "number": "123-4567898275",
|
||||
* "myScore": 8.906,
|
||||
* "totalScore": 8.907,
|
||||
* "scored": true,
|
||||
* "scoreTime": "2025-06-25 09:15:00"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* SQL示例:
|
||||
* SELECT
|
||||
* a.id AS athleteId,
|
||||
* a.player_name AS name,
|
||||
* a.id_card AS idCard,
|
||||
* a.team_name AS team,
|
||||
* a.player_no AS number,
|
||||
* a.total_score AS totalScore,
|
||||
* s.score AS myScore,
|
||||
* CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END AS scored,
|
||||
* s.score_time AS scoreTime
|
||||
* FROM martial_athlete a
|
||||
* LEFT JOIN martial_score s
|
||||
* ON a.id = s.athlete_id
|
||||
* AND s.judge_id = #{judgeId}
|
||||
* WHERE a.venue_id = #{venueId}
|
||||
* AND a.project_id = #{projectId}
|
||||
* AND a.is_deleted = 0
|
||||
* ORDER BY a.order_num ASC
|
||||
*/
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
* "msg": "登录成功",
|
||||
* "data": {
|
||||
* "token": "xxx",
|
||||
* "userRole": "pub",
|
||||
* "refereeType": 2, // 1-主裁判, 2-裁判员
|
||||
* "matchId": "123",
|
||||
* "matchName": "2025年全国武术散打锦标赛...",
|
||||
* "matchTime": "2025年6月25日 9:00",
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
getMyAthletes: athleteApi.getMyAthletes,
|
||||
|
||||
/**
|
||||
* 获取选手列表(裁判长)
|
||||
* 获取选手列表(主裁判)
|
||||
* @param {Object} params - { competitionId, venueId, projectId }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
@@ -82,14 +82,14 @@ export default {
|
||||
submitScore: scoreApi.submitScore,
|
||||
|
||||
/**
|
||||
* 获取评分详情(裁判长查看)
|
||||
* 获取评分详情(主裁判查看)
|
||||
* @param {Object} params - { athleteId }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getScoreDetail: scoreApi.getScoreDetail,
|
||||
|
||||
/**
|
||||
* 修改评分(裁判长)
|
||||
* 修改评分(主裁判)
|
||||
* @param {Object} data - { athleteId, modifierId, modifiedScore, note }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
@@ -127,7 +127,7 @@ export default {
|
||||
* 1. 需要实现的新接口(小程序专用):
|
||||
* - POST /api/mini/login # 登录验证
|
||||
* - GET /api/mini/athletes # 普通评委选手列表
|
||||
* - GET /api/mini/athletes/admin # 裁判长选手列表
|
||||
* - GET /api/mini/athletes/admin # 主裁判选手列表
|
||||
* - GET /api/mini/score/detail/{athleteId} # 评分详情
|
||||
* - PUT /api/mini/score/modify # 修改评分
|
||||
*
|
||||
|
||||
36
api/score.js
36
api/score.js
@@ -13,7 +13,7 @@ import request from '@/utils/request.js'
|
||||
*/
|
||||
export function getDeductions(params) {
|
||||
return request({
|
||||
url: '/martial/deductionItem/list',
|
||||
url: '/blade-martial/deductionItem/list',
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
@@ -35,7 +35,7 @@ export function getDeductions(params) {
|
||||
*/
|
||||
export function submitScore(data) {
|
||||
return request({
|
||||
url: '/martial/score/submit',
|
||||
url: '/mini/score/submit',
|
||||
method: 'POST',
|
||||
data,
|
||||
showLoading: true,
|
||||
@@ -44,7 +44,7 @@ export function submitScore(data) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评分详情(裁判长查看)
|
||||
* 获取评分详情(主裁判查看)
|
||||
* @param {Object} params
|
||||
* @param {String} params.athleteId - 选手ID
|
||||
* @returns {Promise}
|
||||
@@ -54,14 +54,14 @@ export function submitScore(data) {
|
||||
*/
|
||||
export function getScoreDetail(params) {
|
||||
return request({
|
||||
url: `/api/mini/score/detail/${params.athleteId}`,
|
||||
url: `/mini/score/detail/${params.athleteId}`,
|
||||
method: 'GET',
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改评分(裁判长)
|
||||
* 修改评分(主裁判)
|
||||
* @param {Object} data
|
||||
* @param {String} data.athleteId - 选手ID
|
||||
* @param {String} data.modifierId - 修改人ID
|
||||
@@ -82,11 +82,33 @@ export function modifyScore(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选手列表
|
||||
* @param {Object} params
|
||||
* @param {String} params.judgeId - 裁判ID
|
||||
* @param {Number} params.refereeType - 裁判类型(1-主裁判, 2-裁判员)
|
||||
* @param {String} params.projectId - 项目ID(可选)
|
||||
* @param {String} params.venueId - 场地ID(可选)
|
||||
* @returns {Promise}
|
||||
*
|
||||
* 注意:此接口需要后端实现
|
||||
* 建议路径: GET /api/mini/score/athletes
|
||||
*/
|
||||
export function getAthletes(params) {
|
||||
return request({
|
||||
url: '/api/mini/score/athletes',
|
||||
method: 'GET',
|
||||
params,
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
getDeductions,
|
||||
submitScore,
|
||||
getScoreDetail,
|
||||
modifyScore
|
||||
modifyScore,
|
||||
getAthletes
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +180,7 @@ export default {
|
||||
* }
|
||||
*
|
||||
* 实现逻辑:
|
||||
* 1. 验证权限(只有裁判长可以修改)
|
||||
* 1. 验证权限(只有主裁判可以修改)
|
||||
* 2. 保存 originalScore(如果是第一次修改)
|
||||
* 3. 更新 totalScore
|
||||
* 4. 记录 modifyReason 和 modifyTime
|
||||
|
||||
@@ -28,3 +28,14 @@ button::after {
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 防止 iOS Safari 双击缩放 */
|
||||
button, .control-btn, [class*="btn"] {
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 全局禁用双击缩放 */
|
||||
html {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ const ENV_CONFIG = {
|
||||
dataMode: 'api',
|
||||
|
||||
// API基础路径(dataMode为'api'时使用)
|
||||
apiBaseURL: 'http://localhost:8123',
|
||||
// uni.request 不支持 devServer proxy,必须用完整地址
|
||||
apiBaseURL: 'http://142.91.105.230:8123',
|
||||
|
||||
// 调试模式
|
||||
debug: true,
|
||||
|
||||
// 请求超时时间(毫秒)
|
||||
timeout: 30000,
|
||||
|
||||
166
doc/Linux命令行编译样式问题修复记录.md
Normal file
166
doc/Linux命令行编译样式问题修复记录.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Linux/Mac 命令行编译样式异常问题修复记录
|
||||
|
||||
## 问题描述
|
||||
|
||||
在 Windows 上使用 HBuilderX 运行项目时样式正常,但在 Linux/Mac 上使用 `npm run dev:h5` 命令行编译时,页面样式完全异常,组件的 scoped 样式没有生效。
|
||||
|
||||
## 问题现象
|
||||
|
||||
- 页面布局错乱
|
||||
- rpx 单位没有被转换为 px
|
||||
- 组件的 scoped 样式没有被正确打包
|
||||
|
||||
## 根本原因
|
||||
|
||||
### 1. postcss.config.js 配置覆盖问题
|
||||
|
||||
项目中自定义的 `postcss.config.js` 文件覆盖了 uni-app 的默认 postcss 配置:
|
||||
|
||||
```javascript
|
||||
// 原配置 - 问题配置
|
||||
const autoprefixer = require('autoprefixer')
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
autoprefixer()
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
这个配置**没有包含 uni-app 的 postcss 插件**,导致:
|
||||
- rpx 单位没有被转换为 `%?数值?%` 占位符格式
|
||||
- uni-app 运行时无法在浏览器中将占位符转换为实际的 px 值
|
||||
|
||||
### 2. HBuilderX vs 命令行编译的区别
|
||||
|
||||
| 特性 | HBuilderX | 命令行 (npm run dev:h5) |
|
||||
|------|-----------|------------------------|
|
||||
| 编译器 | 内置优化版编译器 | 依赖 node_modules |
|
||||
| rpx 处理 | 自动转换 | 需要 postcss 插件 |
|
||||
| 样式处理 | 完善的内置处理 | 依赖配置文件 |
|
||||
| 版本兼容 | 内部统一管理 | 可能存在版本冲突 |
|
||||
|
||||
### 3. PostCSS 版本兼容问题
|
||||
|
||||
- 项目使用 `postcss-loader@3.0.0`(旧版)
|
||||
- 但 `postcss` 被升级到了 8.x 版本
|
||||
- postcss-loader 3.x 与 postcss 8 不兼容
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 修改 postcss.config.js
|
||||
|
||||
添加 uni-app 的 postcss 插件:
|
||||
|
||||
```javascript
|
||||
const autoprefixer = require('autoprefixer')
|
||||
|
||||
// 引入 uni-app 的 postcss 插件来处理 rpx 转换
|
||||
const uniappPlugin = require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
uniappPlugin,
|
||||
autoprefixer
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 降级 postcss 版本
|
||||
|
||||
```bash
|
||||
npm install postcss@7 --save --legacy-peer-deps
|
||||
```
|
||||
|
||||
### 3. 降级 sass 版本(可选,提高兼容性)
|
||||
|
||||
```bash
|
||||
npm install sass@1.32.13 --save
|
||||
```
|
||||
|
||||
## 修复后的依赖版本
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"postcss": "^7.0.39",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"sass": "^1.32.13"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 验证方法
|
||||
|
||||
1. 构建项目:
|
||||
```bash
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
2. 检查构建后的 JS 文件中 rpx 是否被转换:
|
||||
```bash
|
||||
# 应该看到 %?90?% 这样的占位符,而不是 90rpx
|
||||
grep -oE "height:%\?[0-9]+\?%" dist/build/h5/static/js/pages-login-login.*.js
|
||||
```
|
||||
|
||||
3. 启动开发服务器验证样式:
|
||||
```bash
|
||||
npm run dev:h5
|
||||
# 访问 http://localhost:8080 查看样式
|
||||
```
|
||||
|
||||
## 技术细节
|
||||
|
||||
### uni-app 的 rpx 转换流程
|
||||
|
||||
1. **编译时**:postcss 插件将 `90rpx` 转换为 `%?90?%` 占位符
|
||||
2. **运行时**:uni-app 的 Vue 运行时根据屏幕宽度将占位符转换为实际 px 值
|
||||
3. **计算公式**:`px = rpx * (屏幕宽度 / 750)`
|
||||
|
||||
### 相关文件
|
||||
|
||||
- `/postcss.config.js` - PostCSS 配置
|
||||
- `/node_modules/@dcloudio/vue-cli-plugin-uni/packages/postcss/index.js` - uni-app postcss 插件
|
||||
- `/node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue/dist/vue.runtime.esm.js` - 运行时 rpx 转换
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **不要随意升级 postcss 版本**:postcss-loader 3.x 只兼容 postcss 7.x
|
||||
2. **保留 uni-app postcss 插件**:这是 rpx 转换的关键
|
||||
3. **Node.js 版本建议**:使用 Node 16.x 以获得最佳兼容性
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js: 16.x (推荐 16.20.2)
|
||||
- npm: 8.x
|
||||
- postcss: 7.x
|
||||
- postcss-loader: 3.x
|
||||
|
||||
## 相关命令
|
||||
|
||||
```bash
|
||||
# 安装 nvm(如果没有)
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
|
||||
|
||||
# 切换到 Node 16
|
||||
nvm install 16
|
||||
nvm use 16
|
||||
|
||||
# 重新安装依赖
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
|
||||
# 开发模式
|
||||
npm run dev:h5
|
||||
|
||||
# 生产构建
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
## 修复日期
|
||||
|
||||
2024-12-17
|
||||
|
||||
## 修复分支
|
||||
|
||||
`devops`
|
||||
225
index.html
225
index.html
@@ -3,15 +3,236 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<!-- 关键:使用最严格的 viewport 设置 -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<title>武术评分系统</title>
|
||||
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
|
||||
<style>
|
||||
* {
|
||||
/* 允许垂直滚动,但禁用其他触摸动作 */
|
||||
touch-action: pan-y !important;
|
||||
-webkit-touch-callout: none !important;
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
-webkit-user-select: none !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
/* 针对按钮元素完全禁用所有触摸动作 */
|
||||
button,
|
||||
.control-btn,
|
||||
[class*="btn"],
|
||||
[class*="control"],
|
||||
.decrease,
|
||||
.increase {
|
||||
touch-action: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
user-select: none !important;
|
||||
-webkit-touch-callout: none !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* 允许输入框正常交互 */
|
||||
input, textarea {
|
||||
touch-action: manipulation !important;
|
||||
-webkit-user-select: text !important;
|
||||
user-select: text !important;
|
||||
}
|
||||
|
||||
/* 防止页面整体缩放 */
|
||||
html, body {
|
||||
touch-action: pan-y !important;
|
||||
-ms-touch-action: pan-y !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// UniApp H5 专用:iOS Safari 双击缩放终极解决方案
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var lastTouchEnd = 0;
|
||||
var touchStartTime = 0;
|
||||
var touchCount = 0;
|
||||
var resetTimer = null;
|
||||
var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
|
||||
console.log('iOS 检测:', isIOS);
|
||||
console.log('User Agent:', navigator.userAgent);
|
||||
|
||||
// 方案1: 全局拦截 touchstart - 最高优先级
|
||||
document.addEventListener('touchstart', function(event) {
|
||||
var now = Date.now();
|
||||
touchStartTime = now;
|
||||
|
||||
// 清除重置计时器
|
||||
if (resetTimer) {
|
||||
clearTimeout(resetTimer);
|
||||
}
|
||||
|
||||
// 检查是否是快速连续触摸
|
||||
var timeSinceLastTouch = now - lastTouchEnd;
|
||||
|
||||
if (timeSinceLastTouch < 350) {
|
||||
touchCount++;
|
||||
|
||||
// 如果是第二次或更多次快速触摸,立即阻止
|
||||
if (touchCount >= 1) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
console.log('阻止快速连续触摸', touchCount, timeSinceLastTouch);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
touchCount = 0;
|
||||
}
|
||||
|
||||
// 600ms 后重置计数器
|
||||
resetTimer = setTimeout(function() {
|
||||
touchCount = 0;
|
||||
}, 600);
|
||||
|
||||
}, { passive: false, capture: true });
|
||||
|
||||
// 方案2: 全局拦截 touchend
|
||||
document.addEventListener('touchend', function(event) {
|
||||
var now = Date.now();
|
||||
var touchDuration = now - touchStartTime;
|
||||
var timeSinceLastTouch = now - lastTouchEnd;
|
||||
|
||||
// 如果触摸时间很短(<150ms)且距离上次触摸很近(<350ms),很可能是双击
|
||||
if (touchDuration < 150 && timeSinceLastTouch < 350 && timeSinceLastTouch > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
console.log('阻止疑似双击', touchDuration, timeSinceLastTouch);
|
||||
return false;
|
||||
}
|
||||
|
||||
lastTouchEnd = now;
|
||||
}, { passive: false, capture: true });
|
||||
|
||||
// 方案3: 完全禁用 dblclick 事件
|
||||
document.addEventListener('dblclick', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
console.log('阻止 dblclick 事件');
|
||||
return false;
|
||||
}, { passive: false, capture: true });
|
||||
|
||||
// 方案4: 禁用手势缩放
|
||||
document.addEventListener('gesturestart', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
console.log('阻止 gesturestart');
|
||||
}, { passive: false, capture: true });
|
||||
|
||||
document.addEventListener('gesturechange', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, { passive: false, capture: true });
|
||||
|
||||
document.addEventListener('gestureend', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, { passive: false, capture: true });
|
||||
|
||||
// 方案5: 监听 click 事件,过滤快速连续点击
|
||||
var lastClickTime = 0;
|
||||
document.addEventListener('click', function(event) {
|
||||
var now = Date.now();
|
||||
var timeSinceLastClick = now - lastClickTime;
|
||||
|
||||
// 如果距离上次点击小于350ms,阻止
|
||||
if (timeSinceLastClick < 350 && timeSinceLastClick > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
console.log('阻止快速连续点击', timeSinceLastClick);
|
||||
return false;
|
||||
}
|
||||
|
||||
lastClickTime = now;
|
||||
}, { passive: false, capture: true });
|
||||
|
||||
// 方案6: 针对按钮元素的特殊处理
|
||||
function addButtonProtection() {
|
||||
var selectors = [
|
||||
'.control-btn',
|
||||
'.control-btn.decrease',
|
||||
'.control-btn.increase',
|
||||
'button',
|
||||
'[class*="btn"]'
|
||||
];
|
||||
|
||||
selectors.forEach(function(selector) {
|
||||
var elements = document.querySelectorAll(selector);
|
||||
elements.forEach(function(element) {
|
||||
// 移除所有现有的事件监听器(通过克隆节点)
|
||||
var newElement = element.cloneNode(true);
|
||||
element.parentNode.replaceChild(newElement, element);
|
||||
|
||||
// 添加新的保护性事件监听器
|
||||
['touchstart', 'touchend', 'touchmove', 'click', 'dblclick'].forEach(function(eventType) {
|
||||
newElement.addEventListener(eventType, function(e) {
|
||||
if (eventType === 'dblclick') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
return false;
|
||||
}
|
||||
}, { passive: false, capture: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// DOM 加载完成后添加按钮保护
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(addButtonProtection, 100);
|
||||
// 使用 MutationObserver 监听 DOM 变化
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
addButtonProtection();
|
||||
});
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setTimeout(addButtonProtection, 100);
|
||||
}
|
||||
|
||||
// 方案7: 使用 CSS 强制禁用缩放
|
||||
var style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
* {
|
||||
touch-action: pan-y !important;
|
||||
}
|
||||
.control-btn,
|
||||
.control-btn *,
|
||||
button,
|
||||
button * {
|
||||
touch-action: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
user-select: none !important;
|
||||
-webkit-touch-callout: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
console.log('iOS Safari 双击缩放防护已启用 - UniApp H5 专用版本');
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>请开启JavaScript运行本应用</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"usingComponents" : true
|
||||
},
|
||||
"h5" : {
|
||||
"template": "index.html",
|
||||
"title" : "武术评分系统",
|
||||
"router" : {
|
||||
"mode" : "hash",
|
||||
|
||||
@@ -4,54 +4,83 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取我的选手列表(普通评委)
|
||||
* 获取选手列表(根据裁判类型返回不同数据)
|
||||
* @param {Object} params
|
||||
* @param {String} params.judgeId - 评委ID
|
||||
* @param {String} params.venueId - 场地ID
|
||||
* @param {String} params.projectId - 项目ID
|
||||
* @returns {Array} 选手列表(带评分状态)
|
||||
* @param {Number} params.refereeType - 裁判类型(1-主裁判, 2-裁判员)
|
||||
* @param {String} params.venueId - 场地ID(可选)
|
||||
* @param {String} params.projectId - 项目ID(可选)
|
||||
* @returns {Array} 选手列表
|
||||
*/
|
||||
export function getMyAthletes(params) {
|
||||
// 模拟3个选手数据
|
||||
const { refereeType } = params
|
||||
|
||||
// 主裁判:返回已有评分的选手
|
||||
if (refereeType === 1) {
|
||||
return [
|
||||
{
|
||||
athleteId: '1',
|
||||
athleteId: 1,
|
||||
name: '张三',
|
||||
idCard: '123456789000000000',
|
||||
team: '少林寺武术大学院',
|
||||
number: '123-4567898275',
|
||||
myScore: 8.906, // 我的评分
|
||||
totalScore: 8.907, // 总分
|
||||
scored: true, // 已评分
|
||||
scoreTime: '2025-06-25 09:15:00'
|
||||
team: '少林寺武术大学院',
|
||||
projectName: '女子组长拳',
|
||||
orderNum: 1,
|
||||
totalScore: 8.907,
|
||||
scoredJudgeCount: 6,
|
||||
competitionStatus: 2
|
||||
},
|
||||
{
|
||||
athleteId: '2',
|
||||
athleteId: 2,
|
||||
name: '李四',
|
||||
idCard: '123456789000000001',
|
||||
team: '武当山武术学院',
|
||||
number: '123-4567898276',
|
||||
myScore: 8.901,
|
||||
team: '武当山武术学院',
|
||||
projectName: '女子组长拳',
|
||||
orderNum: 2,
|
||||
totalScore: 8.902,
|
||||
scored: true,
|
||||
scoreTime: '2025-06-25 09:20:00'
|
||||
scoredJudgeCount: 6,
|
||||
competitionStatus: 2
|
||||
},
|
||||
{
|
||||
athleteId: '3',
|
||||
athleteId: 4,
|
||||
name: '赵六',
|
||||
number: '123-4567898278',
|
||||
team: '华山武术学院',
|
||||
projectName: '女子组长拳',
|
||||
orderNum: 4,
|
||||
totalScore: 8.899,
|
||||
scoredJudgeCount: 5,
|
||||
competitionStatus: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 裁判员:返回待评分的选手
|
||||
return [
|
||||
{
|
||||
athleteId: 3,
|
||||
name: '王五',
|
||||
idCard: '123456789000000002',
|
||||
team: '峨眉派武术学校',
|
||||
number: '123-4567898277',
|
||||
myScore: null, // 未评分
|
||||
totalScore: null,
|
||||
scored: false,
|
||||
scoreTime: null
|
||||
projectName: '女子组长拳',
|
||||
orderNum: 3,
|
||||
competitionStatus: 0
|
||||
},
|
||||
{
|
||||
athleteId: 5,
|
||||
name: '孙七',
|
||||
idCard: '123456789000000004',
|
||||
team: '崆峒派武术学校',
|
||||
number: '123-4567898279',
|
||||
projectName: '女子组长拳',
|
||||
orderNum: 5,
|
||||
competitionStatus: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选手列表(裁判长)
|
||||
* 获取选手列表(主裁判)
|
||||
* @param {Object} params
|
||||
* @param {String} params.competitionId - 比赛ID
|
||||
* @param {String} params.venueId - 场地ID
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
getMyAthletes: athleteMock.getMyAthletes,
|
||||
|
||||
/**
|
||||
* 获取选手列表(裁判长)
|
||||
* 获取选手列表(主裁判)
|
||||
* @param {Object} params - { competitionId, venueId, projectId }
|
||||
* @returns {Array} 选手列表(带评分统计)
|
||||
*/
|
||||
@@ -70,14 +70,14 @@ export default {
|
||||
submitScore: scoreMock.submitScore,
|
||||
|
||||
/**
|
||||
* 获取评分详情(裁判长查看)
|
||||
* 获取评分详情(主裁判查看)
|
||||
* @param {Object} params - { athleteId }
|
||||
* @returns {Object} 评分详情(选手信息+评委评分)
|
||||
*/
|
||||
getScoreDetail: scoreMock.getScoreDetail,
|
||||
|
||||
/**
|
||||
* 修改评分(裁判长)
|
||||
* 修改评分(主裁判)
|
||||
* @param {Object} params - { athleteId, modifierId, modifiedScore, note }
|
||||
* @returns {Object} 修改结果
|
||||
*/
|
||||
|
||||
@@ -23,13 +23,13 @@ export function login(params) {
|
||||
// 返回Mock登录数据
|
||||
return {
|
||||
token: 'mock_token_' + Date.now(),
|
||||
userRole: role, // 'pub' 或 'admin'
|
||||
matchId: '123',
|
||||
refereeType: role === 'pub' ? 2 : 1, // 1-主裁判, 2-裁判员
|
||||
matchId: matchCode || '200', // 使用传入的比赛编码,默认200
|
||||
matchName: '2025年全国武术散打锦标赛暨第十七届世界武术锦标赛选拔赛',
|
||||
matchTime: '2025年6月25日 9:00',
|
||||
judgeId: '456',
|
||||
judgeName: '欧阳丽娜',
|
||||
// 普通评委有固定场地,裁判长可以查看所有场地
|
||||
// 普通评委有固定场地,主裁判可以查看所有场地
|
||||
venueId: role === 'pub' ? '1' : null,
|
||||
venueName: role === 'pub' ? '第一场地' : null,
|
||||
// 分配的项目列表
|
||||
|
||||
@@ -7,20 +7,26 @@
|
||||
* 获取扣分项列表
|
||||
* @param {Object} params
|
||||
* @param {String} params.projectId - 项目ID
|
||||
* @returns {Array} 扣分项列表
|
||||
* @returns {Object} 扣分项列表(包装在records中,与后端API格式一致)
|
||||
*/
|
||||
export function getDeductions(params) {
|
||||
// 模拟8个扣分项
|
||||
return [
|
||||
{ id: '1', text: '扣分项描述', score: -0.1, checked: false },
|
||||
{ id: '2', text: '扣分项描述', score: -0.1, checked: false },
|
||||
{ id: '3', text: '扣分项描述', score: -0.1, checked: false },
|
||||
{ id: '4', text: '扣分项描述', score: -0.1, checked: false },
|
||||
{ id: '5', text: '扣分项描述', score: -0.1, checked: false },
|
||||
{ id: '6', text: '扣分项描述', score: -0.1, checked: false },
|
||||
{ id: '7', text: '扣分项描述', score: -0.1, checked: false },
|
||||
{ id: '8', text: '扣分项描述', score: -0.1, checked: false }
|
||||
]
|
||||
// 模拟8个扣分项(字段名与后端API保持一致)
|
||||
return {
|
||||
records: [
|
||||
{ id: '1', itemName: '动作不规范', deductionPoint: '0.1' },
|
||||
{ id: '2', itemName: '节奏不稳', deductionPoint: '0.1' },
|
||||
{ id: '3', itemName: '力度不足', deductionPoint: '0.1' },
|
||||
{ id: '4', itemName: '平衡失误', deductionPoint: '0.1' },
|
||||
{ id: '5', itemName: '器械掉落', deductionPoint: '0.2' },
|
||||
{ id: '6', itemName: '出界', deductionPoint: '0.1' },
|
||||
{ id: '7', itemName: '动作遗漏', deductionPoint: '0.2' },
|
||||
{ id: '8', itemName: '其他失误', deductionPoint: '0.1' }
|
||||
],
|
||||
total: 8,
|
||||
size: 100,
|
||||
current: 1,
|
||||
pages: 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +62,7 @@ export function submitScore(params) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评分详情(裁判长查看)
|
||||
* 获取评分详情(主裁判查看)
|
||||
* @param {Object} params
|
||||
* @param {String} params.athleteId - 选手ID
|
||||
* @returns {Object} 评分详情
|
||||
@@ -125,10 +131,10 @@ export function getScoreDetail(params) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改评分(裁判长)
|
||||
* 修改评分(主裁判)
|
||||
* @param {Object} params
|
||||
* @param {String} params.athleteId - 选手ID
|
||||
* @param {String} params.modifierId - 修改人ID(裁判长)
|
||||
* @param {String} params.modifierId - 修改人ID(主裁判)
|
||||
* @param {Number} params.modifiedScore - 修改后的分数
|
||||
* @param {String} params.note - 修改原因
|
||||
* @returns {Object} 修改结果
|
||||
|
||||
18803
package-lock.json
generated
18803
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -3,5 +3,52 @@
|
||||
"name": "martial-admin-mini",
|
||||
"version": "1.0.0",
|
||||
"description": "武术比赛评分系统",
|
||||
"main": "main.js"
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service serve",
|
||||
"build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/uni-cli-i18n": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/uni-h5": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/uni-i18n": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/uni-migration": "^2.0.2-3081220230817001",
|
||||
"autoprefixer": "^9.8.8",
|
||||
"cache-loader": "^4.1.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"css-loader": "^3.6.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"postcss": "^7.0.39",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"sass": "^1.32.13",
|
||||
"sass-loader": "^10.5.2",
|
||||
"thread-loader": "^2.1.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-loader": "^15.11.1",
|
||||
"webpack": "^4.47.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/uni-cli-shared": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/uni-template-compiler": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/vue-cli-plugin-uni": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/webpack-uni-mp-loader": "^2.0.2-3081220230817001",
|
||||
"@dcloudio/webpack-uni-pages-loader": "^2.0.2-3081220230817001",
|
||||
"@vue/cli-plugin-babel": "~4.5.19",
|
||||
"@vue/cli-service": "~4.5.19",
|
||||
"babel-plugin-import": "^1.13.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"mini-types": "*",
|
||||
"postcss-comment": "^2.0.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
},
|
||||
"browserslist": [
|
||||
"Android >= 4.4",
|
||||
"ios >= 9"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ export default {
|
||||
const {
|
||||
token,
|
||||
userRole,
|
||||
refereeType,
|
||||
matchId,
|
||||
matchName,
|
||||
matchTime,
|
||||
@@ -121,15 +122,16 @@ export default {
|
||||
// 保存用户信息到全局数据
|
||||
getApp().globalData = {
|
||||
userRole, // 'pub' 或 'admin'
|
||||
refereeType, // 1-主裁判, 2-裁判员
|
||||
matchCode: this.matchCode,
|
||||
matchId,
|
||||
matchName,
|
||||
matchTime,
|
||||
judgeId,
|
||||
judgeName,
|
||||
venueId, // 普通评委有场地,裁判长为null
|
||||
venueId, // 普通评委有场地,主裁判为null
|
||||
venueName,
|
||||
projects, // 分配的项目列表
|
||||
projects, // 分配的项目列表(从登录接口返回)
|
||||
currentProjectIndex: 0 // 当前选中的项目索引
|
||||
}
|
||||
|
||||
@@ -153,12 +155,12 @@ export default {
|
||||
// 根据角色跳转到不同页面
|
||||
setTimeout(() => {
|
||||
if (userRole === 'admin') {
|
||||
// 裁判长跳转到多场地列表页(可以修改评分)
|
||||
// 主裁判跳转到多场地列表页(可以修改评分)
|
||||
uni.navigateTo({
|
||||
url: '/pages/score-list-multi/score-list-multi'
|
||||
})
|
||||
} else {
|
||||
// 普通裁判跳转到评分列表页(可以评分)
|
||||
// 裁判员跳转到评分列表页(可以评分)
|
||||
uni.navigateTo({
|
||||
url: '/pages/score-list/score-list'
|
||||
})
|
||||
|
||||
@@ -50,7 +50,13 @@
|
||||
</view>
|
||||
|
||||
<view class="score-control">
|
||||
<view class="control-btn decrease" @click="decreaseScore">
|
||||
<!-- 减分按钮 - 使用 catchtouchstart 阻止事件冒泡 -->
|
||||
<view
|
||||
class="control-btn decrease"
|
||||
@touchstart="onDecreaseStart"
|
||||
@touchend="onDecreaseEnd"
|
||||
@touchcancel="onTouchCancel"
|
||||
>
|
||||
<text class="btn-symbol">-</text>
|
||||
<text class="btn-value">-0.001</text>
|
||||
</view>
|
||||
@@ -60,15 +66,17 @@
|
||||
<text class="no-modify-text">可不改</text>
|
||||
</view>
|
||||
|
||||
<view class="control-btn increase" @click="increaseScore">
|
||||
<!-- 加分按钮 - 使用 catchtouchstart 阻止事件冒泡 -->
|
||||
<view
|
||||
class="control-btn increase"
|
||||
@touchstart="onIncreaseStart"
|
||||
@touchend="onIncreaseEnd"
|
||||
@touchcancel="onTouchCancel"
|
||||
>
|
||||
<text class="btn-symbol">+</text>
|
||||
<text class="btn-value">+0.001</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <view class="modify-tip">
|
||||
裁判长修改:保留3位小数点,超过上限或下限时,按钮置灰
|
||||
</view> -->
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
@@ -114,7 +122,15 @@ export default {
|
||||
originalScore: 8.000,
|
||||
note: '',
|
||||
minScore: 5.0,
|
||||
maxScore: 10.0
|
||||
maxScore: 10.0,
|
||||
// 防止双击的状态管理
|
||||
isTouching: false,
|
||||
touchTimer: null,
|
||||
lastTouchTime: 0,
|
||||
// 长按相关
|
||||
longPressTimer: null,
|
||||
longPressInterval: null,
|
||||
isLongPressing: false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -124,9 +140,9 @@ export default {
|
||||
const globalData = app.globalData || {}
|
||||
|
||||
// 获取当前选手信息(从 score-list-multi 页面传递)
|
||||
const currentAthlete = globalData.currentAthlete || {}
|
||||
const currentAthlete = globalData.currentAthlete ||
|
||||
|
||||
// 获取裁判长ID
|
||||
// 获取主裁判ID
|
||||
this.modifierId = globalData.judgeId
|
||||
|
||||
// 调试信息
|
||||
@@ -141,9 +157,151 @@ export default {
|
||||
if (currentAthlete.athleteId) {
|
||||
await this.loadScoreDetail(currentAthlete.athleteId)
|
||||
}
|
||||
|
||||
// H5 平台特殊处理:禁用双击缩放
|
||||
// #ifdef H5
|
||||
this.disableDoubleTapZoom()
|
||||
// #endif
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 清理定时器
|
||||
this.clearAllTimers()
|
||||
},
|
||||
|
||||
methods: {
|
||||
// #ifdef H5
|
||||
disableDoubleTapZoom() {
|
||||
// 在 H5 环境下,添加额外的事件监听来防止双击缩放
|
||||
this.$nextTick(() => {
|
||||
const decreaseBtn = document.querySelector('.control-btn.decrease')
|
||||
const increaseBtn = document.querySelector('.control-btn.increase')
|
||||
|
||||
const preventZoom = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
if (decreaseBtn) {
|
||||
decreaseBtn.addEventListener('touchstart', preventZoom, { passive: false, capture: true })
|
||||
decreaseBtn.addEventListener('touchend', preventZoom, { passive: false, capture: true })
|
||||
decreaseBtn.addEventListener('touchmove', preventZoom, { passive: false, capture: true })
|
||||
decreaseBtn.addEventListener('click', preventZoom, { passive: false, capture: true })
|
||||
}
|
||||
|
||||
if (increaseBtn) {
|
||||
increaseBtn.addEventListener('touchstart', preventZoom, { passive: false, capture: true })
|
||||
increaseBtn.addEventListener('touchend', preventZoom, { passive: false, capture: true })
|
||||
increaseBtn.addEventListener('touchmove', preventZoom, { passive: false, capture: true })
|
||||
increaseBtn.addEventListener('click', preventZoom, { passive: false, capture: true })
|
||||
}
|
||||
})
|
||||
},
|
||||
// #endif
|
||||
|
||||
clearAllTimers() {
|
||||
if (this.touchTimer) {
|
||||
clearTimeout(this.touchTimer)
|
||||
this.touchTimer = null
|
||||
}
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer)
|
||||
this.longPressTimer = null
|
||||
}
|
||||
if (this.longPressInterval) {
|
||||
clearInterval(this.longPressInterval)
|
||||
this.longPressInterval = null
|
||||
}
|
||||
},
|
||||
|
||||
// 减分按钮 - touchstart
|
||||
onDecreaseStart(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
// 防止快速连续触摸(300ms内的触摸被忽略)
|
||||
if (now - this.lastTouchTime < 300) {
|
||||
return
|
||||
}
|
||||
|
||||
this.lastTouchTime = now
|
||||
this.isTouching = true
|
||||
|
||||
// 立即执行一次减分
|
||||
this.decreaseScore()
|
||||
|
||||
// 设置长按定时器(500ms后开始连续减分)
|
||||
this.longPressTimer = setTimeout(() => {
|
||||
this.isLongPressing = true
|
||||
// 每100ms执行一次减分
|
||||
this.longPressInterval = setInterval(() => {
|
||||
this.decreaseScore()
|
||||
}, 100)
|
||||
}, 500)
|
||||
},
|
||||
|
||||
// 减分按钮 - touchend
|
||||
onDecreaseEnd(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.isTouching = false
|
||||
this.isLongPressing = false
|
||||
this.clearAllTimers()
|
||||
},
|
||||
|
||||
// 加分按钮 - touchstart
|
||||
onIncreaseStart(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
// 防止快速连续触摸(300ms内的触摸被忽略)
|
||||
if (now - this.lastTouchTime < 300) {
|
||||
return
|
||||
}
|
||||
|
||||
this.lastTouchTime = now
|
||||
this.isTouching = true
|
||||
|
||||
// 立即执行一次加分
|
||||
this.increaseScore()
|
||||
|
||||
// 设置长按定时器(500ms后开始连续加分)
|
||||
this.longPressTimer = setTimeout(() => {
|
||||
this.isLongPressing = true
|
||||
// 每100ms执行一次加分
|
||||
this.longPressInterval = setInterval(() => {
|
||||
this.increaseScore()
|
||||
}, 100)
|
||||
}, 500)
|
||||
},
|
||||
|
||||
// 加分按钮 - touchend
|
||||
onIncreaseEnd(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.isTouching = false
|
||||
this.isLongPressing = false
|
||||
this.clearAllTimers()
|
||||
},
|
||||
|
||||
// 触摸取消
|
||||
onTouchCancel(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.isTouching = false
|
||||
this.isLongPressing = false
|
||||
this.clearAllTimers()
|
||||
},
|
||||
|
||||
async loadScoreDetail(athleteId) {
|
||||
try {
|
||||
uni.showLoading({
|
||||
@@ -151,9 +309,6 @@ export default {
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 🔥 关键改动:使用 dataAdapter 获取评分详情
|
||||
// Mock模式:调用 mock/score.js 的 getScoreDetail 函数
|
||||
// API模式:调用 api/score.js 的 getScoreDetail 函数(GET /api/mini/score/detail/{athleteId})
|
||||
const response = await dataAdapter.getData('getScoreDetail', {
|
||||
athleteId: athleteId
|
||||
})
|
||||
@@ -202,12 +357,26 @@ export default {
|
||||
decreaseScore() {
|
||||
if (this.currentScore > this.minScore) {
|
||||
this.currentScore = parseFloat((this.currentScore - 0.001).toFixed(3))
|
||||
|
||||
// 添加触觉反馈(仅在支持的平台)
|
||||
// #ifndef H5
|
||||
uni.vibrateShort({
|
||||
type: 'light'
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
|
||||
increaseScore() {
|
||||
if (this.currentScore < this.maxScore) {
|
||||
this.currentScore = parseFloat((this.currentScore + 0.001).toFixed(3))
|
||||
|
||||
// 添加触觉反馈(仅在支持的平台)
|
||||
// #ifndef H5
|
||||
uni.vibrateShort({
|
||||
type: 'light'
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
|
||||
@@ -236,14 +405,16 @@ export default {
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 🔥 关键改动:使用 dataAdapter 修改评分
|
||||
// Mock模式:调用 mock/score.js 的 modifyScore 函数
|
||||
// API模式:调用 api/score.js 的 modifyScore 函数(PUT /api/mini/score/modify)
|
||||
// 获取场地ID
|
||||
const app = getApp()
|
||||
const venueId = app.globalData?.currentVenueId
|
||||
|
||||
const response = await dataAdapter.getData('modifyScore', {
|
||||
athleteId: this.athleteInfo.athleteId,
|
||||
modifierId: this.modifierId,
|
||||
modifiedScore: this.currentScore,
|
||||
note: this.note
|
||||
note: this.note,
|
||||
venueId: venueId // 添加场地ID
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
@@ -465,8 +636,16 @@ export default {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #F5F5F5;
|
||||
border-radius: 12rpx;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
/* 关键:禁用所有可能导致缩放的触摸行为 */
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.control-btn.decrease {
|
||||
@@ -480,6 +659,7 @@ export default {
|
||||
.btn-symbol {
|
||||
font-size: 48rpx;
|
||||
font-weight: 300;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.control-btn.decrease .btn-symbol {
|
||||
@@ -493,6 +673,7 @@ export default {
|
||||
.btn-value {
|
||||
font-size: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.control-btn.decrease .btn-value {
|
||||
@@ -521,13 +702,6 @@ export default {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.modify-tip {
|
||||
font-size: 24rpx;
|
||||
color: #FF4D6A;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 备注 */
|
||||
.note-section {
|
||||
margin: 30rpx;
|
||||
|
||||
@@ -31,28 +31,22 @@
|
||||
<view class="score-control">
|
||||
<view class="control-btn decrease" @click="decreaseScore">
|
||||
<text class="btn-symbol">-</text>
|
||||
<!-- <text class="btn-value">-0.001</text> -->
|
||||
</view>
|
||||
|
||||
<view class="score-display">
|
||||
<view class="score-display" @click="showScoreInput">
|
||||
<text class="current-score">{{ currentScore.toFixed(3) }}</text>
|
||||
<text class="edit-hint">点击编辑</text>
|
||||
</view>
|
||||
|
||||
<view class="control-btn increase" @click="increaseScore">
|
||||
<text class="btn-symbol">+</text>
|
||||
<!-- <text class="btn-value">+0.001</text> -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <view class="judge-tip">
|
||||
裁判评分:保留3位小数点,超过上限或下限时,按钮置灰
|
||||
</view> -->
|
||||
|
||||
<!-- 扣分项 -->
|
||||
<view class="deduction-section">
|
||||
<view class="deduction-header">
|
||||
<text class="deduction-label">扣分项:</text>
|
||||
<!-- <text class="deduction-hint">扣分项多选</text> -->
|
||||
</view>
|
||||
|
||||
<view class="deduction-list">
|
||||
@@ -82,12 +76,35 @@
|
||||
v-model="note"
|
||||
maxlength="200"
|
||||
/>
|
||||
<!-- <text class="optional-text">可不填</text> -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button class="submit-btn" @click="handleSubmit">提交</button>
|
||||
|
||||
<!-- 分数输入弹窗 -->
|
||||
<view v-if="showInputModal" class="modal-overlay" @click="hideScoreInput">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">输入分数</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<input
|
||||
type="digit"
|
||||
class="score-input"
|
||||
v-model="inputScore"
|
||||
placeholder="请输入5-10之间的分数"
|
||||
:focus="showInputModal"
|
||||
@confirm="confirmScoreInput"
|
||||
/>
|
||||
<text class="input-hint">分数范围:{{ minScore }} - {{ maxScore }},保留3位小数</text>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="modal-btn cancel" @click="hideScoreInput">取消</button>
|
||||
<button class="modal-btn confirm" @click="confirmScoreInput">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -107,20 +124,22 @@ export default {
|
||||
},
|
||||
judgeId: '',
|
||||
projectId: '',
|
||||
competitionId: '',
|
||||
venueId: '',
|
||||
currentScore: 8.000,
|
||||
note: '',
|
||||
minScore: 5.0,
|
||||
maxScore: 10.0,
|
||||
deductions: []
|
||||
deductions: [],
|
||||
showInputModal: false,
|
||||
inputScore: ''
|
||||
}
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
// 获取全局数据
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
|
||||
// 加载当前选手信息(从 score-list 页面传递)
|
||||
const currentAthlete = globalData.currentAthlete || {}
|
||||
this.player = {
|
||||
athleteId: currentAthlete.athleteId || '',
|
||||
@@ -130,49 +149,44 @@ export default {
|
||||
number: currentAthlete.number || ''
|
||||
}
|
||||
|
||||
// 如果选手已评分,加载其原有评分
|
||||
if (currentAthlete.scored && currentAthlete.myScore) {
|
||||
this.currentScore = currentAthlete.myScore
|
||||
}
|
||||
|
||||
// 加载评委ID和项目ID
|
||||
this.judgeId = globalData.judgeId
|
||||
const projects = globalData.projects || []
|
||||
const currentIndex = globalData.currentProjectIndex || 0
|
||||
const currentProject = projects[currentIndex] || {}
|
||||
this.projectId = currentProject.projectId
|
||||
this.projectId = globalData.currentProjectId || ''
|
||||
this.competitionId = globalData.matchId || globalData.matchCode || ''
|
||||
this.venueId = globalData.currentVenueId || globalData.venueId || ''
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('评分详情页加载:', {
|
||||
athlete: this.player,
|
||||
judgeId: this.judgeId,
|
||||
projectId: this.projectId,
|
||||
competitionId: this.competitionId,
|
||||
venueId: this.venueId,
|
||||
initialScore: this.currentScore
|
||||
})
|
||||
}
|
||||
|
||||
// 加载扣分项列表
|
||||
await this.loadDeductions()
|
||||
},
|
||||
|
||||
methods: {
|
||||
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,
|
||||
const records = response.data && response.data.records ? response.data.records : []
|
||||
this.deductions = records.map(item => ({
|
||||
deductionId: item.id,
|
||||
deductionName: item.itemName,
|
||||
deductionScore: parseFloat(item.deductionPoint || 0),
|
||||
checked: false
|
||||
}))
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('扣分项加载成功:', this.deductions)
|
||||
}
|
||||
@@ -187,7 +201,19 @@ export default {
|
||||
},
|
||||
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
if (config.debug) {
|
||||
console.log('返回上一页')
|
||||
}
|
||||
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: (err) => {
|
||||
console.error('返回失败:', err)
|
||||
uni.redirectTo({
|
||||
url: '/pages/score-list/score-list'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
decreaseScore() {
|
||||
@@ -202,12 +228,44 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
showScoreInput() {
|
||||
this.inputScore = this.currentScore.toFixed(3)
|
||||
this.showInputModal = true
|
||||
},
|
||||
|
||||
hideScoreInput() {
|
||||
this.showInputModal = false
|
||||
this.inputScore = ''
|
||||
},
|
||||
|
||||
confirmScoreInput() {
|
||||
const score = parseFloat(this.inputScore)
|
||||
|
||||
if (isNaN(score)) {
|
||||
uni.showToast({
|
||||
title: '请输入有效的数字',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (score < this.minScore || score > this.maxScore) {
|
||||
uni.showToast({
|
||||
title: `分数必须在${this.minScore}-${this.maxScore}之间`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.currentScore = parseFloat(score.toFixed(3))
|
||||
this.hideScoreInput()
|
||||
},
|
||||
|
||||
toggleDeduction(index) {
|
||||
this.deductions[index].checked = !this.deductions[index].checked
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
// 验证评分范围
|
||||
if (this.currentScore < this.minScore || this.currentScore > this.maxScore) {
|
||||
uni.showToast({
|
||||
title: `评分必须在${this.minScore}-${this.maxScore}分之间`,
|
||||
@@ -216,14 +274,25 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
// 收集选中的扣分项
|
||||
if (!this.competitionId) {
|
||||
uni.showToast({
|
||||
title: '缺少比赛ID,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.projectId) {
|
||||
uni.showToast({
|
||||
title: '缺少项目ID,请返回重新选择',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const selectedDeductions = this.deductions
|
||||
.filter(item => item.checked)
|
||||
.map(item => ({
|
||||
deductionId: item.deductionId,
|
||||
deductionName: item.deductionName,
|
||||
deductionScore: item.deductionScore
|
||||
}))
|
||||
.map(item => item.deductionId)
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
@@ -231,20 +300,25 @@ export default {
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 🔥 关键改动:使用 dataAdapter 提交评分
|
||||
// Mock模式:调用 mock/score.js 的 submitScore 函数
|
||||
// API模式:调用 api/score.js 的 submitScore 函数(POST /martial/score/submit)
|
||||
const response = await dataAdapter.getData('submitScore', {
|
||||
const submitData = {
|
||||
athleteId: this.player.athleteId,
|
||||
judgeId: this.judgeId,
|
||||
projectId: this.projectId,
|
||||
competitionId: this.competitionId,
|
||||
venueId: this.venueId,
|
||||
score: this.currentScore,
|
||||
deductions: selectedDeductions,
|
||||
note: this.note
|
||||
})
|
||||
}
|
||||
|
||||
if (config.debug) {
|
||||
console.log('准备提交评分数据:', submitData)
|
||||
}
|
||||
|
||||
const response = await dataAdapter.getData('submitScore', submitData)
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('评分提交成功:', {
|
||||
athleteId: this.player.athleteId,
|
||||
@@ -254,14 +328,12 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
// 返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
@@ -301,12 +373,19 @@ export default {
|
||||
|
||||
.nav-left {
|
||||
position: absolute;
|
||||
left: 30rpx;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 120rpx;
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-left:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
@@ -435,6 +514,14 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.score-display:active {
|
||||
background-color: rgba(27, 124, 94, 0.1);
|
||||
}
|
||||
|
||||
.current-score {
|
||||
@@ -443,6 +530,12 @@ export default {
|
||||
color: #1B7C5E;
|
||||
}
|
||||
|
||||
.edit-hint {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.judge-tip {
|
||||
padding: 0 30rpx;
|
||||
font-size: 24rpx;
|
||||
@@ -578,4 +671,95 @@ export default {
|
||||
.submit-btn:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 分数输入弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 600rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 40rpx 30rpx 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20rpx 30rpx 30rpx;
|
||||
}
|
||||
|
||||
.score-input {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
border: 2rpx solid #E0E0E0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 36rpx;
|
||||
text-align: center;
|
||||
color: #1B7C5E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.score-input:focus {
|
||||
border-color: #1B7C5E;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
border-top: 1rpx solid #E0E0E0;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
color: #666666;
|
||||
border-right: 1rpx solid #E0E0E0;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
color: #1B7C5E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</scroll-view>
|
||||
|
||||
<view class="venue-tip">
|
||||
<!-- <text class="tip-bold">裁判长可看见所有场地和项目</text> -->
|
||||
<!-- <text class="tip-bold">主裁判可看见所有场地和项目</text> -->
|
||||
<!-- <text class="tip-normal">(场地和项目可动态全部),可以点击切换</text> -->
|
||||
</view>
|
||||
|
||||
@@ -68,13 +68,20 @@
|
||||
<view class="player-header">
|
||||
<view class="player-name">{{ player.name }}</view>
|
||||
|
||||
<!-- 动作区域:始终显示 -->
|
||||
<view class="action-area">
|
||||
<!-- 已评分:显示总分和修改按钮 -->
|
||||
<view class="action-area" v-if="player.totalScore">
|
||||
<template v-if="player.scoringComplete && player.totalScore > 0">
|
||||
<text class="total-score">总分:{{ player.totalScore }}</text>
|
||||
<view class="chief-actions">
|
||||
<!-- <text class="chief-hint">裁判长功能:修改评分、修改按钮需等总分出来才出现</text> -->
|
||||
<button class="modify-btn" @click="goToModify(player)">修改</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 未评分:显示评分中提示 -->
|
||||
<template v-else>
|
||||
<text class="scoring-status">评分中...</text>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -123,12 +130,12 @@ export default {
|
||||
time: globalData.matchTime || '比赛时间'
|
||||
}
|
||||
|
||||
// 注意:裁判长没有固定场地和项目,需要查看所有
|
||||
// 注意:主裁判没有固定场地和项目,需要查看所有
|
||||
this.competitionId = globalData.matchId
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('裁判长列表页加载:', {
|
||||
console.log('主裁判列表页加载:', {
|
||||
userRole: globalData.userRole,
|
||||
competitionId: this.competitionId
|
||||
})
|
||||
@@ -205,7 +212,7 @@ export default {
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 🔥 关键改动:使用 dataAdapter 获取选手列表(裁判长视图)
|
||||
// 🔥 关键改动:使用 dataAdapter 获取选手列表(主裁判视图)
|
||||
// Mock模式:调用 mock/athlete.js 的 getAthletesForAdmin 函数
|
||||
// API模式:调用 api/athlete.js 的 getAthletesForAdmin 函数(GET /api/mini/athletes/admin)
|
||||
const response = await dataAdapter.getData('getAthletesForAdmin', {
|
||||
@@ -217,11 +224,11 @@ export default {
|
||||
uni.hideLoading()
|
||||
|
||||
// 保存选手列表
|
||||
this.players = response.data || []
|
||||
this.players = (response.data.records || response.data) || []
|
||||
|
||||
// 计算评分统计(裁判长视图:统计有总分的选手)
|
||||
// 计算评分统计(主裁判视图:统计有总分的选手)
|
||||
this.totalCount = this.players.length
|
||||
this.scoredCount = this.players.filter(p => p.totalScore).length
|
||||
this.scoredCount = this.players.filter(p => p.scoringComplete).length
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
@@ -389,6 +396,8 @@ export default {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.venue-tab.active {
|
||||
@@ -441,6 +450,8 @@ export default {
|
||||
color: #666666;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.project-btn.active {
|
||||
@@ -505,6 +516,15 @@ export default {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.scoring-status {
|
||||
font-size: 26rpx;
|
||||
color: #FF9800;
|
||||
font-weight: 500;
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: #FFF3E0;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.chief-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -527,6 +547,8 @@ export default {
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.modify-btn:active {
|
||||
|
||||
@@ -18,11 +18,28 @@
|
||||
<!-- 场地和项目选择 -->
|
||||
<view class="venue-section">
|
||||
<view class="venue-header">
|
||||
<view class="venue-tab active">{{ venueInfo.name }}</view>
|
||||
<view
|
||||
class="venue-tab"
|
||||
:class="{ active: index === currentVenueIndex }"
|
||||
v-for="(venue, index) in venues"
|
||||
:key="venue.id"
|
||||
@click="switchVenue(index)"
|
||||
>
|
||||
{{ venue.venueName }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="project-section">
|
||||
<view class="project-btn active">{{ projectInfo.name }}</view>
|
||||
<view
|
||||
class="project-btn"
|
||||
:class="{ active: index === currentProjectIndex }"
|
||||
v-for="(project, index) in projects"
|
||||
:key="project.id"
|
||||
@click="switchProject(index)"
|
||||
>
|
||||
{{ project.projectName }}
|
||||
</view>
|
||||
<view class="no-project-tip" v-if="projects.length === 0">当前场地暂无比赛项目</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -33,34 +50,52 @@
|
||||
</view>
|
||||
|
||||
<!-- 选手列表 -->
|
||||
<view class="player-list">
|
||||
<view class="player-list" v-if="projects.length > 0">
|
||||
<!-- 遍历选手列表 -->
|
||||
<view
|
||||
class="player-card"
|
||||
v-for="player in players"
|
||||
:key="player.athleteId"
|
||||
@click="handlePlayerClick(player)"
|
||||
>
|
||||
<view class="player-header">
|
||||
<view class="player-name">{{ player.name }}</view>
|
||||
|
||||
<!-- 已评分:显示我的评分和总分 -->
|
||||
<view class="player-scores" v-if="player.scored">
|
||||
<text class="my-score">我的评分:{{ player.myScore }}</text>
|
||||
<text class="total-score">总分:{{ player.totalScore }}</text>
|
||||
<!-- 主裁判:显示总分和已评分裁判数 -->
|
||||
<view class="player-scores" v-if="refereeType === 1">
|
||||
<text class="total-score">
|
||||
总分:{{ player.scoringComplete ? player.totalScore : '评分中' }}
|
||||
</text>
|
||||
<text class="judge-count">
|
||||
已评分:{{ player.scoredJudgeCount || 0 }}/{{ player.requiredJudgeCount || 0 }}人
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 裁判员:根据评分状态显示不同内容 -->
|
||||
<view class="judge-action" v-else>
|
||||
<!-- 已评分:显示分数和修改按钮 -->
|
||||
<view class="scored-info" v-if="player.scored">
|
||||
<text class="my-score-text">我的评分:{{ player.myScore }}</text>
|
||||
<button
|
||||
class="score-btn modify-btn"
|
||||
@click.stop="goToScoreDetail(player)"
|
||||
>
|
||||
修改
|
||||
</button>
|
||||
</view>
|
||||
<!-- 未评分:显示评分按钮 -->
|
||||
<button
|
||||
class="score-btn"
|
||||
v-else
|
||||
@click="goToScoreDetail(player)"
|
||||
@click.stop="goToScoreDetail(player)"
|
||||
>
|
||||
评分
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="player-info">
|
||||
<view class="info-item">身份证:{{ player.idCard }}</view>
|
||||
<view class="info-item" v-if="player.idCard">身份证:{{ player.idCard }}</view>
|
||||
<view class="info-item">队伍:{{ player.team }}</view>
|
||||
<view class="info-item">编号:{{ player.number }}</view>
|
||||
</view>
|
||||
@@ -89,6 +124,12 @@ export default {
|
||||
name: ''
|
||||
},
|
||||
judgeId: '',
|
||||
matchId: '',
|
||||
refereeType: 2, // 裁判类型(1-主裁判, 2-裁判员)
|
||||
venues: [], // 所有场地列表
|
||||
currentVenueIndex: 0, // 当前选中的场地索引
|
||||
projects: [], // 所有项目列表
|
||||
currentProjectIndex: 0, // 当前选中的项目索引
|
||||
players: [],
|
||||
scoredCount: 0,
|
||||
totalCount: 0
|
||||
@@ -96,6 +137,7 @@ export default {
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
try {
|
||||
// 获取全局数据
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
@@ -106,34 +148,85 @@ export default {
|
||||
time: globalData.matchTime || '比赛时间'
|
||||
}
|
||||
|
||||
// 加载场地信息
|
||||
this.venueInfo = {
|
||||
id: globalData.venueId,
|
||||
name: globalData.venueName || '场地'
|
||||
}
|
||||
|
||||
// 加载项目信息
|
||||
const projects = globalData.projects || []
|
||||
const currentIndex = globalData.currentProjectIndex || 0
|
||||
const currentProject = projects[currentIndex] || {}
|
||||
this.projectInfo = {
|
||||
id: currentProject.projectId,
|
||||
name: currentProject.projectName || '项目'
|
||||
}
|
||||
|
||||
this.judgeId = globalData.judgeId
|
||||
this.matchId = globalData.matchId || globalData.matchCode
|
||||
this.refereeType = globalData.refereeType || 2 // 默认为裁判员
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('初始化数据:', {
|
||||
judgeId: this.judgeId,
|
||||
matchId: this.matchId,
|
||||
matchCode: globalData.matchCode,
|
||||
refereeType: this.refereeType
|
||||
})
|
||||
}
|
||||
|
||||
// 检查必要参数
|
||||
if (!this.matchId) {
|
||||
throw new Error('缺少比赛ID,请重新登录')
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
uni.showLoading({
|
||||
title: '加载中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 1. 先获取场地列表
|
||||
const venuesResponse = await dataAdapter.getData('getVenues', {
|
||||
competitionId: this.matchId
|
||||
})
|
||||
this.venues = venuesResponse.data?.records || []
|
||||
this.currentVenueIndex = 0
|
||||
|
||||
// 设置当前场地信息(使用第一条数据的ID)
|
||||
if (this.venues.length > 0) {
|
||||
this.venueInfo = {
|
||||
id: this.venues[0].id,
|
||||
name: this.venues[0].name
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 再获取项目列表
|
||||
const projectsResponse = await dataAdapter.getData('getProjects', {
|
||||
competitionId: this.matchId
|
||||
})
|
||||
this.projects = projectsResponse.data?.records || []
|
||||
this.currentProjectIndex = 0
|
||||
|
||||
// 设置当前项目信息(使用第一条数据的ID)
|
||||
if (this.projects.length > 0) {
|
||||
this.projectInfo = {
|
||||
id: this.projects[0].id,
|
||||
name: this.projects[0].projectName
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('评分列表页加载:', {
|
||||
judgeId: this.judgeId,
|
||||
venueId: this.venueInfo.id,
|
||||
projectId: this.projectInfo.id
|
||||
projectId: this.projectInfo.id,
|
||||
venuesCount: this.venues.length,
|
||||
projectsCount: this.projects.length
|
||||
})
|
||||
}
|
||||
|
||||
// 加载选手列表
|
||||
// 3. 最后加载选手列表(使用场地和项目的第一条数据ID)
|
||||
await this.loadPlayers()
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('页面加载失败:', error)
|
||||
|
||||
uni.showToast({
|
||||
title: error.message || '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -146,9 +239,10 @@ export default {
|
||||
|
||||
// 🔥 关键改动:使用 dataAdapter 获取选手列表
|
||||
// Mock模式:调用 mock/athlete.js 的 getMyAthletes 函数
|
||||
// API模式:调用 api/athlete.js 的 getMyAthletes 函数(GET /api/mini/athletes)
|
||||
// API模式:调用 api/athlete.js 的 getMyAthletes 函数(GET /api/mini/score/athletes)
|
||||
const response = await dataAdapter.getData('getMyAthletes', {
|
||||
judgeId: this.judgeId,
|
||||
refereeType: this.refereeType, // 传递裁判类型
|
||||
venueId: this.venueInfo.id,
|
||||
projectId: this.projectInfo.id
|
||||
})
|
||||
@@ -182,14 +276,108 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理选手卡片点击
|
||||
* - 主裁判:跳转到查看详情页面
|
||||
* - 裁判员:不处理(通过评分按钮跳转)
|
||||
*/
|
||||
handlePlayerClick(player) {
|
||||
if (this.refereeType === 1) {
|
||||
// 主裁判:查看评分详情
|
||||
this.goToScoreDetail(player)
|
||||
}
|
||||
// 裁判员不处理卡片点击,只能通过评分按钮跳转
|
||||
},
|
||||
|
||||
goToScoreDetail(player) {
|
||||
// 保存当前选手信息到全局数据
|
||||
// 保存当前选手信息、项目ID和场地ID到全局数据
|
||||
const app = getApp()
|
||||
app.globalData.currentAthlete = player
|
||||
app.globalData.currentProjectId = this.projectInfo.id
|
||||
app.globalData.currentVenueId = this.venueInfo.id
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('进入评分详情:', {
|
||||
athleteId: player.athleteId,
|
||||
athleteName: player.name,
|
||||
projectId: this.projectInfo.id,
|
||||
projectName: this.projectInfo.name,
|
||||
venueId: this.venueInfo.id,
|
||||
venueName: this.venueInfo.name,
|
||||
refereeType: this.refereeType
|
||||
})
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: '/pages/score-detail/score-detail'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换场地
|
||||
* @param {Number} index - 场地索引
|
||||
*/
|
||||
async switchVenue(index) {
|
||||
// 如果点击的是当前场地,不做处理
|
||||
if (index === this.currentVenueIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新当前场地索引
|
||||
this.currentVenueIndex = index
|
||||
|
||||
// 更新当前场地信息
|
||||
const currentVenue = this.venues[index] || {}
|
||||
this.venueInfo = {
|
||||
id: currentVenue.id,
|
||||
name: currentVenue.name
|
||||
}
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('切换场地:', {
|
||||
index: index,
|
||||
venueId: this.venueInfo.id,
|
||||
venueName: this.venueInfo.name
|
||||
})
|
||||
}
|
||||
|
||||
// 重新加载选手列表
|
||||
await this.loadPlayers()
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换项目
|
||||
* @param {Number} index - 项目索引
|
||||
*/
|
||||
async switchProject(index) {
|
||||
// 如果点击的是当前项目,不做处理
|
||||
if (index === this.currentProjectIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新当前项目索引
|
||||
this.currentProjectIndex = index
|
||||
|
||||
// 更新当前项目信息
|
||||
const currentProject = this.projects[index] || {}
|
||||
this.projectInfo = {
|
||||
id: currentProject.id,
|
||||
name: currentProject.projectName
|
||||
}
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('切换项目:', {
|
||||
index: index,
|
||||
projectId: this.projectInfo.id,
|
||||
projectName: this.projectInfo.name
|
||||
})
|
||||
}
|
||||
|
||||
// 重新加载选手列表
|
||||
await this.loadPlayers()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,26 +467,41 @@ export default {
|
||||
.venue-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 4rpx solid #1B7C5E;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.venue-header::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.venue-tab {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
position: relative;
|
||||
padding: 20rpx 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #666666;
|
||||
background-color: #F5F5F5;
|
||||
border-radius: 8rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.venue-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -24rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4rpx;
|
||||
.venue-tab:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.venue-tab.active {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
background-color: #1B7C5E;
|
||||
}
|
||||
|
||||
@@ -310,7 +513,15 @@ export default {
|
||||
.project-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.project-section::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.project-btn {
|
||||
@@ -321,6 +532,14 @@ export default {
|
||||
font-size: 28rpx;
|
||||
color: #1B7C5E;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.project-btn.active {
|
||||
@@ -402,6 +621,12 @@ export default {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.judge-count {
|
||||
font-size: 24rpx;
|
||||
color: #1B7C5E;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -427,6 +652,29 @@ export default {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.judge-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scored-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.my-score-text {
|
||||
font-size: 28rpx;
|
||||
color: #1B7C5E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modify-btn {
|
||||
background: linear-gradient(135deg, #FF9500 0%, #FFB340 100%);
|
||||
padding: 10rpx 30rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.player-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -438,4 +686,12 @@ export default {
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.no-project-tip {
|
||||
padding: 30rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
13
postcss.config.js
Normal file
13
postcss.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const autoprefixer = require('autoprefixer')
|
||||
|
||||
// 引入 uni-app 的 postcss 插件来处理 rpx 转换
|
||||
// 使用 postcss.plugin 旧版 API (postcss-loader 3.x 兼容)
|
||||
const postcss = require('postcss')
|
||||
const uniappPlugin = require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
uniappPlugin,
|
||||
autoprefixer
|
||||
]
|
||||
}
|
||||
7
postcss.config.js.bak
Normal file
7
postcss.config.js.bak
Normal file
@@ -0,0 +1,7 @@
|
||||
const autoprefixer = require('autoprefixer')
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
autoprefixer()
|
||||
]
|
||||
}
|
||||
14
public/index.html
Normal file
14
public/index.html
Normal 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
18
src/App.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
|
||||
@import "common/common.css";
|
||||
</style>
|
||||
154
src/api/athlete.js
Normal file
154
src/api/athlete.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* API接口 - 选手模块
|
||||
* 真实后端接口调用(需要后端实现)
|
||||
*/
|
||||
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
/**
|
||||
* 获取我的选手列表(普通评委)
|
||||
* @param {Object} params
|
||||
* @param {String} params.matchCode - 比赛编码(推荐方式)
|
||||
* @param {String} params.judgeId - 评委ID(备选方式)
|
||||
* @param {String} params.venueId - 场地ID(备选方式)
|
||||
* @param {String} params.projectId - 项目ID(备选方式)
|
||||
* @returns {Promise}
|
||||
*
|
||||
* 注意:此接口需要后端实现
|
||||
* 建议路径: GET /api/mini/athletes
|
||||
*
|
||||
* 推荐实现方式:
|
||||
* 1. 优先从 Token 中解析评委信息(最安全)
|
||||
* 2. 或使用 matchCode 参数,后端根据 Token 中的邀请码关联查询
|
||||
* 3. 或使用 judgeId + venueId + projectId 直接查询
|
||||
*/
|
||||
export function getMyAthletes(params) {
|
||||
return request({
|
||||
url: '/mini/score/athletes',
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
refereeType: 2 // 裁判员
|
||||
},
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选手列表(主裁判)
|
||||
* @param {Object} params
|
||||
* @param {String} params.competitionId - 比赛ID
|
||||
* @param {String} params.venueId - 场地ID
|
||||
* @param {String} params.projectId - 项目ID
|
||||
* @returns {Promise}
|
||||
*
|
||||
* 注意:此接口需要后端实现
|
||||
* 建议路径: GET /api/mini/athletes/admin
|
||||
*/
|
||||
export function getAthletesForAdmin(params) {
|
||||
return request({
|
||||
url: '/mini/score/athletes',
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
refereeType: 1 // 主裁判
|
||||
},
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取场地列表
|
||||
* @param {Object} params
|
||||
* @param {String} params.competitionId - 比赛ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getVenues(params) {
|
||||
return request({
|
||||
url: '/martial/venue/list',
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
current: 1,
|
||||
size: 100
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目列表
|
||||
* @param {Object} params
|
||||
* @param {String} params.competitionId - 比赛ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getProjects(params) {
|
||||
return request({
|
||||
url: '/martial/project/list',
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
current: 1,
|
||||
size: 100
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
getMyAthletes,
|
||||
getAthletesForAdmin,
|
||||
getVenues,
|
||||
getProjects
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端接口规范(待实现):
|
||||
*
|
||||
* GET /api/mini/athletes
|
||||
*
|
||||
* 请求参数:
|
||||
* {
|
||||
* "judgeId": "456",
|
||||
* "venueId": "1",
|
||||
* "projectId": "5"
|
||||
* }
|
||||
*
|
||||
* 响应:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "success": true,
|
||||
* "msg": "操作成功",
|
||||
* "data": [
|
||||
* {
|
||||
* "athleteId": "1",
|
||||
* "name": "张三",
|
||||
* "idCard": "123456789000000000",
|
||||
* "team": "少林寺武术大学院",
|
||||
* "number": "123-4567898275",
|
||||
* "myScore": 8.906,
|
||||
* "totalScore": 8.907,
|
||||
* "scored": true,
|
||||
* "scoreTime": "2025-06-25 09:15:00"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* SQL示例:
|
||||
* SELECT
|
||||
* a.id AS athleteId,
|
||||
* a.player_name AS name,
|
||||
* a.id_card AS idCard,
|
||||
* a.team_name AS team,
|
||||
* a.player_no AS number,
|
||||
* a.total_score AS totalScore,
|
||||
* s.score AS myScore,
|
||||
* CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END AS scored,
|
||||
* s.score_time AS scoreTime
|
||||
* FROM martial_athlete a
|
||||
* LEFT JOIN martial_score s
|
||||
* ON a.id = s.athlete_id
|
||||
* AND s.judge_id = #{judgeId}
|
||||
* WHERE a.venue_id = #{venueId}
|
||||
* AND a.project_id = #{projectId}
|
||||
* AND a.is_deleted = 0
|
||||
* ORDER BY a.order_num ASC
|
||||
*/
|
||||
85
src/api/auth.js
Normal file
85
src/api/auth.js
Normal 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
158
src/api/index.js
Normal 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
165
src/api/score.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* API接口 - 评分模块
|
||||
* 真实后端接口调用(需要后端实现)
|
||||
*/
|
||||
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
/**
|
||||
* 获取扣分项列表
|
||||
* @param {Object} params
|
||||
* @param {String} params.projectId - 项目ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getDeductions(params) {
|
||||
return request({
|
||||
url: '/blade-martial/deductionItem/list',
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
current: 1,
|
||||
size: 100
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交评分
|
||||
* @param {Object} data
|
||||
* @param {String} data.athleteId - 选手ID
|
||||
* @param {String} data.judgeId - 评委ID
|
||||
* @param {Number} data.score - 评分
|
||||
* @param {Array} data.deductions - 扣分项
|
||||
* @param {String} data.note - 备注
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function submitScore(data) {
|
||||
return request({
|
||||
url: '/mini/score/submit',
|
||||
method: 'POST',
|
||||
data,
|
||||
showLoading: true,
|
||||
loadingText: '提交中...'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评分详情(主裁判查看)
|
||||
* @param {Object} params
|
||||
* @param {String} params.athleteId - 选手ID
|
||||
* @returns {Promise}
|
||||
*
|
||||
* 注意:此接口需要后端实现
|
||||
* 建议路径: GET /api/mini/score/detail/{athleteId}
|
||||
*/
|
||||
export function getScoreDetail(params) {
|
||||
return request({
|
||||
url: `/mini/score/detail/${params.athleteId}`,
|
||||
method: 'GET',
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改评分(主裁判)
|
||||
* @param {Object} data
|
||||
* @param {String} data.athleteId - 选手ID
|
||||
* @param {String} data.modifierId - 修改人ID
|
||||
* @param {Number} data.modifiedScore - 修改后的分数
|
||||
* @param {String} data.note - 修改原因
|
||||
* @returns {Promise}
|
||||
*
|
||||
* 注意:此接口需要后端实现
|
||||
* 建议路径: PUT /api/mini/score/modify
|
||||
*/
|
||||
export function modifyScore(data) {
|
||||
return request({
|
||||
url: '/mini/score/modify',
|
||||
method: 'PUT',
|
||||
data,
|
||||
showLoading: true,
|
||||
loadingText: '修改中...'
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
getDeductions,
|
||||
submitScore,
|
||||
getScoreDetail,
|
||||
modifyScore
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端接口规范(待实现):
|
||||
*
|
||||
* 1. GET /api/mini/score/detail/{athleteId}
|
||||
*
|
||||
* 响应:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "success": true,
|
||||
* "msg": "操作成功",
|
||||
* "data": {
|
||||
* "athleteInfo": {
|
||||
* "athleteId": "1",
|
||||
* "name": "张三",
|
||||
* "idCard": "123456789000000000",
|
||||
* "team": "少林寺武术大学院",
|
||||
* "number": "123-4567898275",
|
||||
* "totalScore": 8.907
|
||||
* },
|
||||
* "judgeScores": [
|
||||
* {
|
||||
* "judgeId": "1",
|
||||
* "judgeName": "欧阳丽娜",
|
||||
* "score": 8.907,
|
||||
* "scoreTime": "2025-06-25 09:15:00",
|
||||
* "note": ""
|
||||
* }
|
||||
* ],
|
||||
* "modification": null
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* SQL示例:
|
||||
* SELECT
|
||||
* s.judge_id AS judgeId,
|
||||
* s.judge_name AS judgeName,
|
||||
* s.score,
|
||||
* s.score_time AS scoreTime,
|
||||
* s.note
|
||||
* FROM martial_score s
|
||||
* WHERE s.athlete_id = #{athleteId}
|
||||
* ORDER BY s.score_time ASC
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* 2. PUT /api/mini/score/modify
|
||||
*
|
||||
* 请求:
|
||||
* {
|
||||
* "athleteId": "1",
|
||||
* "modifierId": "789",
|
||||
* "modifiedScore": 8.910,
|
||||
* "note": "修改原因"
|
||||
* }
|
||||
*
|
||||
* 响应:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "success": true,
|
||||
* "msg": "修改成功",
|
||||
* "data": {
|
||||
* "athleteId": "1",
|
||||
* "originalScore": 8.907,
|
||||
* "modifiedScore": 8.910,
|
||||
* "modifyTime": "2025-06-25 10:00:00"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 实现逻辑:
|
||||
* 1. 验证权限(只有主裁判可以修改)
|
||||
* 2. 保存 originalScore(如果是第一次修改)
|
||||
* 3. 更新 totalScore
|
||||
* 4. 记录 modifyReason 和 modifyTime
|
||||
*/
|
||||
30
src/common/common.css
Normal file
30
src/common/common.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/* 全局样式 */
|
||||
page {
|
||||
background-color: #F5F5F5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 防止系统样式影响 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 清除默认样式 */
|
||||
view, text, button, input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
73
src/config/env.config.js
Normal file
73
src/config/env.config.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 环境配置文件
|
||||
* 控制应用的数据源模式(Mock数据 或 真实API)
|
||||
*
|
||||
* 使用说明:
|
||||
* 1. Mock模式(UI演示、前端独立开发):设置 dataMode: 'mock'
|
||||
* 2. API模式(真实数据对接):设置 dataMode: 'api'
|
||||
* 3. 可在代码中动态切换模式
|
||||
*/
|
||||
|
||||
const ENV_CONFIG = {
|
||||
// 开发环境配置
|
||||
development: {
|
||||
// 数据模式: 'mock' | 'api'
|
||||
// mock - 使用本地Mock数据(保护UI版本)
|
||||
// api - 调用真实后端接口
|
||||
dataMode: 'api',
|
||||
|
||||
// API基础路径(dataMode为'api'时使用)
|
||||
// uni.request 不支持 devServer proxy,必须用完整地址
|
||||
apiBaseURL: 'https://martial-api.aitisai.com',
|
||||
|
||||
// 请求超时时间(毫秒)
|
||||
timeout: 30000,
|
||||
|
||||
// 调试模式
|
||||
debug: true
|
||||
},
|
||||
|
||||
// 测试环境配置
|
||||
test: {
|
||||
dataMode: 'api',
|
||||
apiBaseURL: 'http://test-api.yourdomain.com',
|
||||
debug: true,
|
||||
timeout: 30000,
|
||||
mockDelay: 0
|
||||
},
|
||||
|
||||
// 生产环境配置
|
||||
production: {
|
||||
dataMode: 'api',
|
||||
apiBaseURL: 'https://martial-api.aitisai.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
11
src/main.js
Normal 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
67
src/manifest.json
Normal 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
162
src/mock/athlete.js
Normal 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
117
src/mock/index.js
Normal 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
56
src/mock/login.js
Normal 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
162
src/mock/score.js
Normal 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
|
||||
}
|
||||
54
src/pages.json
Normal file
54
src/pages.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/score-list/score-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"onReachBottomDistance": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/modify-score/modify-score",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/score-list-multi/score-list-multi",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/score-detail/score-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/general-judge/general-judge",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "评分系统",
|
||||
"navigationBarBackgroundColor": "#1B7C5E",
|
||||
"backgroundColor": "#F5F5F5"
|
||||
}
|
||||
}
|
||||
556
src/pages/general-judge/general-judge.vue
Normal file
556
src/pages/general-judge/general-judge.vue
Normal file
@@ -0,0 +1,556 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-title">总裁评分系统</view>
|
||||
<view class="nav-right">
|
||||
<view class="logout-btn" @click="handleLogout">退出</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-info">
|
||||
<view class="user-name">{{ judgeName }}</view>
|
||||
<view class="user-role">总裁(裁判长)</view>
|
||||
<view class="match-name">{{ matchName }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 场地选择 -->
|
||||
<view class="venue-section">
|
||||
<view class="section-title">选择场地</view>
|
||||
<view class="venue-list">
|
||||
<view
|
||||
class="venue-item"
|
||||
:class="{ active: selectedVenueId === null }"
|
||||
@click="selectVenue(null)"
|
||||
>
|
||||
<text>全部场地</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="venue in venues"
|
||||
:key="venue.id"
|
||||
class="venue-item"
|
||||
:class="{ active: selectedVenueId === venue.id }"
|
||||
@click="selectVenue(venue.id)"
|
||||
>
|
||||
<text>{{ venue.venueName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待确认成绩列表 -->
|
||||
<view class="result-section">
|
||||
<view class="section-title">
|
||||
待确认成绩
|
||||
<text class="count">({{ pendingResults.length }})</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="pendingResults.length === 0" class="empty">
|
||||
<text>暂无待确认成绩</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="result-list">
|
||||
<view
|
||||
v-for="result in pendingResults"
|
||||
:key="result.id"
|
||||
class="result-item pending"
|
||||
@click="showConfirmDialog(result)"
|
||||
>
|
||||
<view class="result-info">
|
||||
<view class="player-name">{{ result.playerName }}</view>
|
||||
<view class="team-name">{{ result.teamName }}</view>
|
||||
</view>
|
||||
<view class="score-info">
|
||||
<view class="chief-score">
|
||||
<text class="label">主裁判分:</text>
|
||||
<text class="value">{{ result.chiefJudgeScore || result.finalScore }}</text>
|
||||
</view>
|
||||
<view class="status-tag pending">待确认</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已确认成绩列表 -->
|
||||
<view class="result-section">
|
||||
<view class="section-title">
|
||||
已确认成绩
|
||||
<text class="count">({{ confirmedResults.length }})</text>
|
||||
</view>
|
||||
|
||||
<view v-if="confirmedResults.length === 0" class="empty">
|
||||
<text>暂无已确认成绩</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="result-list">
|
||||
<view
|
||||
v-for="result in confirmedResults"
|
||||
:key="result.id"
|
||||
class="result-item confirmed"
|
||||
>
|
||||
<view class="result-info">
|
||||
<view class="player-name">{{ result.playerName }}</view>
|
||||
<view class="team-name">{{ result.teamName }}</view>
|
||||
</view>
|
||||
<view class="score-info">
|
||||
<view class="chief-score">
|
||||
<text class="label">最终得分:</text>
|
||||
<text class="value confirmed">{{ result.finalScore }}</text>
|
||||
</view>
|
||||
<view class="status-tag confirmed">已确认</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<view v-if="showDialog" class="dialog-mask" @click="closeDialog">
|
||||
<view class="dialog-content" @click.stop>
|
||||
<view class="dialog-title">确认/修改分数</view>
|
||||
<view class="dialog-body">
|
||||
<view class="info-row">
|
||||
<text class="label">选手:</text>
|
||||
<text class="value">{{ currentResult.playerName }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">主裁判分:</text>
|
||||
<text class="value">{{ currentResult.chiefJudgeScore || currentResult.finalScore }}</text>
|
||||
</view>
|
||||
<view class="input-row">
|
||||
<text class="label">确认分数:</text>
|
||||
<input
|
||||
type="digit"
|
||||
v-model="confirmScore"
|
||||
placeholder="留空则确认原分数"
|
||||
class="score-input"
|
||||
/>
|
||||
</view>
|
||||
<view class="input-row">
|
||||
<text class="label">备注:</text>
|
||||
<input
|
||||
type="text"
|
||||
v-model="confirmNote"
|
||||
placeholder="可选"
|
||||
class="note-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dialog-footer">
|
||||
<button class="btn-cancel" @click="closeDialog">取消</button>
|
||||
<button class="btn-confirm" @click="confirmResult">确认</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from "@/config/env.config.js"
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
judgeName: "",
|
||||
matchName: "",
|
||||
matchId: null,
|
||||
venues: [],
|
||||
selectedVenueId: null,
|
||||
pendingResults: [],
|
||||
confirmedResults: [],
|
||||
loading: false,
|
||||
showDialog: false,
|
||||
currentResult: {},
|
||||
confirmScore: "",
|
||||
confirmNote: ""
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
const app = getApp()
|
||||
this.judgeName = app.globalData.judgeName || ""
|
||||
this.matchName = app.globalData.matchName || ""
|
||||
this.matchId = app.globalData.matchId
|
||||
this.loadVenues()
|
||||
this.loadAllResults()
|
||||
},
|
||||
methods: {
|
||||
async loadVenues() {
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: config.apiBaseURL + "/mini/general/venues",
|
||||
method: "GET",
|
||||
data: { competitionId: this.matchId },
|
||||
header: {
|
||||
"Authorization": uni.getStorageSync("token")
|
||||
}
|
||||
})
|
||||
if (res[1].data.success) {
|
||||
this.venues = res[1].data.data || []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载场地失败:", e)
|
||||
}
|
||||
},
|
||||
async loadAllResults() {
|
||||
this.loading = true
|
||||
try {
|
||||
// 加载待确认成绩
|
||||
const pendingRes = await uni.request({
|
||||
url: config.apiBaseURL + "/mini/general/pending",
|
||||
method: "GET",
|
||||
data: { competitionId: this.matchId },
|
||||
header: {
|
||||
"Authorization": uni.getStorageSync("token")
|
||||
}
|
||||
})
|
||||
if (pendingRes[1].data.success) {
|
||||
let results = pendingRes[1].data.data || []
|
||||
if (this.selectedVenueId) {
|
||||
results = results.filter(r => r.venueId === this.selectedVenueId)
|
||||
}
|
||||
this.pendingResults = results
|
||||
}
|
||||
|
||||
// 加载已确认成绩
|
||||
const confirmedRes = await uni.request({
|
||||
url: config.apiBaseURL + "/mini/general/confirmed",
|
||||
method: "GET",
|
||||
data: { competitionId: this.matchId },
|
||||
header: {
|
||||
"Authorization": uni.getStorageSync("token")
|
||||
}
|
||||
})
|
||||
if (confirmedRes[1].data.success) {
|
||||
let results = confirmedRes[1].data.data || []
|
||||
if (this.selectedVenueId) {
|
||||
results = results.filter(r => r.venueId === this.selectedVenueId)
|
||||
}
|
||||
this.confirmedResults = results
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载成绩失败:", e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
selectVenue(venueId) {
|
||||
this.selectedVenueId = venueId
|
||||
this.loadAllResults()
|
||||
},
|
||||
showConfirmDialog(result) {
|
||||
this.currentResult = result
|
||||
this.confirmScore = ""
|
||||
this.confirmNote = ""
|
||||
this.showDialog = true
|
||||
},
|
||||
closeDialog() {
|
||||
this.showDialog = false
|
||||
this.currentResult = {}
|
||||
},
|
||||
async confirmResult() {
|
||||
const app = getApp()
|
||||
try {
|
||||
uni.showLoading({ title: "提交中...", mask: true })
|
||||
const res = await uni.request({
|
||||
url: config.apiBaseURL + "/mini/general/confirm",
|
||||
method: "POST",
|
||||
data: {
|
||||
resultId: String(this.currentResult.id),
|
||||
generalJudgeId: String(app.globalData.judgeId),
|
||||
score: this.confirmScore ? parseFloat(this.confirmScore) : null,
|
||||
note: this.confirmNote || null
|
||||
},
|
||||
header: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": uni.getStorageSync("token")
|
||||
}
|
||||
})
|
||||
uni.hideLoading()
|
||||
if (res[1].data.success) {
|
||||
uni.showToast({ title: "确认成功", icon: "success" })
|
||||
this.closeDialog()
|
||||
this.loadAllResults()
|
||||
} else {
|
||||
uni.showToast({ title: res[1].data.msg || "确认失败", icon: "none" })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: "网络错误", icon: "none" })
|
||||
}
|
||||
},
|
||||
handleLogout() {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.removeStorageSync("token")
|
||||
uni.reLaunch({ url: "/pages/login/login" })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: 90rpx;
|
||||
background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
padding: 10rpx 20rpx;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%);
|
||||
padding: 30rpx;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.9;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.match-name {
|
||||
font-size: 26rpx;
|
||||
opacity: 0.8;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.venue-section, .result-section {
|
||||
margin: 20rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.venue-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.venue-item {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #F5F5F5;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.venue-item.active {
|
||||
background: #8B4513;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
background: #FAFAFA;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.result-item.pending {
|
||||
border-left: 6rpx solid #FF9800;
|
||||
}
|
||||
|
||||
.result-item.confirmed {
|
||||
border-left: 6rpx solid #4CAF50;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.team-name {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.score-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chief-score .label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chief-score .value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #FF9800;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.chief-score .value.confirmed {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.status-tag.pending {
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.status-tag.confirmed {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
width: 80%;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #EEE;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.info-row, .input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.info-row .label, .input-row .label {
|
||||
width: 160rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-row .value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.score-input, .note-input {
|
||||
flex: 1;
|
||||
height: 70rpx;
|
||||
border: 1rpx solid #DDD;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
border-top: 1rpx solid #EEE;
|
||||
}
|
||||
|
||||
.btn-cancel, .btn-confirm {
|
||||
flex: 1;
|
||||
height: 90rpx;
|
||||
line-height: 90rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #F5F5F5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: #8B4513;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
310
src/pages/login/login.vue
Normal file
310
src/pages/login/login.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-title">评分系统</view>
|
||||
<view class="nav-right">
|
||||
<view class="icon-menu">···</view>
|
||||
<view class="icon-close">⊗</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="content">
|
||||
<view class="page-title">进入评分</view>
|
||||
|
||||
<!-- 比赛编码输入 -->
|
||||
<view class="input-group">
|
||||
<view class="input-label">比赛编码</view>
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
placeholder="请输入比赛编码"
|
||||
v-model="matchCode"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评委邀请码输入 -->
|
||||
<view class="input-group">
|
||||
<view class="input-label">评委邀请码</view>
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
placeholder="请输入评委邀请码"
|
||||
v-model="inviteCode"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 立即评分按钮 -->
|
||||
<button class="submit-btn" @click="handleSubmit">立即评分</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dataAdapter from '@/utils/dataAdapter.js'
|
||||
import config from '@/config/env.config.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
matchCode: '',
|
||||
inviteCode: ''
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// 开发环境显示当前数据模式
|
||||
if (config.debug) {
|
||||
console.log('='.repeat(50))
|
||||
console.log('当前数据模式:', config.dataMode)
|
||||
console.log('Mock模式:', dataAdapter.isMockMode() ? '是' : '否')
|
||||
console.log('API模式:', dataAdapter.isApiMode() ? '是' : '否')
|
||||
console.log('='.repeat(50))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleSubmit() {
|
||||
// 表单验证
|
||||
if (!this.matchCode) {
|
||||
uni.showToast({
|
||||
title: '请输入比赛编码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!this.inviteCode) {
|
||||
uni.showToast({
|
||||
title: '请输入评委邀请码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示加载
|
||||
uni.showLoading({
|
||||
title: '登录中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 🔥 关键改动:使用 dataAdapter 进行登录
|
||||
// Mock模式:调用 mock/login.js 的 login 函数
|
||||
// API模式:调用 api/auth.js 的 login 函数(POST /api/mini/login)
|
||||
const response = await dataAdapter.getData('login', {
|
||||
matchCode: this.matchCode,
|
||||
inviteCode: this.inviteCode
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
// 处理登录响应(Mock和API返回格式相同)
|
||||
const {
|
||||
token,
|
||||
userRole,
|
||||
matchId,
|
||||
matchName,
|
||||
matchTime,
|
||||
judgeId,
|
||||
judgeName,
|
||||
venueId,
|
||||
venueName,
|
||||
projects
|
||||
} = response.data
|
||||
|
||||
// 保存Token到本地存储
|
||||
uni.setStorageSync('token', token)
|
||||
|
||||
// 保存用户信息到全局数据
|
||||
getApp().globalData = {
|
||||
token, // Token(用于登录状态检查)
|
||||
userRole, // 'pub' 或 'admin'
|
||||
matchCode: this.matchCode, // 比赛编码
|
||||
inviteCode: this.inviteCode, // 邀请码(重要:用于后续API调用)
|
||||
matchId,
|
||||
matchName,
|
||||
matchTime,
|
||||
judgeId,
|
||||
judgeName,
|
||||
venueId, // 普通评委有场地,主裁判为null
|
||||
venueName,
|
||||
projects, // 分配的项目列表
|
||||
currentProjectIndex: 0 // 当前选中的项目索引
|
||||
}
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('登录成功:', {
|
||||
userRole,
|
||||
judgeName,
|
||||
venueId: venueId || '全部场地',
|
||||
projects: projects.length + '个项目'
|
||||
})
|
||||
}
|
||||
|
||||
// 显示登录成功提示
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
// 根据角色跳转到不同页面
|
||||
setTimeout(() => {
|
||||
if (userRole === 'general') {
|
||||
// 总裁跳转到总裁专用页面
|
||||
uni.navigateTo({
|
||||
url: '/pages/general-judge/general-judge'
|
||||
})
|
||||
} else if (userRole === 'admin') {
|
||||
// 主裁判跳转到多场地列表页(可以修改评分)
|
||||
uni.navigateTo({
|
||||
url: '/pages/score-list-multi/score-list-multi'
|
||||
})
|
||||
} else {
|
||||
// 裁判员跳转到评分列表页(可以评分)
|
||||
uni.navigateTo({
|
||||
url: '/pages/score-list/score-list'
|
||||
})
|
||||
}
|
||||
}, 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>
|
||||
675
src/pages/modify-score/modify-score.vue
Normal file
675
src/pages/modify-score/modify-score.vue
Normal file
@@ -0,0 +1,675 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-left" @click="goBack">
|
||||
<text class="back-icon">‹</text>
|
||||
</view>
|
||||
<view class="nav-title">修改评分</view>
|
||||
<view class="nav-right">
|
||||
<view class="icon-menu">···</view>
|
||||
<view class="icon-close">⊗</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选手信息 -->
|
||||
<view class="player-info-section">
|
||||
<view class="player-header">
|
||||
<view class="player-name">{{ athleteInfo.name }}</view>
|
||||
<view class="total-score-label">
|
||||
<text class="label-text">总分:</text>
|
||||
<text class="score-value">{{ formatScore(athleteInfo.totalScore) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="player-details">
|
||||
<view class="detail-item">身份证:{{ athleteInfo.idCard }}</view>
|
||||
<view class="detail-item">队伍:{{ athleteInfo.team }}</view>
|
||||
<view class="detail-item">编号:{{ athleteInfo.number }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评委评分统计 -->
|
||||
<view class="judges-section">
|
||||
<view class="section-title">共有{{ judgeScores.length }}位评委完成评分</view>
|
||||
<view class="judges-scores">
|
||||
<view
|
||||
class="judge-score-item"
|
||||
v-for="judge in judgeScores"
|
||||
:key="judge.judgeId"
|
||||
>
|
||||
<text class="judge-name">{{ judge.judgeName }}:</text>
|
||||
<text class="judge-score">{{ judge.score }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 修改总分区域 -->
|
||||
<view class="modify-section">
|
||||
<view class="modify-header">
|
||||
<text class="modify-label">修改总分(±0.050分)</text>
|
||||
</view>
|
||||
|
||||
<view class="score-control">
|
||||
<view class="control-btn decrease" @click="decreaseScore">
|
||||
<text class="btn-symbol">-</text>
|
||||
<text class="btn-value">-0.001</text>
|
||||
</view>
|
||||
|
||||
<view class="score-display">
|
||||
<text class="current-score">{{ currentScore.toFixed(3) }}</text>
|
||||
<text class="no-modify-text">可不改</text>
|
||||
</view>
|
||||
|
||||
<view class="control-btn increase" @click="increaseScore">
|
||||
<text class="btn-symbol">+</text>
|
||||
<text class="btn-value">+0.001</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="note-section">
|
||||
<view class="note-label">
|
||||
<text>备注:</text>
|
||||
</view>
|
||||
<view class="note-input-wrapper">
|
||||
<textarea
|
||||
class="note-input"
|
||||
placeholder="请输入修改备注"
|
||||
v-model="note"
|
||||
maxlength="200"
|
||||
/>
|
||||
<text class="optional-text">可不填</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 修改按钮 -->
|
||||
<button class="modify-btn" @click="handleModify">修改</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dataAdapter from '@/utils/dataAdapter.js'
|
||||
import config from '@/config/env.config.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
athleteInfo: {
|
||||
athleteId: '',
|
||||
name: '',
|
||||
idCard: '',
|
||||
team: '',
|
||||
number: '',
|
||||
totalScore: 0
|
||||
},
|
||||
judgeScores: [],
|
||||
modification: null,
|
||||
modifierId: '',
|
||||
currentScore: 0,
|
||||
originalScore: 0,
|
||||
note: '',
|
||||
minScore: 0,
|
||||
maxScore: 10.0
|
||||
}
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
// 获取全局数据
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
|
||||
// 检查登录状态
|
||||
if (!globalData.judgeId || !globalData.token) {
|
||||
console.warn('用户未登录,跳转到登录页')
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是主裁判
|
||||
if (globalData.userRole !== 'admin') {
|
||||
console.warn('非主裁判用户,无权修改评分')
|
||||
uni.showToast({
|
||||
title: '无权限',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有选手信息
|
||||
if (!globalData.currentAthlete || !globalData.currentAthlete.athleteId) {
|
||||
console.warn('没有选手信息,返回列表页')
|
||||
uni.showToast({
|
||||
title: '请选择选手',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前选手信息(从 score-list-multi 页面传递)
|
||||
const currentAthlete = globalData.currentAthlete || {}
|
||||
|
||||
// 获取主裁判ID
|
||||
this.modifierId = globalData.judgeId
|
||||
|
||||
// 🔥 关键修复:先用传递过来的选手数据初始化页面
|
||||
this.athleteInfo = {
|
||||
athleteId: currentAthlete.athleteId,
|
||||
name: currentAthlete.name || '',
|
||||
idCard: currentAthlete.idCard || '',
|
||||
team: currentAthlete.team || '',
|
||||
number: currentAthlete.number || '',
|
||||
totalScore: currentAthlete.totalScore || 0
|
||||
}
|
||||
|
||||
// 设置初始分数(使用传递过来的总分)
|
||||
const totalScore = parseFloat(currentAthlete.totalScore) || 0
|
||||
this.originalScore = totalScore
|
||||
this.currentScore = totalScore
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('修改评分页加载:', {
|
||||
currentAthlete: currentAthlete,
|
||||
athleteId: currentAthlete.athleteId,
|
||||
totalScore: totalScore,
|
||||
modifierId: this.modifierId
|
||||
})
|
||||
}
|
||||
|
||||
// 尝试加载选手评分详情(获取各评委的评分)
|
||||
if (currentAthlete.athleteId) {
|
||||
await this.loadScoreDetail(currentAthlete.athleteId)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatScore(score) {
|
||||
if (score === null || score === undefined || score === -1 || score === '-1') {
|
||||
return '--'
|
||||
}
|
||||
if (typeof score === 'string' && !isNaN(parseFloat(score))) {
|
||||
return parseFloat(score).toFixed(3)
|
||||
}
|
||||
if (typeof score === 'number') {
|
||||
return score.toFixed(3)
|
||||
}
|
||||
return score
|
||||
},
|
||||
|
||||
async loadScoreDetail(athleteId) {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '加载中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
const response = await dataAdapter.getData('getScoreDetail', {
|
||||
athleteId: athleteId
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
// 如果接口返回了数据,更新页面
|
||||
if (response && response.data) {
|
||||
// 更新评委评分列表
|
||||
this.judgeScores = response.data.judgeScores || []
|
||||
this.modification = response.data.modification || null
|
||||
|
||||
// 如果接口返回了选手信息,更新(但保留传递过来的数据作为备用)
|
||||
if (response.data.athleteInfo) {
|
||||
const apiAthleteInfo = response.data.athleteInfo
|
||||
this.athleteInfo = {
|
||||
athleteId: apiAthleteInfo.athleteId || this.athleteInfo.athleteId,
|
||||
name: apiAthleteInfo.name || this.athleteInfo.name,
|
||||
idCard: apiAthleteInfo.idCard || this.athleteInfo.idCard,
|
||||
team: apiAthleteInfo.team || this.athleteInfo.team,
|
||||
number: apiAthleteInfo.number || this.athleteInfo.number,
|
||||
totalScore: apiAthleteInfo.totalScore || this.athleteInfo.totalScore
|
||||
}
|
||||
|
||||
// 更新分数
|
||||
const totalScore = parseFloat(apiAthleteInfo.totalScore) || this.originalScore
|
||||
this.originalScore = totalScore
|
||||
this.currentScore = totalScore
|
||||
}
|
||||
}
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('评分详情加载成功:', {
|
||||
athlete: this.athleteInfo,
|
||||
judges: this.judgeScores.length,
|
||||
originalScore: this.originalScore,
|
||||
currentScore: this.currentScore,
|
||||
modification: this.modification
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('加载评分详情失败:', error)
|
||||
// 不显示错误提示,因为已经有传递过来的数据可以使用
|
||||
if (config.debug) {
|
||||
console.log('使用传递过来的选手数据')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
||||
decreaseScore() {
|
||||
// 限制最小值为原始分数-0.050
|
||||
const minAllowed = parseFloat((this.originalScore - 0.050).toFixed(3))
|
||||
if (this.currentScore > minAllowed) {
|
||||
this.currentScore = parseFloat((this.currentScore - 0.001).toFixed(3))
|
||||
}
|
||||
},
|
||||
|
||||
increaseScore() {
|
||||
// 限制最大值为原始分数+0.050
|
||||
const maxAllowed = parseFloat((this.originalScore + 0.050).toFixed(3))
|
||||
if (this.currentScore < maxAllowed) {
|
||||
this.currentScore = parseFloat((this.currentScore + 0.001).toFixed(3))
|
||||
}
|
||||
},
|
||||
|
||||
async handleModify() {
|
||||
// 验证评分范围(±0.050)
|
||||
const minAllowed = parseFloat((this.originalScore - 0.050).toFixed(3))
|
||||
const maxAllowed = parseFloat((this.originalScore + 0.050).toFixed(3))
|
||||
if (this.currentScore < minAllowed || this.currentScore > maxAllowed) {
|
||||
uni.showToast({
|
||||
title: '评分只能在原始分数±0.050范围内',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有修改
|
||||
if (this.currentScore === this.originalScore && !this.note) {
|
||||
uni.showToast({
|
||||
title: '请修改分数或填写备注',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '提交中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
const response = await dataAdapter.getData('modifyScore', {
|
||||
modifierId: this.modifierId,
|
||||
athleteId: this.athleteInfo.athleteId,
|
||||
modifiedScore: this.currentScore,
|
||||
note: this.note
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('修改评分成功:', {
|
||||
athleteId: this.athleteInfo.athleteId,
|
||||
originalScore: this.originalScore,
|
||||
modifiedScore: this.currentScore,
|
||||
note: this.note,
|
||||
response: response
|
||||
})
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
uni.showToast({
|
||||
title: '修改成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
// 返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('修改评分失败:', error)
|
||||
|
||||
uni.showToast({
|
||||
title: error.message || '修改失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.nav-bar {
|
||||
height: 90rpx;
|
||||
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
position: absolute;
|
||||
left: 30rpx;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 60rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.icon-menu,
|
||||
.icon-close {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 选手信息 */
|
||||
.player-info-section {
|
||||
margin: 30rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.player-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.total-score-label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.player-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 评委评分统计 */
|
||||
.judges-section {
|
||||
margin: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.judges-scores {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.judge-score-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 20rpx;
|
||||
background-color: #F5F5F5;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid #E5E5E5;
|
||||
}
|
||||
|
||||
.judge-name {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.judge-score {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 修改总分区域 */
|
||||
.modify-section {
|
||||
margin: 30rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.modify-header {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.modify-label {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.score-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #F5F5F5;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.control-btn.decrease {
|
||||
background-color: #FFE5E5;
|
||||
}
|
||||
|
||||
.control-btn.increase {
|
||||
background-color: #E5F5F0;
|
||||
}
|
||||
|
||||
.btn-symbol {
|
||||
font-size: 48rpx;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.control-btn.decrease .btn-symbol {
|
||||
color: #FF4D6A;
|
||||
}
|
||||
|
||||
.control-btn.increase .btn-symbol {
|
||||
color: #1B7C5E;
|
||||
}
|
||||
|
||||
.btn-value {
|
||||
font-size: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.control-btn.decrease .btn-value {
|
||||
color: #FF4D6A;
|
||||
}
|
||||
|
||||
.control-btn.increase .btn-value {
|
||||
color: #1B7C5E;
|
||||
}
|
||||
|
||||
.score-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.current-score {
|
||||
font-size: 60rpx;
|
||||
font-weight: 600;
|
||||
color: #1B7C5E;
|
||||
}
|
||||
|
||||
.no-modify-text {
|
||||
font-size: 24rpx;
|
||||
color: #FF4D6A;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.modify-tip {
|
||||
font-size: 24rpx;
|
||||
color: #FF4D6A;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 备注 */
|
||||
.note-section {
|
||||
margin: 30rpx;
|
||||
}
|
||||
|
||||
.note-label {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.note-input-wrapper {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-input {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.note-input::placeholder {
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
.optional-text {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
bottom: 30rpx;
|
||||
font-size: 24rpx;
|
||||
color: #FF4D6A;
|
||||
}
|
||||
|
||||
/* 修改按钮 */
|
||||
.modify-btn {
|
||||
margin: 30rpx;
|
||||
height: 90rpx;
|
||||
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 20rpx rgba(27, 124, 94, 0.3);
|
||||
}
|
||||
|
||||
.modify-btn:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
814
src/pages/score-detail/score-detail.vue
Normal file
814
src/pages/score-detail/score-detail.vue
Normal file
@@ -0,0 +1,814 @@
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<view class="score-display">
|
||||
<input
|
||||
type="digit"
|
||||
class="score-input-inline"
|
||||
:value="scoreInputValue"
|
||||
@input="onScoreInput"
|
||||
@blur="onScoreBlur"
|
||||
@confirm="onScoreConfirm"
|
||||
placeholder="8.000"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="control-btn increase" @click="increaseScore">
|
||||
<text class="btn-symbol">+</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 扣分项 -->
|
||||
<view class="deduction-section">
|
||||
<view class="deduction-header">
|
||||
<text class="deduction-label">扣分项:</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"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button class="submit-btn" @click="handleSubmit">提交</button>
|
||||
|
||||
<!-- 分数输入弹窗 -->
|
||||
<view v-if="showInputModal" class="modal-overlay" @click="hideScoreInput">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">输入分数</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<input
|
||||
type="digit"
|
||||
class="score-input"
|
||||
v-model="inputScore"
|
||||
placeholder="请输入5-10之间的分数"
|
||||
:focus="showInputModal"
|
||||
@confirm="confirmScoreInput"
|
||||
/>
|
||||
<text class="input-hint">分数范围:{{ minScore }} - {{ maxScore }},保留3位小数</text>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="modal-btn cancel" @click="hideScoreInput">取消</button>
|
||||
<button class="modal-btn confirm" @click="confirmScoreInput">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<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: '',
|
||||
competitionId: '',
|
||||
venueId: '',
|
||||
currentScore: 8.000,
|
||||
note: '',
|
||||
minScore: 5.0,
|
||||
maxScore: 10.0,
|
||||
deductions: [],
|
||||
showInputModal: false,
|
||||
inputScore: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
scoreInputValue() {
|
||||
return this.currentScore.toFixed(3)
|
||||
}
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
this.judgeId = globalData.judgeId
|
||||
this.projectId = globalData.currentProjectId || ''
|
||||
this.competitionId = globalData.matchId || globalData.matchCode || ''
|
||||
this.venueId = globalData.currentVenueId || globalData.venueId || ''
|
||||
|
||||
if (config.debug) {
|
||||
console.log('评分详情页加载:', {
|
||||
athlete: this.player,
|
||||
judgeId: this.judgeId,
|
||||
projectId: this.projectId,
|
||||
competitionId: this.competitionId,
|
||||
venueId: this.venueId,
|
||||
initialScore: this.currentScore
|
||||
})
|
||||
}
|
||||
|
||||
await this.loadDeductions()
|
||||
},
|
||||
|
||||
methods: {
|
||||
onScoreInput(e) {
|
||||
// Allow typing, validation happens on blur
|
||||
},
|
||||
|
||||
onScoreBlur(e) {
|
||||
this.validateAndSetScore(e.detail.value)
|
||||
},
|
||||
|
||||
onScoreConfirm(e) {
|
||||
this.validateAndSetScore(e.detail.value)
|
||||
},
|
||||
|
||||
validateAndSetScore(value) {
|
||||
const score = parseFloat(value)
|
||||
|
||||
if (isNaN(score)) {
|
||||
uni.showToast({
|
||||
title: '请输入有效的数字',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (score < this.minScore || score > this.maxScore) {
|
||||
uni.showToast({
|
||||
title: '分数必须在' + this.minScore + '-' + this.maxScore + '之间',
|
||||
icon: 'none'
|
||||
})
|
||||
// Reset to valid range
|
||||
this.currentScore = Math.max(this.minScore, Math.min(this.maxScore, score))
|
||||
return
|
||||
}
|
||||
|
||||
this.currentScore = parseFloat(score.toFixed(3))
|
||||
},
|
||||
|
||||
async loadDeductions() {
|
||||
try {
|
||||
const response = await dataAdapter.getData('getDeductions', {
|
||||
projectId: this.projectId
|
||||
})
|
||||
|
||||
const records = response.data && response.data.records ? response.data.records : []
|
||||
this.deductions = records.map(item => ({
|
||||
deductionId: item.id,
|
||||
deductionName: item.itemName,
|
||||
deductionScore: parseFloat(item.deductionPoint || 0),
|
||||
checked: false
|
||||
}))
|
||||
|
||||
if (config.debug) {
|
||||
console.log('扣分项加载成功:', this.deductions)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载扣分项失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载扣分项失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
if (config.debug) {
|
||||
console.log('返回上一页')
|
||||
}
|
||||
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: (err) => {
|
||||
console.error('返回失败:', err)
|
||||
uni.redirectTo({
|
||||
url: '/pages/score-list/score-list'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
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))
|
||||
}
|
||||
},
|
||||
|
||||
showScoreInput() {
|
||||
this.inputScore = this.currentScore.toFixed(3)
|
||||
this.showInputModal = true
|
||||
},
|
||||
|
||||
hideScoreInput() {
|
||||
this.showInputModal = false
|
||||
this.inputScore = ''
|
||||
},
|
||||
|
||||
confirmScoreInput() {
|
||||
const score = parseFloat(this.inputScore)
|
||||
|
||||
if (isNaN(score)) {
|
||||
uni.showToast({
|
||||
title: '请输入有效的数字',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (score < this.minScore || score > this.maxScore) {
|
||||
uni.showToast({
|
||||
title: `分数必须在${this.minScore}-${this.maxScore}之间`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.currentScore = parseFloat(score.toFixed(3))
|
||||
this.hideScoreInput()
|
||||
},
|
||||
|
||||
toggleDeduction(index) {
|
||||
this.deductions[index].checked = !this.deductions[index].checked
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
if (this.currentScore < this.minScore || this.currentScore > this.maxScore) {
|
||||
uni.showToast({
|
||||
title: `评分必须在${this.minScore}-${this.maxScore}分之间`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.competitionId) {
|
||||
uni.showToast({
|
||||
title: '缺少比赛ID,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.projectId) {
|
||||
uni.showToast({
|
||||
title: '缺少项目ID,请返回重新选择',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const selectedDeductions = this.deductions
|
||||
.filter(item => item.checked)
|
||||
.map(item => item.deductionId)
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '提交中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
const submitData = {
|
||||
athleteId: this.player.athleteId,
|
||||
judgeId: this.judgeId,
|
||||
projectId: this.projectId,
|
||||
competitionId: this.competitionId,
|
||||
venueId: this.venueId,
|
||||
score: this.currentScore,
|
||||
deductions: selectedDeductions,
|
||||
note: this.note
|
||||
}
|
||||
|
||||
if (config.debug) {
|
||||
console.log('准备提交评分数据:', submitData)
|
||||
}
|
||||
|
||||
const response = await dataAdapter.getData('submitScore', submitData)
|
||||
|
||||
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: 0;
|
||||
top: 0;
|
||||
width: 120rpx;
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-left:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
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;
|
||||
justify-content: center;
|
||||
padding: 10rpx;
|
||||
border-radius: 16rpx;
|
||||
min-width: 240rpx;
|
||||
}
|
||||
|
||||
.score-input-inline {
|
||||
width: 200rpx;
|
||||
height: 100rpx;
|
||||
font-size: 64rpx;
|
||||
font-weight: 600;
|
||||
color: #1B7C5E;
|
||||
text-align: center;
|
||||
border: 2rpx solid #E0E0E0;
|
||||
border-radius: 12rpx;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.score-input-inline:focus {
|
||||
border-color: #1B7C5E;
|
||||
}
|
||||
|
||||
.judge-tip {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 分数输入弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 600rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 40rpx 30rpx 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20rpx 30rpx 30rpx;
|
||||
}
|
||||
|
||||
.score-input {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
border: 2rpx solid #E0E0E0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 36rpx;
|
||||
text-align: center;
|
||||
color: #1B7C5E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.score-input:focus {
|
||||
border-color: #1B7C5E;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
border-top: 1rpx solid #E0E0E0;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
color: #666666;
|
||||
border-right: 1rpx solid #E0E0E0;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
color: #1B7C5E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
</style>
|
||||
596
src/pages/score-list-multi/score-list-multi.vue
Normal file
596
src/pages/score-list-multi/score-list-multi.vue
Normal file
@@ -0,0 +1,596 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-title">评分系统</view>
|
||||
<view class="nav-right">
|
||||
<view class="nav-dots">···</view>
|
||||
<view class="nav-circle">◎</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 比赛信息 -->
|
||||
<view class="match-info">
|
||||
<view class="match-title">{{ matchInfo.name }}</view>
|
||||
<view class="match-time">比赛时间:{{ formatDateTime(matchInfo.time) }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 场地和项目卡片 -->
|
||||
<view class="venue-card">
|
||||
<!-- 场地标题行 -->
|
||||
<view class="venue-header">
|
||||
<view class="venue-name">{{ venueInfo.name }}</view>
|
||||
<view class="refresh-link" @click="handleRefresh">刷新</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目筛选 -->
|
||||
<view class="project-row">
|
||||
<view class="project-grid">
|
||||
<view
|
||||
class="project-chip"
|
||||
:class="{ active: index === currentProjectIndex }"
|
||||
v-for="(project, index) in projects"
|
||||
:key="project.projectId"
|
||||
@click="switchProject(index)"
|
||||
>
|
||||
{{ project.projectName }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已评分统计 -->
|
||||
<view class="score-stats">
|
||||
<text class="stats-label">已评分:</text>
|
||||
<text class="stats-value">{{ scoredCount }}/{{ totalCount }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 选手列表 -->
|
||||
<view class="player-list">
|
||||
<!-- 遍历选手列表 -->
|
||||
<view
|
||||
class="player-card"
|
||||
v-for="player in players"
|
||||
:key="player.athleteId"
|
||||
>
|
||||
<view class="card-header">
|
||||
<view class="player-name">{{ player.name }}</view>
|
||||
|
||||
<!-- 已评分:显示总分和修改按钮 -->
|
||||
<view class="action-area" v-if="player.totalScore">
|
||||
<view class="score-tag">
|
||||
<text class="tag-label">总分:</text>
|
||||
<text class="tag-value">{{ formatScore(player.totalScore) }}</text>
|
||||
</view>
|
||||
<button class="modify-btn" @click="goToModify(player)">修改</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="player-details">
|
||||
<view class="detail-row">
|
||||
<text class="detail-text">身份证:{{ player.idCard }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-text">队伍:{{ player.team }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-text">编号:{{ player.number }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-status" v-if="players.length > 0">
|
||||
<view v-if="isLoading" class="loading-text">加载中...</view>
|
||||
<view v-else-if="!hasMore" class="no-more-text">— 没有更多了 —</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!isLoading && players.length === 0">
|
||||
<text class="empty-text">暂无选手数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dataAdapter from '@/utils/dataAdapter.js'
|
||||
import config from '@/config/env.config.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
matchInfo: {
|
||||
name: '',
|
||||
time: ''
|
||||
},
|
||||
venueInfo: {
|
||||
id: '',
|
||||
name: ''
|
||||
},
|
||||
projectInfo: {
|
||||
id: '',
|
||||
name: ''
|
||||
},
|
||||
judgeId: '',
|
||||
projects: [],
|
||||
currentProjectIndex: 0,
|
||||
players: [],
|
||||
scoredCount: 0,
|
||||
totalCount: 0,
|
||||
pagination: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
},
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
isFirstLoad: true
|
||||
}
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
|
||||
// 检查登录状态
|
||||
if (!globalData.judgeId || !globalData.token) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是主裁判
|
||||
if (globalData.userRole !== 'admin') {
|
||||
console.warn('非主裁判用户,跳转到普通评分页')
|
||||
uni.reLaunch({
|
||||
url: '/pages/score-list/score-list'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 加载比赛信息
|
||||
this.matchInfo = {
|
||||
name: globalData.matchName || '比赛名称',
|
||||
time: globalData.matchTime || ''
|
||||
}
|
||||
|
||||
// 从 globalData 获取场地信息(与裁判员相同)
|
||||
this.venueInfo = {
|
||||
id: globalData.venueId,
|
||||
name: globalData.venueName || '场地'
|
||||
}
|
||||
|
||||
// 从 globalData 获取项目列表
|
||||
this.projects = globalData.projects || []
|
||||
this.currentProjectIndex = globalData.currentProjectIndex || 0
|
||||
this.updateCurrentProject()
|
||||
this.judgeId = globalData.judgeId
|
||||
|
||||
// 调试信息
|
||||
if (config.debug) {
|
||||
console.log('主裁判列表页加载:', {
|
||||
userRole: globalData.userRole,
|
||||
judgeId: this.judgeId,
|
||||
venueId: this.venueInfo.id,
|
||||
projectId: this.projectInfo.id
|
||||
})
|
||||
}
|
||||
|
||||
await this.loadPlayers(true)
|
||||
this.isFirstLoad = false
|
||||
},
|
||||
|
||||
async onShow() {
|
||||
// 从修改评分页返回时刷新数据
|
||||
if (!this.isFirstLoad) {
|
||||
if (config.debug) {
|
||||
console.log('页面显示,刷新数据')
|
||||
}
|
||||
await this.loadPlayers(true)
|
||||
}
|
||||
},
|
||||
|
||||
async onPullDownRefresh() {
|
||||
await this.loadPlayers(true)
|
||||
uni.stopPullDownRefresh()
|
||||
},
|
||||
|
||||
async onReachBottom() {
|
||||
if (this.hasMore && !this.isLoading) {
|
||||
await this.loadMore()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return ''
|
||||
try {
|
||||
const date = new Date(dateTimeStr)
|
||||
if (isNaN(date.getTime())) return dateTimeStr
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hours = date.getHours()
|
||||
const minutes = date.getMinutes()
|
||||
const paddedMinutes = minutes < 10 ? '0' + minutes : minutes
|
||||
return year + '年' + month + '月' + day + '日 ' + hours + ':' + paddedMinutes
|
||||
} catch (error) {
|
||||
return dateTimeStr
|
||||
}
|
||||
},
|
||||
|
||||
formatScore(score) {
|
||||
// 处理 null、undefined、-1 等无效值
|
||||
if (score === null || score === undefined || score === -1 || score === '-1') {
|
||||
return '--'
|
||||
}
|
||||
// 如果是字符串类型的数字,直接返回
|
||||
if (typeof score === 'string' && !isNaN(parseFloat(score))) {
|
||||
return score
|
||||
}
|
||||
// 如果是数字类型,保留3位小数
|
||||
if (typeof score === 'number') {
|
||||
return score.toFixed(3)
|
||||
}
|
||||
return score
|
||||
},
|
||||
|
||||
async handleRefresh() {
|
||||
if (this.isLoading) return
|
||||
uni.showToast({ title: '刷新中...', icon: 'loading', duration: 1000 })
|
||||
await this.loadPlayers(true)
|
||||
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1000 })
|
||||
},
|
||||
|
||||
async loadPlayers(refresh = false) {
|
||||
if (this.isLoading) return
|
||||
try {
|
||||
this.isLoading = true
|
||||
if (refresh) {
|
||||
this.pagination.current = 1
|
||||
this.hasMore = true
|
||||
}
|
||||
if (refresh && this.isFirstLoad) {
|
||||
uni.showLoading({ title: '加载中...', mask: true })
|
||||
}
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
const params = {
|
||||
matchCode: globalData.matchCode,
|
||||
judgeId: this.judgeId,
|
||||
venueId: this.venueInfo.id,
|
||||
projectId: this.projectInfo.id,
|
||||
current: this.pagination.current,
|
||||
size: this.pagination.size
|
||||
}
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === undefined || params[key] === null || params[key] === '') {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
if (config.debug) {
|
||||
console.log('请求选手列表参数:', params)
|
||||
}
|
||||
|
||||
// 主裁判使用 getAthletesForAdmin 接口
|
||||
const response = await dataAdapter.getData('getAthletesForAdmin', params)
|
||||
|
||||
if (config.debug) {
|
||||
console.log('选手列表响应:', response)
|
||||
}
|
||||
|
||||
if (refresh && this.isFirstLoad) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
const responseData = response.data || {}
|
||||
const records = responseData.records || response.data || []
|
||||
const total = responseData.total || records.length
|
||||
this.pagination.total = total
|
||||
this.totalCount = total
|
||||
if (refresh) {
|
||||
this.players = records
|
||||
} else {
|
||||
this.players = [...this.players, ...records]
|
||||
}
|
||||
// 主裁判视图:统计有总分的选手
|
||||
this.scoredCount = this.players.filter(p => p.totalScore).length
|
||||
this.hasMore = this.players.length < total
|
||||
|
||||
if (config.debug) {
|
||||
console.log('选手列表处理结果:', {
|
||||
total: total,
|
||||
loaded: this.players.length,
|
||||
scored: this.scoredCount,
|
||||
players: this.players
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('加载选手列表失败:', error)
|
||||
uni.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadMore() {
|
||||
if (!this.hasMore || this.isLoading) return
|
||||
this.pagination.current++
|
||||
await this.loadPlayers(false)
|
||||
},
|
||||
|
||||
goToModify(player) {
|
||||
const app = getApp()
|
||||
app.globalData.currentAthlete = player
|
||||
app.globalData.currentProjectId = this.projectInfo.id
|
||||
uni.navigateTo({ url: '/pages/modify-score/modify-score' })
|
||||
},
|
||||
|
||||
updateCurrentProject() {
|
||||
const currentProject = this.projects[this.currentProjectIndex] || {}
|
||||
this.projectInfo = {
|
||||
id: currentProject.projectId,
|
||||
name: currentProject.projectName || '项目'
|
||||
}
|
||||
},
|
||||
|
||||
async switchProject(index) {
|
||||
if (index === this.currentProjectIndex) return
|
||||
this.currentProjectIndex = index
|
||||
const app = getApp()
|
||||
app.globalData.currentProjectIndex = index
|
||||
this.updateCurrentProject()
|
||||
await this.loadPlayers(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* ==================== 导航栏 ==================== */
|
||||
.nav-bar {
|
||||
height: 90rpx;
|
||||
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.nav-dots, .nav-circle {
|
||||
font-size: 32rpx;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* ==================== 比赛信息 ==================== */
|
||||
.match-info {
|
||||
padding: 30rpx;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.match-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1B7C5E;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.match-time {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* ==================== 场地卡片 ==================== */
|
||||
.venue-card {
|
||||
margin: 0 30rpx 20rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.venue-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 4rpx solid #1B7C5E;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.venue-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.refresh-link {
|
||||
font-size: 26rpx;
|
||||
color: #4A90D9;
|
||||
}
|
||||
|
||||
.project-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.project-chip {
|
||||
padding: 20rpx 12rpx;
|
||||
border: 2rpx solid #1B7C5E;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #1B7C5E;
|
||||
background-color: #FFFFFF;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-chip.active {
|
||||
background-color: #1B7C5E;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* ==================== 评分统计 ==================== */
|
||||
.score-stats {
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 32rpx;
|
||||
color: #1B7C5E;
|
||||
font-weight: 600;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
/* ==================== 选手卡片 ==================== */
|
||||
.player-list {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.player-card {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* ==================== 操作区域 ==================== */
|
||||
.action-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.score-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 20rpx;
|
||||
background-color: #F5F5F5;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid #E5E5E5;
|
||||
}
|
||||
|
||||
.tag-label {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.tag-value {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modify-btn {
|
||||
padding: 12rpx 40rpx;
|
||||
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ==================== 选手详情 ==================== */
|
||||
.player-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* ==================== 加载状态 ==================== */
|
||||
.loading-status {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 26rpx;
|
||||
color: #1B7C5E;
|
||||
}
|
||||
|
||||
.no-more-text {
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
/* ==================== 空状态 ==================== */
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
698
src/pages/score-list/score-list.vue
Normal file
698
src/pages/score-list/score-list.vue
Normal file
@@ -0,0 +1,698 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-title">评分系统</view>
|
||||
<view class="nav-right">
|
||||
<view class="logout-btn" @click="handleLogout">退出</view>
|
||||
<view class="nav-dots">···</view>
|
||||
<view class="nav-circle">◎</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 比赛信息 -->
|
||||
<view class="match-info">
|
||||
<view class="match-title">{{ matchInfo.name }}</view>
|
||||
<view class="match-time">比赛时间:{{ formatDateTime(matchInfo.time) }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 场地和项目卡片 -->
|
||||
<view class="venue-card">
|
||||
<!-- 场地标题行 -->
|
||||
<view class="venue-header">
|
||||
<view class="venue-name">{{ venueInfo.name }}</view>
|
||||
<view class="refresh-link" @click="handleRefresh">刷新</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目筛选 - 横向滑动 -->
|
||||
<scroll-view class="project-scroll" scroll-x="true" :show-scrollbar="false">
|
||||
<view class="project-row">
|
||||
<view
|
||||
class="project-chip"
|
||||
:class="{ active: index === currentProjectIndex }"
|
||||
v-for="(project, index) in projects"
|
||||
:key="project.projectId"
|
||||
@click="switchProject(index)"
|
||||
>
|
||||
{{ project.projectName }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="no-project-tip" v-if="projects.length === 0">当前场地暂无比赛项目</view>
|
||||
</view>
|
||||
|
||||
<!-- 评分统计 -->
|
||||
<view class="score-stats">
|
||||
<text class="stats-label">已评分:</text>
|
||||
<text class="stats-value">{{ scoredCount }}/{{ totalCount }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 选手列表 -->
|
||||
<view class="player-list" v-if="projects.length > 0">
|
||||
<!-- 选手卡片 -->
|
||||
<view
|
||||
class="player-card"
|
||||
v-for="player in players"
|
||||
:key="player.athleteId"
|
||||
>
|
||||
<!-- 已评分状态 -->
|
||||
<template v-if="player.scored">
|
||||
<view class="card-header">
|
||||
<view class="player-name">{{ player.name }}</view>
|
||||
<view class="score-tags">
|
||||
<view class="score-tag">
|
||||
<text class="tag-label">我的评分:</text>
|
||||
<text class="tag-value">{{ player.myScore }}</text>
|
||||
</view>
|
||||
<!-- 总分:只有所有裁判都评分完成后才显示 -->
|
||||
<view class="score-tag" v-if="player.scoringComplete">
|
||||
<text class="tag-label">总分:</text>
|
||||
<text class="tag-value">{{ formatScore(player.totalScore) }}</text>
|
||||
</view>
|
||||
<view class="score-tag waiting" v-else>
|
||||
<text class="tag-label">总分:</text>
|
||||
<text class="tag-value">评分中({{ player.scoredJudgeCount || 0 }}/{{ player.requiredJudgeCount || "?" }})</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 未评分状态 -->
|
||||
<template v-else>
|
||||
<view class="card-header">
|
||||
<view class="player-name">{{ player.name }}</view>
|
||||
<view class="action-row">
|
||||
<button class="score-btn" @click="goToScoreDetail(player)">评分</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 选手详细信息 -->
|
||||
<view class="player-details">
|
||||
<view class="detail-row">
|
||||
<text class="detail-text">身份证:{{ player.idCard }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-text">队伍:{{ player.team }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-text">编号:{{ player.number }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-status" v-if="players.length > 0">
|
||||
<view v-if="isLoading" class="loading-text">加载中...</view>
|
||||
<view v-else-if="!hasMore" class="no-more-text">— 没有更多了 —</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!isLoading && players.length === 0">
|
||||
<text class="empty-text">暂无选手数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dataAdapter from '@/utils/dataAdapter.js'
|
||||
import config from '@/config/env.config.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
matchInfo: {
|
||||
name: '',
|
||||
time: ''
|
||||
},
|
||||
venueInfo: {
|
||||
id: '',
|
||||
name: ''
|
||||
},
|
||||
projectInfo: {
|
||||
id: '',
|
||||
name: ''
|
||||
},
|
||||
judgeId: '',
|
||||
projects: [],
|
||||
currentProjectIndex: 0,
|
||||
players: [],
|
||||
scoredCount: 0,
|
||||
totalCount: 0,
|
||||
pagination: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
},
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
isFirstLoad: true
|
||||
}
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
|
||||
if (!globalData.judgeId || !globalData.token) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
this.matchInfo = {
|
||||
name: globalData.matchName || '比赛名称',
|
||||
time: globalData.matchTime || ''
|
||||
}
|
||||
|
||||
this.venueInfo = {
|
||||
id: globalData.venueId,
|
||||
name: globalData.venueName || '第一场地'
|
||||
}
|
||||
|
||||
this.projects = globalData.projects || []
|
||||
this.currentProjectIndex = globalData.currentProjectIndex || 0
|
||||
this.updateCurrentProject()
|
||||
this.judgeId = globalData.judgeId
|
||||
|
||||
if (config.debug) {
|
||||
console.log('评分列表页加载:', {
|
||||
judgeId: this.judgeId,
|
||||
venueId: this.venueInfo.id,
|
||||
projectId: this.projectInfo.id
|
||||
})
|
||||
}
|
||||
|
||||
await this.loadPlayers(true)
|
||||
this.isFirstLoad = false
|
||||
},
|
||||
|
||||
async onShow() {
|
||||
if (!this.isFirstLoad) {
|
||||
if (config.debug) {
|
||||
console.log('页面显示,刷新数据')
|
||||
}
|
||||
await this.loadPlayers(true)
|
||||
}
|
||||
},
|
||||
|
||||
async onPullDownRefresh() {
|
||||
await this.loadPlayers(true)
|
||||
uni.stopPullDownRefresh()
|
||||
},
|
||||
|
||||
async onReachBottom() {
|
||||
if (this.hasMore && !this.isLoading) {
|
||||
await this.loadMore()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return ''
|
||||
try {
|
||||
const date = new Date(dateTimeStr)
|
||||
if (isNaN(date.getTime())) return dateTimeStr
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hours = date.getHours()
|
||||
const minutes = date.getMinutes()
|
||||
const paddedMinutes = minutes < 10 ? '0' + minutes : minutes
|
||||
return year + '年' + month + '月' + day + '日 ' + hours + ':' + paddedMinutes
|
||||
} catch (error) {
|
||||
return dateTimeStr
|
||||
}
|
||||
},
|
||||
|
||||
formatScore(score) {
|
||||
if (score === null || score === undefined || score === -1 || score === '-1') {
|
||||
return '--'
|
||||
}
|
||||
if (typeof score === 'string' && !isNaN(parseFloat(score))) {
|
||||
return score
|
||||
}
|
||||
if (typeof score === 'number') {
|
||||
return score.toFixed(3)
|
||||
}
|
||||
return score
|
||||
},
|
||||
|
||||
calculateTotalScore(player) {
|
||||
if (!player.judgeScores || !Array.isArray(player.judgeScores)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const totalJudges = player.totalJudges || 0
|
||||
const scoredCount = player.judgeScores.length
|
||||
|
||||
if (totalJudges === 0 || scoredCount < totalJudges) {
|
||||
return null
|
||||
}
|
||||
|
||||
const scores = player.judgeScores.map(j => parseFloat(j.score)).filter(s => !isNaN(s))
|
||||
|
||||
if (scores.length < 3) {
|
||||
if (scores.length === 0) return null
|
||||
const sum = scores.reduce((a, b) => a + b, 0)
|
||||
return sum / scores.length
|
||||
}
|
||||
|
||||
scores.sort((a, b) => a - b)
|
||||
const trimmedScores = scores.slice(1, -1)
|
||||
const sum = trimmedScores.reduce((a, b) => a + b, 0)
|
||||
const average = sum / trimmedScores.length
|
||||
|
||||
return average
|
||||
},
|
||||
|
||||
isAllJudgesScored(player) {
|
||||
if (!player.judgeScores || !Array.isArray(player.judgeScores)) {
|
||||
return false
|
||||
}
|
||||
const totalJudges = player.totalJudges || 0
|
||||
return totalJudges > 0 && player.judgeScores.length >= totalJudges
|
||||
},
|
||||
|
||||
getDisplayTotalScore(player) {
|
||||
const score = this.calculateTotalScore(player)
|
||||
if (score === null) {
|
||||
return '--'
|
||||
}
|
||||
return score.toFixed(3)
|
||||
},
|
||||
|
||||
getJudgeProgress(player) {
|
||||
const scored = player.judgeScores ? player.judgeScores.length : 0
|
||||
const total = player.totalJudges || '?'
|
||||
return scored + '/' + total
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.removeStorageSync("judgeInfo")
|
||||
uni.removeStorageSync("token")
|
||||
uni.reLaunch({ url: "/pages/login/login" })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async handleRefresh() {
|
||||
if (this.isLoading) return
|
||||
uni.showToast({ title: '刷新中...', icon: 'loading', duration: 1000 })
|
||||
await this.loadPlayers(true)
|
||||
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1000 })
|
||||
},
|
||||
|
||||
async loadPlayers(refresh = false) {
|
||||
if (this.isLoading) return
|
||||
try {
|
||||
this.isLoading = true
|
||||
if (refresh) {
|
||||
this.pagination.current = 1
|
||||
this.hasMore = true
|
||||
}
|
||||
if (refresh && this.isFirstLoad) {
|
||||
uni.showLoading({ title: '加载中...', mask: true })
|
||||
}
|
||||
const app = getApp()
|
||||
const globalData = app.globalData || {}
|
||||
const params = {
|
||||
matchCode: globalData.matchCode,
|
||||
competitionId: globalData.matchId,
|
||||
judgeId: this.judgeId,
|
||||
venueId: this.venueInfo.id,
|
||||
projectId: this.projectInfo.id,
|
||||
current: this.pagination.current,
|
||||
size: this.pagination.size
|
||||
}
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === undefined || params[key] === null || params[key] === '') {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
if (config.debug) {
|
||||
console.log('请求选手列表参数:', params)
|
||||
}
|
||||
|
||||
const response = await dataAdapter.getData('getMyAthletes', params)
|
||||
|
||||
if (config.debug) {
|
||||
console.log('选手列表响应:', response)
|
||||
}
|
||||
|
||||
if (refresh && this.isFirstLoad) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
const responseData = response.data || {}
|
||||
const records = responseData.records || response.data || []
|
||||
const total = responseData.total || records.length
|
||||
this.pagination.total = total
|
||||
this.totalCount = total
|
||||
if (refresh) {
|
||||
this.players = records
|
||||
} else {
|
||||
this.players = [...this.players, ...records]
|
||||
}
|
||||
this.scoredCount = this.players.filter(p => p.scored).length
|
||||
this.hasMore = this.players.length < total
|
||||
|
||||
if (config.debug) {
|
||||
console.log('选手列表处理结果:', {
|
||||
total: total,
|
||||
loaded: this.players.length,
|
||||
scored: this.scoredCount,
|
||||
players: this.players
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('加载选手列表失败:', error)
|
||||
uni.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadMore() {
|
||||
if (!this.hasMore || this.isLoading) return
|
||||
this.pagination.current++
|
||||
await this.loadPlayers(false)
|
||||
},
|
||||
|
||||
goToScoreDetail(player) {
|
||||
const app = getApp()
|
||||
app.globalData.currentAthlete = player
|
||||
app.globalData.currentProjectId = this.projectInfo.id
|
||||
uni.navigateTo({ url: '/pages/score-detail/score-detail' })
|
||||
},
|
||||
|
||||
updateCurrentProject() {
|
||||
const currentProject = this.projects[this.currentProjectIndex] || {}
|
||||
this.projectInfo = {
|
||||
id: currentProject.projectId,
|
||||
name: currentProject.projectName || '项目'
|
||||
}
|
||||
},
|
||||
|
||||
async switchProject(index) {
|
||||
if (index === this.currentProjectIndex) return
|
||||
this.currentProjectIndex = index
|
||||
const app = getApp()
|
||||
app.globalData.currentProjectIndex = index
|
||||
this.updateCurrentProject()
|
||||
await this.loadPlayers(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* ==================== 导航栏 ==================== */
|
||||
.nav-bar {
|
||||
height: 90rpx;
|
||||
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
font-size: 26rpx;
|
||||
color: #FFFFFF;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.nav-dots, .nav-circle {
|
||||
font-size: 32rpx;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* ==================== 比赛信息 ==================== */
|
||||
.match-info {
|
||||
padding: 30rpx;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.match-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1B7C5E;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.match-time {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* ==================== 场地卡片 ==================== */
|
||||
.venue-card {
|
||||
margin: 0 30rpx 20rpx;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.venue-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 4rpx solid #1B7C5E;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.venue-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.refresh-link {
|
||||
font-size: 26rpx;
|
||||
color: #4A90D9;
|
||||
}
|
||||
|
||||
/* ==================== 项目筛选 - 横向滑动 ==================== */
|
||||
.project-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-row {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
.project-chip {
|
||||
display: inline-block;
|
||||
padding: 20rpx 32rpx;
|
||||
border: 2rpx solid #1B7C5E;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #1B7C5E;
|
||||
background-color: #FFFFFF;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-chip.active {
|
||||
background-color: #1B7C5E;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* ==================== 评分统计 ==================== */
|
||||
.score-stats {
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 32rpx;
|
||||
color: #1B7C5E;
|
||||
font-weight: 600;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
/* ==================== 选手卡片 ==================== */
|
||||
.player-list {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.player-card {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* ==================== 已评分标签 ==================== */
|
||||
.score-tags {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.score-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 20rpx;
|
||||
background-color: #F5F5F5;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid #E5E5E5;
|
||||
}
|
||||
|
||||
.tag-label {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.tag-value {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ==================== 等待中状态 ==================== */
|
||||
.score-tag.waiting {
|
||||
background-color: #FFF7E6;
|
||||
border-color: #FFD591;
|
||||
}
|
||||
|
||||
.score-tag.waiting .tag-value {
|
||||
color: #FA8C16;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* ==================== 未评分操作 ==================== */
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.score-btn {
|
||||
padding: 12rpx 40rpx;
|
||||
background: linear-gradient(135deg, #1B7C5E 0%, #2A9D7E 100%);
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ==================== 选手详情 ==================== */
|
||||
.player-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* ==================== 加载状态 ==================== */
|
||||
.loading-status {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 26rpx;
|
||||
color: #1B7C5E;
|
||||
}
|
||||
|
||||
.no-more-text {
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
/* ==================== 空状态 ==================== */
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.no-project-tip {
|
||||
padding: 30rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
0
src/static/.gitkeep
Normal file
0
src/static/.gitkeep
Normal file
73
src/uni.scss
Normal file
73
src/uni.scss
Normal 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
257
src/utils/dataAdapter.js
Normal 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
279
src/utils/request.js
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* 网络请求封装
|
||||
* 统一处理HTTP请求、响应、错误、Token等
|
||||
*
|
||||
* 特性:
|
||||
* - 自动添加Token(Blade-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)
|
||||
}
|
||||
|
||||
// 构建完整URL(GET请求需要拼接查询参数)
|
||||
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
223
test-h5.html
Normal 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>
|
||||
@@ -247,7 +247,7 @@ export default new DataAdapter()
|
||||
* |---------------------|----------------------|---------------------|---------------|
|
||||
* | login | mockData.login | apiService.login | 登录验证 |
|
||||
* | getMyAthletes | mockData.getMyAthletes | apiService.getMyAthletes | 选手列表(评委) |
|
||||
* | getAthletesForAdmin | mockData.getAthletesForAdmin | apiService.getAthletesForAdmin | 选手列表(裁判长) |
|
||||
* | getAthletesForAdmin | mockData.getAthletesForAdmin | apiService.getAthletesForAdmin | 选手列表(主裁判) |
|
||||
* | submitScore | mockData.submitScore | apiService.submitScore | 提交评分 |
|
||||
* | getScoreDetail | mockData.getScoreDetail | apiService.getScoreDetail | 评分详情 |
|
||||
* | modifyScore | mockData.modifyScore | apiService.modifyScore | 修改评分 |
|
||||
|
||||
41
vue.config.js
Normal file
41
vue.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
outputDir: 'dist/build/h5',
|
||||
assetsDir: 'static',
|
||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||
productionSourceMap: false,
|
||||
css: {
|
||||
extract: true,
|
||||
sourceMap: false
|
||||
},
|
||||
devServer: {
|
||||
port: 8080,
|
||||
host: '0.0.0.0',
|
||||
open: false,
|
||||
disableHostCheck: true,
|
||||
overlay: {
|
||||
warnings: false,
|
||||
errors: true
|
||||
},
|
||||
proxy: {
|
||||
'/mini': {
|
||||
target: 'http://localhost:8123',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/martial': {
|
||||
target: 'http://localhost:8123',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
chainWebpack: config => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
config.performance.hints(false)
|
||||
}
|
||||
config.module
|
||||
.rule('vue')
|
||||
.use('vue-loader')
|
||||
.tap(options => {
|
||||
return options
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user