From 7ec9a77c2ace977fffdb139e962eda319edb4e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=85=E6=88=BF?= Date: Thu, 11 Dec 2025 14:06:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Mock=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BF=9D=E6=8A=A4=E6=9C=BA=E5=88=B6=20-=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E6=9E=B6=E6=9E=84=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完成内容: ✅ 第一层保护: Git分支隔离 - 创建 v1.0-mock 标签 - 创建 feature/api-integration 分支 ✅ 第二层保护: 配置开关控制 - config/env.config.js (环境配置,支持Mock/API模式切换) ✅ 第三层保护: 代码架构分离 - utils/request.js (网络请求封装,支持Blade-Auth) - utils/dataAdapter.js (核心适配器,自动选择数据源) ✅ Mock数据模块 (4个文件): - mock/index.js (统一入口) - mock/login.js (登录Mock数据) - mock/athlete.js (选手Mock数据,含场地、项目) - mock/score.js (评分Mock数据,含扣分项、详情、修改) ✅ API接口模块 (4个文件): - api/index.js (统一入口) - api/auth.js (认证API,含后端接口规范) - api/athlete.js (选手API,含SQL示例) - api/score.js (评分API,含实现逻辑说明) 特性: - 通过修改 config/env.config.js 的 dataMode 即可切换Mock/API模式 - Mock模式: 完全离线,无需后端,UI功能完整 - API模式: 调用真实后端接口(需后端实现5个专用接口) - 零UI修改: 原有页面代码完全保护,仅替换数据源 下一步: - 修改5个页面使用 dataAdapter - 测试Mock模式功能 - 后端开发5个小程序专用接口 代码统计: - 新增11个文件 - 约1000行代码 - 完整的注释和使用说明 --- .claude/settings.local.json | 5 +- api/athlete.js | 142 ++++++++++++++++++++ api/auth.js | 85 ++++++++++++ api/index.js | 158 ++++++++++++++++++++++ api/score.js | 165 +++++++++++++++++++++++ config/env.config.js | 75 +++++++++++ mock/athlete.js | 162 +++++++++++++++++++++++ mock/index.js | 117 ++++++++++++++++ mock/login.js | 44 ++++++ mock/score.js | 162 +++++++++++++++++++++++ utils/dataAdapter.js | 257 ++++++++++++++++++++++++++++++++++++ utils/request.js | 245 ++++++++++++++++++++++++++++++++++ 12 files changed, 1616 insertions(+), 1 deletion(-) create mode 100644 api/athlete.js create mode 100644 api/auth.js create mode 100644 api/index.js create mode 100644 api/score.js create mode 100644 config/env.config.js create mode 100644 mock/athlete.js create mode 100644 mock/index.js create mode 100644 mock/login.js create mode 100644 mock/score.js create mode 100644 utils/dataAdapter.js create mode 100644 utils/request.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0cfaeac..510fdf9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,10 @@ "allow": [ "Bash(tree:*)", "Bash(find:*)", - "Bash(git add:*)" + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git tag:*)", + "Bash(git checkout:*)" ], "deny": [], "ask": [] diff --git a/api/athlete.js b/api/athlete.js new file mode 100644 index 0000000..fad6e2d --- /dev/null +++ b/api/athlete.js @@ -0,0 +1,142 @@ +/** + * API接口 - 选手模块 + * 真实后端接口调用(需要后端实现) + */ + +import request from '@/utils/request.js' + +/** + * 获取我的选手列表(普通评委) + * @param {Object} params + * @param {String} params.judgeId - 评委ID + * @param {String} params.venueId - 场地ID + * @param {String} params.projectId - 项目ID + * @returns {Promise} + * + * 注意:此接口需要后端实现 + * 建议路径: GET /api/mini/athletes + */ +export function getMyAthletes(params) { + return request({ + url: '/api/mini/athletes', + method: 'GET', + data: params, + showLoading: true + }) +} + +/** + * 获取选手列表(裁判长) + * @param {Object} params + * @param {String} params.competitionId - 比赛ID + * @param {String} params.venueId - 场地ID + * @param {String} params.projectId - 项目ID + * @returns {Promise} + * + * 注意:此接口需要后端实现 + * 建议路径: GET /api/mini/athletes/admin + */ +export function getAthletesForAdmin(params) { + return request({ + url: '/api/mini/athletes/admin', + method: 'GET', + data: params, + showLoading: true + }) +} + +/** + * 获取场地列表 + * @param {Object} params + * @param {String} params.competitionId - 比赛ID + * @returns {Promise} + */ +export function getVenues(params) { + return request({ + url: '/martial/venue/list', + method: 'GET', + data: { + ...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', + data: { + ...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 + */ diff --git a/api/auth.js b/api/auth.js new file mode 100644 index 0000000..851992e --- /dev/null +++ b/api/auth.js @@ -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: '/api/mini/login', + method: 'POST', + data, + showLoading: true, + loadingText: '登录中...' + }) +} + +/** + * 退出登录 + * @returns {Promise} + */ +export function logout() { + return request({ + url: '/api/mini/logout', + method: 'POST' + }) +} + +/** + * Token验证 + * @returns {Promise} + */ +export function verifyToken() { + return request({ + url: '/api/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": ["女子组长拳", "男子组陈氏太极拳"] + * } + * } + */ diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..6ba4483 --- /dev/null +++ b/api/index.js @@ -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个专用接口 + * } + */ diff --git a/api/score.js b/api/score.js new file mode 100644 index 0000000..e554c35 --- /dev/null +++ b/api/score.js @@ -0,0 +1,165 @@ +/** + * API接口 - 评分模块 + * 真实后端接口调用(需要后端实现) + */ + +import request from '@/utils/request.js' + +/** + * 获取扣分项列表 + * @param {Object} params + * @param {String} params.projectId - 项目ID + * @returns {Promise} + */ +export function getDeductions(params) { + return request({ + url: '/martial/deductionItem/list', + method: 'GET', + data: { + ...params, + current: 1, + size: 100 + } + }) +} + +/** + * 提交评分 + * @param {Object} data + * @param {String} data.athleteId - 选手ID + * @param {String} data.judgeId - 评委ID + * @param {Number} data.score - 评分 + * @param {Array} data.deductions - 扣分项 + * @param {String} data.note - 备注 + * @returns {Promise} + */ +export function submitScore(data) { + return request({ + url: '/martial/score/submit', + method: 'POST', + data, + showLoading: true, + loadingText: '提交中...' + }) +} + +/** + * 获取评分详情(裁判长查看) + * @param {Object} params + * @param {String} params.athleteId - 选手ID + * @returns {Promise} + * + * 注意:此接口需要后端实现 + * 建议路径: GET /api/mini/score/detail/{athleteId} + */ +export function getScoreDetail(params) { + return request({ + url: `/api/mini/score/detail/${params.athleteId}`, + method: 'GET', + showLoading: true + }) +} + +/** + * 修改评分(裁判长) + * @param {Object} data + * @param {String} data.athleteId - 选手ID + * @param {String} data.modifierId - 修改人ID + * @param {Number} data.modifiedScore - 修改后的分数 + * @param {String} data.note - 修改原因 + * @returns {Promise} + * + * 注意:此接口需要后端实现 + * 建议路径: PUT /api/mini/score/modify + */ +export function modifyScore(data) { + return request({ + url: '/api/mini/score/modify', + method: 'PUT', + data, + showLoading: true, + loadingText: '修改中...' + }) +} + +export default { + getDeductions, + submitScore, + getScoreDetail, + modifyScore +} + +/** + * 后端接口规范(待实现): + * + * 1. GET /api/mini/score/detail/{athleteId} + * + * 响应: + * { + * "code": 200, + * "success": true, + * "msg": "操作成功", + * "data": { + * "athleteInfo": { + * "athleteId": "1", + * "name": "张三", + * "idCard": "123456789000000000", + * "team": "少林寺武术大学院", + * "number": "123-4567898275", + * "totalScore": 8.907 + * }, + * "judgeScores": [ + * { + * "judgeId": "1", + * "judgeName": "欧阳丽娜", + * "score": 8.907, + * "scoreTime": "2025-06-25 09:15:00", + * "note": "" + * } + * ], + * "modification": null + * } + * } + * + * SQL示例: + * SELECT + * s.judge_id AS judgeId, + * s.judge_name AS judgeName, + * s.score, + * s.score_time AS scoreTime, + * s.note + * FROM martial_score s + * WHERE s.athlete_id = #{athleteId} + * ORDER BY s.score_time ASC + * + * --- + * + * 2. PUT /api/mini/score/modify + * + * 请求: + * { + * "athleteId": "1", + * "modifierId": "789", + * "modifiedScore": 8.910, + * "note": "修改原因" + * } + * + * 响应: + * { + * "code": 200, + * "success": true, + * "msg": "修改成功", + * "data": { + * "athleteId": "1", + * "originalScore": 8.907, + * "modifiedScore": 8.910, + * "modifyTime": "2025-06-25 10:00:00" + * } + * } + * + * 实现逻辑: + * 1. 验证权限(只有裁判长可以修改) + * 2. 保存 originalScore(如果是第一次修改) + * 3. 更新 totalScore + * 4. 记录 modifyReason 和 modifyTime + */ diff --git a/config/env.config.js b/config/env.config.js new file mode 100644 index 0000000..3697f22 --- /dev/null +++ b/config/env.config.js @@ -0,0 +1,75 @@ +/** + * 环境配置文件 + * 控制应用的数据源模式(Mock数据 或 真实API) + * + * 使用说明: + * 1. Mock模式(UI演示、前端独立开发):设置 dataMode: 'mock' + * 2. API模式(真实数据对接):设置 dataMode: 'api' + * 3. 可在代码中动态切换模式 + */ + +const ENV_CONFIG = { + // 开发环境配置 + development: { + // 数据模式: 'mock' | 'api' + // mock - 使用本地Mock数据(保护UI版本) + // api - 调用真实后端接口 + dataMode: 'mock', + + // API基础路径(dataMode为'api'时使用) + apiBaseURL: 'http://localhost:8080', + + // 是否开启调试模式 + debug: true, + + // 请求超时时间(毫秒) + timeout: 30000, + + // 是否模拟网络延迟(仅Mock模式) + mockDelay: 300 + }, + + // 测试环境配置 + test: { + dataMode: 'api', + apiBaseURL: 'http://test-api.yourdomain.com', + debug: true, + timeout: 30000, + mockDelay: 0 + }, + + // 生产环境配置 + production: { + dataMode: 'api', + apiBaseURL: 'https://api.yourdomain.com', + debug: false, + timeout: 30000, + mockDelay: 0 + } +} + +// 获取当前环境(开发/测试/生产) +const env = process.env.NODE_ENV || 'development' + +// 导出当前环境的配置 +export default { + ...ENV_CONFIG[env], + env +} + +/** + * 快速切换数据模式示例: + * + * // 在代码中使用 + * import config from '@/config/env.config.js' + * + * if (config.dataMode === 'mock') { + * console.log('当前使用Mock数据') + * } else { + * console.log('当前使用真实API') + * } + * + * // 查看当前环境 + * console.log('当前环境:', config.env) + * console.log('数据模式:', config.dataMode) + */ diff --git a/mock/athlete.js b/mock/athlete.js new file mode 100644 index 0000000..ceffcf4 --- /dev/null +++ b/mock/athlete.js @@ -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} 项目列表 + */ +export function getProjects(params) { + return [ + '女子组长拳', + '男子组陈氏太极拳', + '女子组双剑(含长穗双剑)', + '男子组杨氏太极拳', + '女子组刀术', + '男子组棍术', + '女子组枪术', + '男子组剑术' + ] +} + +export default { + getMyAthletes, + getAthletesForAdmin, + getVenues, + getProjects +} diff --git a/mock/index.js b/mock/index.js new file mode 100644 index 0000000..8db80bf --- /dev/null +++ b/mock/index.js @@ -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: '表现优秀' + * }) + */ diff --git a/mock/login.js b/mock/login.js new file mode 100644 index 0000000..2c6ba78 --- /dev/null +++ b/mock/login.js @@ -0,0 +1,44 @@ +/** + * 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' + ? ['女子组长拳', '男子组陈氏太极拳'] + : ['女子组长拳', '男子组陈氏太极拳', '女子组双剑(含长穗双剑)', '男子组杨氏太极拳', '女子组刀术', '男子组棍术', '女子组枪术', '男子组剑术'] + } +} + +export default { + login +} diff --git a/mock/score.js b/mock/score.js new file mode 100644 index 0000000..a472f65 --- /dev/null +++ b/mock/score.js @@ -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 +} diff --git a/utils/dataAdapter.js b/utils/dataAdapter.js new file mode 100644 index 0000000..abe9fe9 --- /dev/null +++ b/utils/dataAdapter.js @@ -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 | 项目列表 | + */ diff --git a/utils/request.js b/utils/request.js new file mode 100644 index 0000000..b1ab70d --- /dev/null +++ b/utils/request.js @@ -0,0 +1,245 @@ +/** + * 网络请求封装 + * 统一处理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 请求数据 + * @param {Object} options.header 自定义请求头 + * @param {Boolean} options.showLoading 是否显示Loading + * @param {String} options.loadingText Loading文本 + * @returns {Promise} + */ +function request(options = {}) { + const { + url = '', + method = 'GET', + data = {}, + header = {}, + showLoading = false, + loadingText = '加载中...' + } = options + + // 显示Loading + if (showLoading) { + uni.showLoading({ + title: loadingText, + mask: true + }) + } + + // 打印调试信息 + if (config.debug) { + console.log(`[API请求] ${method} ${url}`, data) + } + + return new Promise((resolve, reject) => { + uni.request({ + url: config.apiBaseURL + url, + method, + data, + 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, data = {}, options = {}) { + return request({ + url, + method: 'GET', + data, + ...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)) + */