From d17f43135d5627c155bf95ac2e38e71a082ebc40 Mon Sep 17 00:00:00 2001 From: ltx <1006158920@qq.com> Date: Fri, 7 Aug 2026 11:28:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=A4=9A=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E8=B5=9B=E4=BA=8B=E6=9C=8D=E5=8A=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增参赛者、裁判、检录员角色识别及角色化首页与个人中心 - 新增裁判注册、身份认证提示、赛事执裁报名及报名记录查询 - 新增检录工作台,支持检录任务、轮次状态与运动员检录操作 - 新增赛事成绩查询页面及成绩详情展示 - 封装成绩、检录和裁判报名接口,补充请求参数清理逻辑 - 新增检录 Tab 图标、页面路由及本地开发配置 - 新增 Git 忽略规则,排除依赖、构建缓存、日志和本地工具文件 --- .gitignore | 23 ++++ app.js | 4 +- app.json | 4 +- assets/tab_scan.png | Bin 0 -> 959 bytes assets/tab_scan_sel.png | Bin 0 -> 3412 bytes pages/detail/detail.js | 71 ++++++++++- pages/detail/detail.wxml | 4 +- pages/home/home.js | 101 ++++++++++++++- pages/home/home.wxml | 42 +++++-- pages/home/home.wxss | 23 ++++ pages/mine/mine.js | 28 ++++- pages/mine/mine.wxml | 32 ++++- pages/mine/mine.wxss | 31 +++++ project.private.config.json | 11 +- subpackages/mine/auth/auth.js | 8 +- subpackages/mine/auth/auth.wxml | 6 +- subpackages/mine/auth/auth.wxss | 13 ++ subpackages/mine/checkin/checkin.js | 152 ++++++++++++++++++++++ subpackages/mine/checkin/checkin.json | 5 + subpackages/mine/checkin/checkin.wxml | 113 +++++++++++++++++ subpackages/mine/checkin/checkin.wxss | 39 ++++++ subpackages/mine/login/login.wxml | 6 +- subpackages/mine/register/register.js | 19 ++- subpackages/mine/register/register.wxml | 16 ++- subpackages/mine/register/register.wxss | 48 +++++++ subpackages/mine/scores/scores.js | 79 ++++++++++++ subpackages/mine/scores/scores.json | 5 + subpackages/mine/scores/scores.wxml | 74 +++++++++++ subpackages/mine/scores/scores.wxss | 53 ++++++++ utils/auth.js | 8 +- utils/competition.js | 161 ++++++++++++++++++++++++ utils/constants.js | 7 ++ utils/http.js | 33 ++++- utils/role.js | 61 +++++++++ 34 files changed, 1239 insertions(+), 41 deletions(-) create mode 100644 .gitignore create mode 100644 assets/tab_scan.png create mode 100644 assets/tab_scan_sel.png create mode 100644 subpackages/mine/checkin/checkin.js create mode 100644 subpackages/mine/checkin/checkin.json create mode 100644 subpackages/mine/checkin/checkin.wxml create mode 100644 subpackages/mine/checkin/checkin.wxss create mode 100644 subpackages/mine/scores/scores.js create mode 100644 subpackages/mine/scores/scores.json create mode 100644 subpackages/mine/scores/scores.wxml create mode 100644 subpackages/mine/scores/scores.wxss create mode 100644 utils/competition.js create mode 100644 utils/role.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c9d938 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Local project settings +project.private.config.json + +# Dependencies and generated packages +node_modules/ +miniprogram_npm/ + +# Build and tool caches +.codex-*/ +dist/ +build/ +coverage/ + +# Temporary files and logs +.tmp-* +*.log +*.tmp + +# Editor and operating-system files +.idea/ +.vscode/ +.DS_Store +Thumbs.db diff --git a/app.js b/app.js index 6bc9a45..fd55593 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ App({ globalData: { - // baseUrl: 'http://8.153.108.90:9996', - baseUrl: 'https://event.yuedongsports.com', + baseUrl: 'http://127.0.0.1:9997', + // baseUrl: 'https://event.yuedongsports.com', accessToken: '', refreshToken: '', openid: '', diff --git a/app.json b/app.json index 80fe614..3370e46 100644 --- a/app.json +++ b/app.json @@ -46,7 +46,9 @@ "team-detail/team-detail", "order/order", "order-detail/order-detail", - "cancel-registration/cancel-registration" + "cancel-registration/cancel-registration", + "scores/scores", + "checkin/checkin" ] } ], diff --git a/assets/tab_scan.png b/assets/tab_scan.png new file mode 100644 index 0000000000000000000000000000000000000000..6c3f596b0e99e1880cc633227330181e936670ad GIT binary patch literal 959 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 literal 0 HcmV?d00001 diff --git a/pages/detail/detail.js b/pages/detail/detail.js index a6cb0ba..12d2ae8 100644 --- a/pages/detail/detail.js +++ b/pages/detail/detail.js @@ -1,6 +1,8 @@ const { API } = require('../../utils/constants'); const { request } = require('../../utils/http'); const { formatDateTime, eventFormatText } = require('../../utils/format'); +const competition = require('../../utils/competition'); +const { ROLE, getProfileRole } = require('../../utils/role'); /** 接口未返回时的 UI 兜底(与产品稿一致,有真实数据后自动以接口为准) */ var DETAIL_UI_FALLBACK = { @@ -510,6 +512,8 @@ Page({ applyButtonText: '加载中...', /** 当前用户是否已报名该赛事 */ isRegistered: false, + /** 裁判身份进入赛事详情时,报名动作改为报名执裁 */ + isReferee: false, /** 收费模式 0免费 1定价赛事 2单价赛事 3单价项目 */ chargeMode: null }, @@ -517,6 +521,10 @@ Page({ onLoad(options) { console.log('Detail onLoad options:', options); + const loggedIn = !!(wx.getStorageSync('accessToken') || ''); + const role = loggedIn ? getProfileRole(wx.getStorageSync('userProfile') || {}) : ROLE.PARTICIPANT; + this.setData({ isReferee: role === ROLE.REFEREE }); + let rawId = options.id; // 处理扫描普通二维码进入:options.q 为 URL 编码后的链接 @@ -706,7 +714,12 @@ Page({ // 只有 registrationType 为 0 或 2 才允许报名;为 1 时灰态但可点,点击引导去后台 if (canApply) { - if (registrationType === 1) { + if (this.data.isReferee) { + applyDisabled = false; + applyGrey = false; + applyBlockedByAdmin = false; + finalApplyButtonText = '报名执裁'; + } else if (registrationType === 1) { applyDisabled = false; applyGrey = true; applyBlockedByAdmin = true; @@ -766,8 +779,9 @@ Page({ applyDisabled, applyBlockedByAdmin, applyButtonText: finalApplyButtonText, - isRegistered + isRegistered: this.data.isReferee ? false : isRegistered }); + if (this.data.isReferee) this._syncRefereeApplication(id); }) .catch((err) => { // wx.showToast({ title: err.Error, icon: 'none' }); @@ -784,6 +798,10 @@ Page({ onApplyTap() { const id = this.data.id; if (!id) return; + if (this.data.isReferee) { + this.onRefereeApplyTap(); + return; + } if (this.data.isRegistered) { wx.showModal({ title: '提示', @@ -811,6 +829,55 @@ Page({ }); }, + _syncRefereeApplication(eventId) { + return competition.fetchRefereeApplications().then((res) => { + const rows = (res && res.rows) || []; + const applied = rows.some((item) => String(item.eventId) === String(eventId)); + if (applied) { + this.setData({ isRegistered: true, applyDisabled: true, applyGrey: true, applyButtonText: '已报名执裁' }); + } + }).catch(() => {}); + }, + + onRefereeApplyTap() { + if (!wx.getStorageSync('accessToken')) { + wx.navigateTo({ url: '/subpackages/mine/login/login' }); + return; + } + if (this.data.isRegistered) { + wx.showToast({ title: '已报名执裁当前赛事', icon: 'none' }); + return; + } + if (this.data.applyDisabled) { + wx.showToast({ title: toastForEventStatus(this.data.eventStatus), icon: 'none' }); + return; + } + wx.showModal({ + title: '报名执裁', + content: `确认报名执裁“${this.data.eventTitle}”吗?`, + confirmText: '确认报名', + confirmColor: '#1a57b2', + success: (res) => { + if (!res.confirm) return; + wx.showLoading({ title: '提交中...', mask: true }); + competition.applyForReferee({ + eventId: this.data.id, + eventName: this.data.eventTitle, + coverImage: this.data.heroImage, + address: this.data.eventAddress, + eventStatus: this.data.eventStatus, + matchStartText: this.data.matchStartText, + matchEndText: this.data.matchEndText + }).then(() => { + this.setData({ isRegistered: true, applyDisabled: true, applyGrey: true, applyButtonText: '已报名执裁' }); + wx.showToast({ title: '报名执裁成功', icon: 'success' }); + }).catch((err) => { + wx.showToast({ title: (err && err.message) || '报名执裁失败', icon: 'none' }); + }).finally(() => wx.hideLoading()); + } + }); + }, + onShareAppMessage() { const { id, eventTitle, heroImage } = this.data; return { diff --git a/pages/detail/detail.wxml b/pages/detail/detail.wxml index 080a5c6..d9d77e2 100644 --- a/pages/detail/detail.wxml +++ b/pages/detail/detail.wxml @@ -12,8 +12,8 @@ - 报名方式: - {{registrationMethodText || '—'}} + {{isReferee ? '执裁报名' : '报名方式'}}: + {{isReferee ? '裁判在线报名' : (registrationMethodText || '—')}} 收费模式: diff --git a/pages/home/home.js b/pages/home/home.js index 24acc04..2e4286a 100644 --- a/pages/home/home.js +++ b/pages/home/home.js @@ -2,6 +2,8 @@ const { API } = require('../../utils/constants'); const { request } = require('../../utils/http'); const { formatDateTime, eventStatusText, eventStatusForPill, eventFormatText } = require('../../utils/format'); const auth = require('../../utils/auth'); +const competition = require('../../utils/competition'); +const { ROLE, getProfileRole, updateEventTabBar } = require('../../utils/role'); /** 统一后台轮播字段(camelCase / PascalCase、枚举数字)供 wxml 绑定 */ function normalizeBannerItem(raw) { @@ -54,6 +56,11 @@ Page({ data: { sectionTab: 'list', loggedIn: false, + role: ROLE.PARTICIPANT, + isReferee: false, + isChecker: false, + checkerContext: null, + checkerLoading: false, banners: [], rows: [], @@ -73,21 +80,69 @@ Page({ }, onLoad() { - this.fetchBanners(); - this.loadEvents(true); + this._syncRoleContext(); + if (this.data.isChecker) { + this.loadCheckerContext(); + } else { + this.fetchBanners(); + this.loadEvents(true); + } }, onShow() { - const loggedIn = !!(wx.getStorageSync('accessToken') || ''); - this.setData({ loggedIn }); + const roleChanged = this._syncRoleContext(); + if (this.data.isChecker) { + if (!this.data.checkerContext) this.loadCheckerContext(); + return; + } + if (roleChanged) { + this.setData({ sectionTab: 'list', rows: [], pageNo: 1, hasMore: true }); + this.fetchBanners(); + this.loadEvents(true); + return; + } + const loggedIn = this.data.loggedIn; if (this.data.sectionTab === 'registered' && loggedIn) { this.loadMyEvents(true); } }, + _syncRoleContext() { + const loggedIn = !!(wx.getStorageSync('accessToken') || ''); + const profile = loggedIn ? (wx.getStorageSync('userProfile') || {}) : {}; + const role = loggedIn ? getProfileRole(profile) : ROLE.PARTICIPANT; + const changed = role !== this.data.role; + this.setData({ + loggedIn, + role, + isReferee: role === ROLE.REFEREE, + isChecker: role === ROLE.CHECKER + }); + updateEventTabBar(role); + wx.setNavigationBarTitle({ title: role === ROLE.CHECKER ? '检录' : '赛事' }); + return changed; + }, + + loadCheckerContext() { + if (this.data.checkerLoading) return Promise.resolve(); + this.setData({ checkerLoading: true }); + return competition.fetchCurrentCheckin() + .then((context) => this.setData({ checkerContext: context || null })) + .catch((err) => wx.showToast({ title: (err && err.message) || '检录信息加载失败', icon: 'none' })) + .finally(() => this.setData({ checkerLoading: false })); + }, + + goToCheckin() { + wx.navigateTo({ url: '/subpackages/mine/checkin/checkin' }); + }, + /** 页面 disableScroll 后,下拉刷新由 scroll-view 的 refresher 承担 */ onRefresherRefresh() { this.setData({ refresherTriggered: true }); + if (this.data.isChecker) { + this.loadCheckerContext().finally(() => this.setData({ refresherTriggered: false })); + return; + } this.fetchBanners(); const p = this.data.sectionTab === 'list' @@ -99,6 +154,10 @@ Page({ }, onPullDownRefresh() { + if (this.data.isChecker) { + this.loadCheckerContext().finally(() => wx.stopPullDownRefresh()); + return; + } this.fetchBanners(); const p = this.data.sectionTab === 'list' @@ -448,6 +507,8 @@ Page({ if (this.data.loading) return Promise.resolve(); + if (this.data.isReferee) return this.loadMyRefereeEvents(reset); + if (reset) { this.setData({ rows: [], @@ -552,5 +613,37 @@ Page({ loadingText: '加载失败' }); }); + }, + + loadMyRefereeEvents(reset) { + this.setData({ loading: true, isFirstLoad: !!reset }); + return competition.fetchRefereeApplications() + .then((res) => { + const rawRows = (res && Array.isArray(res.rows)) ? res.rows : []; + const rows = rawRows.map((item) => ({ + id: item.eventId, + rowKey: item.id || `referee-${item.eventId}`, + eventName: item.eventName, + coverImage: item.coverImage || '', + address: item.address || '', + eventStatus: item.eventStatus, + eventStatusText: item.applicationStatusText || '已报名执裁', + registrationTimeText: formatDateTime(item.appliedAt), + matchStartText: item.matchStartText || '', + matchEndText: item.matchEndText || '', + isRefereeApplication: true + })); + this.setData({ + rows, + hasMore: false, + loading: false, + isFirstLoad: false, + loadingText: '没有更多了' + }); + }) + .catch((err) => { + this.setData({ rows: [], hasMore: false, loading: false, isFirstLoad: false, loadingText: '加载失败' }); + wx.showToast({ title: (err && err.message) || '执裁报名加载失败', icon: 'none' }); + }); } }); diff --git a/pages/home/home.wxml b/pages/home/home.wxml index 64df67d..99af220 100644 --- a/pages/home/home.wxml +++ b/pages/home/home.wxml @@ -1,5 +1,29 @@ - + + + + 检录任务 + {{checkerContext.venue || '等待分配场地'}} + + {{checkerContext.eventName || '当前暂无检录任务'}} + 检录员工作台 + + + + 当前检次{{checkerContext.currentCheckinName || checkerContext.roundName}} · 共 {{checkerContext.totalCheckins || checkerContext.totalRounds}} 检 + 进行中 + + + 检录范围 + 全部运动员 + 后一检仅接收已通过前一检的运动员 + + + + 正在加载检录任务... + 当前暂无检录任务,下拉可刷新 + + - 赛事报名 + {{isReferee ? '报名执裁' : '赛事报名'}} - 已报赛事 + {{isReferee ? '已报名执裁' : '已报赛事'}} @@ -43,7 +67,7 @@ @@ -86,7 +110,8 @@ 比赛时间:{{item.matchStartText}}~{{item.matchEndText}} - 报名时间:{{item.registrationTimeText}} + {{isReferee ? '报名执裁时间' : '报名时间'}}:{{item.registrationTimeText}} + 比赛时间:{{item.matchStartText}}~{{item.matchEndText}} 已报项目:{{item.projectCount}} 项 @@ -95,7 +120,10 @@ - + + 已报名执裁 + + 上传素材 @@ -131,4 +159,4 @@ 没有更多了 - \ No newline at end of file + diff --git a/pages/home/home.wxss b/pages/home/home.wxss index 9dbcf38..a5f676e 100644 --- a/pages/home/home.wxss +++ b/pages/home/home.wxss @@ -18,6 +18,27 @@ page { /* padding-bottom: calc( env(safe-area-inset-bottom)); */ } +.checker-home { padding-top: 6rpx; } +.checker-hero { padding: 38rpx 34rpx 72rpx; border-radius: 22rpx; color: #fff; background: linear-gradient(145deg,#16498f 0%,#2774c8 100%); box-shadow: 0 10rpx 30rpx rgba(26,87,178,.2); } +.checker-hero-top { display: flex; align-items: center; justify-content: space-between; } +.checker-live { padding: 7rpx 16rpx; border-radius: 999rpx; display: flex; align-items: center; background: rgba(255,255,255,.15); font-size: 21rpx; } +.checker-live-dot { width: 12rpx; height: 12rpx; margin-right: 10rpx; border-radius: 50%; background: #58e4a9; } +.checker-venue { color: rgba(255,255,255,.7); font-size: 21rpx; } +.checker-event { display: block; margin-top: 28rpx; font-size: 34rpx; font-weight: 700; line-height: 1.45; } +.checker-welcome { display: block; margin-top: 10rpx; color: rgba(255,255,255,.7); font-size: 23rpx; } +.checker-task-card { margin: -40rpx 18rpx 0; padding: 30rpx; position: relative; border-radius: 22rpx; background: #fff; box-shadow: 0 8rpx 28rpx rgba(30,52,84,.1); } +.checker-task-head { display: flex; align-items: flex-start; justify-content: space-between; } +.checker-task-head > view:first-child { display: flex; flex-direction: column; } +.checker-task-label { color: #8e97a5; font-size: 22rpx; } +.checker-task-round { margin-top: 9rpx; color: #1a57b2; font-size: 36rpx; font-weight: 800; } +.checker-task-status { padding: 7rpx 16rpx; border-radius: 999rpx; color: #00875a; background: #e7f8f1; font-size: 21rpx; } +.checker-group-box { margin-top: 26rpx; padding: 24rpx; border-radius: 16rpx; display: flex; flex-direction: column; background: #f6f8fb; } +.checker-group-name { margin-top: 11rpx; color: #293344; font-size: 29rpx; font-weight: 700; } +.checker-group-order { margin-top: 7rpx; color: #919aa8; font-size: 21rpx; } +.checker-enter-btn { margin-top: 28rpx; height: 84rpx; line-height: 84rpx; border: 0; border-radius: 16rpx; color: #fff; background: #1a57b2; font-size: 29rpx; font-weight: 600; } +.checker-enter-btn::after { border: 0; } +.checker-empty { margin: 30rpx 18rpx; padding: 70rpx 20rpx; border-radius: 20rpx; text-align: center; color: #969eaa; background: #fff; font-size: 25rpx; } + .banner { width: 100%; height: 300rpx; @@ -339,6 +360,8 @@ page { font-weight: 700; } +.action-btn--referee { border-color: #b7d3f8; color: #1a57b2; background: #edf4ff; font-weight: 600; } + .title-row { display: flex; flex-wrap: wrap; diff --git a/pages/mine/mine.js b/pages/mine/mine.js index 94cbbf6..bbc783a 100644 --- a/pages/mine/mine.js +++ b/pages/mine/mine.js @@ -1,4 +1,5 @@ const auth = require('../../utils/auth'); +const { ROLE, ROLE_META, getProfileRole, updateEventTabBar } = require('../../utils/role'); function normalizeAuthStatus(v) { if (v == null || v === '') return 0; @@ -29,12 +30,18 @@ Page({ profile: {}, maskedPhone: '', authStatus: 0, - authStatusText: '未认证' + authStatusText: '未认证', + role: ROLE.PARTICIPANT, + roleLabel: ROLE_META.participant.label, + isParticipant: true, + isReferee: false, + isChecker: false }, onShow() { const accessToken = wx.getStorageSync('accessToken') || ''; if (!accessToken) { + updateEventTabBar(ROLE.PARTICIPANT); this.setData({ loggedIn: false, profile: {}, maskedPhone: '', authStatus: 0, authStatusText: '未认证' }); return; } @@ -53,11 +60,19 @@ Page({ const p = profile || {}; const st = normalizeAuthStatus(p.authStatus != null ? p.authStatus : p.AuthStatus); const phone = p.mobile || p.phone || ''; + const role = getProfileRole(p); + const roleMeta = ROLE_META[role] || ROLE_META.participant; + updateEventTabBar(role); this.setData({ profile: p, maskedPhone: maskPhone(phone), authStatusText: authStatusText(st), - authStatus: st + authStatus: st, + role, + roleLabel: roleMeta.label, + isParticipant: role === ROLE.PARTICIPANT, + isReferee: role === ROLE.REFEREE, + isChecker: role === ROLE.CHECKER }); }) .catch(() => {}); @@ -91,6 +106,14 @@ Page({ wx.navigateTo({ url: '/subpackages/mine/team/team' }); }, + goToScores() { + wx.navigateTo({ url: '/subpackages/mine/scores/scores' }); + }, + + goToCheckin() { + wx.navigateTo({ url: '/subpackages/mine/checkin/checkin' }); + }, + onLogout() { wx.showModal({ title: '提示', @@ -100,6 +123,7 @@ Page({ success: (res) => { if (!res.confirm) return; wx.clearStorageSync(); + updateEventTabBar(ROLE.PARTICIPANT); this.setData({ loggedIn: false, profile: {}, maskedPhone: '', authStatus: 0, authStatusText: '未认证' }); } }); diff --git a/pages/mine/mine.wxml b/pages/mine/mine.wxml index c9bc5e3..6ca1c84 100644 --- a/pages/mine/mine.wxml +++ b/pages/mine/mine.wxml @@ -22,11 +22,12 @@ {{maskedPhone}} + {{roleLabel}} - + {{profile.points || 0}} @@ -43,10 +44,10 @@ - + - 身份认证 - 点击进行身份认证 + {{isReferee ? '裁判身份认证' : '身份认证'}} + {{isReferee ? '完成认证后可参与赛事执裁' : '点击进行身份认证'}} @@ -55,6 +56,26 @@ + + + + + 成绩查询 + › + + + + + + + + + 检录工作台 + 查看当前检录轮次与组别 + + › + + 我的订单 @@ -70,8 +91,9 @@ 我的团队 › + - \ No newline at end of file + diff --git a/pages/mine/mine.wxss b/pages/mine/mine.wxss index 67e9038..aac742d 100644 --- a/pages/mine/mine.wxss +++ b/pages/mine/mine.wxss @@ -142,6 +142,17 @@ letter-spacing: 2rpx; } +.role-pill { + margin-top: 12rpx; + padding: 7rpx 22rpx; + color: #1a57b2; + background: #edf4ff; + border: 1rpx solid #c8dcff; + border-radius: 999rpx; + font-size: 22rpx; + font-weight: 600; +} + /* ====== 内容区 ====== */ .content { padding: 0 28rpx 60rpx; @@ -280,6 +291,26 @@ border-bottom: none; } +.func-line-icon { position: relative; width: 42rpx; height: 42rpx; margin: 0 4rpx; box-sizing: border-box; flex-shrink: 0; } +.score-line-icon { border: 2rpx solid #252d35; border-radius: 15rpx; }.score-line-bars { position: absolute; left: 9rpx; right: 9rpx; bottom: 6rpx; height: 24rpx; display: flex; align-items: flex-end; justify-content: space-between; border-bottom: 3rpx solid #252d35; box-sizing: border-box; }.score-line-bar { width: 5rpx; border-radius: 4rpx 4rpx 0 0; background: #2162c6; }.score-line-bar.bar-one { height: 10rpx; }.score-line-bar.bar-two { height: 17rpx; }.score-line-bar.bar-three { height: 23rpx; } +.scan-corner { position: absolute; width: 13rpx; height: 13rpx; box-sizing: border-box; }.corner-tl { left: 2rpx; top: 2rpx; border-left: 4rpx solid #252d35; border-top: 4rpx solid #252d35; border-radius: 8rpx 0 0 0; }.corner-tr { right: 2rpx; top: 2rpx; border-right: 4rpx solid #252d35; border-top: 4rpx solid #252d35; border-radius: 0 8rpx 0 0; }.corner-bl { left: 2rpx; bottom: 2rpx; border-left: 4rpx solid #252d35; border-bottom: 4rpx solid #252d35; border-radius: 0 0 0 8rpx; }.corner-br { right: 2rpx; bottom: 2rpx; border-right: 4rpx solid #252d35; border-bottom: 4rpx solid #252d35; border-radius: 0 0 8rpx 0; }.scan-center-line { position: absolute; left: 8rpx; right: 8rpx; top: 23rpx; height: 4rpx; border-radius: 999rpx; background: #2162c6; }.scan-person { position: absolute; left: 18rpx; top: 10rpx; width: 13rpx; height: 27rpx; }.scan-person-head { width: 9rpx; height: 9rpx; margin: 0 auto; border: 3rpx solid #2162c6; border-radius: 50%; box-sizing: border-box; background: #fff; }.scan-person-body { width: 13rpx; height: 9rpx; margin-top: 3rpx; border: 3rpx solid #2162c6; border-bottom: 0; border-radius: 8rpx 8rpx 0 0; box-sizing: border-box; background: #fff; } + +.func-menu-main { + flex: 1; + display: flex; + flex-direction: column; +} + +.func-menu-main .func-menu-text { + flex: none; +} + +.func-menu-sub { + margin: 7rpx 0 0 10rpx; + font-size: 21rpx; + color: #9299a6; +} + .func-menu-icon-wrap { width: 72rpx; height: 72rpx; diff --git a/project.private.config.json b/project.private.config.json index d2b5502..8c72078 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -22,12 +22,19 @@ "miniprogram": { "list": [ { - "name": "subpackages/mine/auth/auth", - "pathName": "subpackages/mine/auth/auth", + "name": "subpackages/mine/checkin/checkin", + "pathName": "subpackages/mine/checkin/checkin", "query": "", "scene": null, "launchMode": "default" }, + { + "name": "subpackages/mine/auth/auth", + "pathName": "subpackages/mine/auth/auth", + "query": "", + "launchMode": "default", + "scene": null + }, { "name": "subpackages/mine/cert/cert", "pathName": "subpackages/mine/cert/cert", diff --git a/subpackages/mine/auth/auth.js b/subpackages/mine/auth/auth.js index a5d507a..85263a0 100644 --- a/subpackages/mine/auth/auth.js +++ b/subpackages/mine/auth/auth.js @@ -1,4 +1,5 @@ const auth = require('../../../utils/auth'); +const { ROLE, getProfileRole } = require('../../../utils/role'); const PHONE_RE = /^1[3456789]\d{9}$/; @@ -85,7 +86,8 @@ Page({ authStatusText: '未认证', canSubmit: true, submitBtnText: '提交认证', - loading: true + loading: true, + isReferee: false }, _timer: null, @@ -138,6 +140,7 @@ Page({ auth.fetchMyProfile().catch(() => ({})) ]).then(([res, profile]) => { const d = res && typeof res === 'object' ? res : {}; + const isReferee = getProfileRole(profile) === ROLE.REFEREE; if(d.birthDate ){ d.birthDate = d.birthDate.split(' ')[0] || d.birthday || ''; } @@ -167,7 +170,8 @@ Page({ authStatus, authStatusText: authStatusTextVal, canSubmit, - submitBtnText + submitBtnText, + isReferee }); }).finally(() => { this.setData({ loading: false }); diff --git a/subpackages/mine/auth/auth.wxml b/subpackages/mine/auth/auth.wxml index 85e5c3e..3a0e7c7 100644 --- a/subpackages/mine/auth/auth.wxml +++ b/subpackages/mine/auth/auth.wxml @@ -1,4 +1,8 @@ + + 裁判身份认证 + 请提交本人有效证件,审核通过后即可获得裁判工作权限 + 身份证认证 @@ -151,4 +155,4 @@ --> - \ No newline at end of file + diff --git a/subpackages/mine/auth/auth.wxss b/subpackages/mine/auth/auth.wxss index aa46f8c..cfe9568 100644 --- a/subpackages/mine/auth/auth.wxss +++ b/subpackages/mine/auth/auth.wxss @@ -2,6 +2,19 @@ page { background: #f5f6f8; } +.referee-auth-tip { + margin: 24rpx 24rpx 0; + padding: 24rpx 28rpx; + border-radius: 18rpx; + background: linear-gradient(135deg, #1a57b2 0%, #3a7bd5 100%); + display: flex; + flex-direction: column; + box-shadow: 0 8rpx 24rpx rgba(26, 87, 178, 0.18); +} + +.referee-auth-title { color: #fff; font-size: 30rpx; font-weight: 700; } +.referee-auth-desc { margin-top: 8rpx; color: rgba(255,255,255,.78); font-size: 22rpx; line-height: 1.5; } + view{ box-sizing: border-box; } diff --git a/subpackages/mine/checkin/checkin.js b/subpackages/mine/checkin/checkin.js new file mode 100644 index 0000000..af1706a --- /dev/null +++ b/subpackages/mine/checkin/checkin.js @@ -0,0 +1,152 @@ +const competition = require('../../../utils/competition'); + +Page({ + data: { + loading: true, + context: null, + projects: [], + projectNames: [], + projectIndex: 0, + groups: [], + groupNames: [], + groupIndex: 0, + units: [], + pendingUnits: [], + checkedUnits: [], + checkedGroups: [], + lockedUnits: [], + checkedCount: 0, + pendingCount: 0, + progressPercent: 0, + updatingKey: '' + }, + + onLoad() { this.loadContext(); }, + + onPullDownRefresh() { + this.loadContext(this.currentProjectId(), this.currentGroupId()).finally(() => wx.stopPullDownRefresh()); + }, + + currentProjectId() { + const item = this.data.projects[this.data.projectIndex]; + return item && item.eventProjectId; + }, + + currentGroupId() { + const item = this.data.groups[this.data.groupIndex]; + return item && item.id; + }, + + loadContext(eventProjectId, groupId) { + this.setData({ loading: true }); + return competition.fetchCurrentCheckin({ eventProjectId, groupId }) + .then((context) => this.applyContext(context)) + .catch((err) => wx.showToast({ title: (err && err.message) || '检录信息加载失败', icon: 'none' })) + .finally(() => this.setData({ loading: false })); + }, + + applyContext(context) { + const projects = (context && context.projects) || []; + let projectIndex = projects.findIndex((item) => String(item.eventProjectId) === String(context.selectedEventProjectId)); + if (projectIndex < 0) projectIndex = 0; + const groups = (projects[projectIndex] && projects[projectIndex].groups) || []; + let groupIndex = groups.findIndex((item) => String(item.id) === String(context.selectedGroupId)); + if (groupIndex < 0) groupIndex = 0; + const units = (context && context.units) || []; + const pendingUnits = (context && context.pendingUnits) || units.filter((item) => item.canCheck && !item.checked); + const checkedUnits = (context && context.checkedUnits) || units.filter((item) => item.checked); + const lockedUnits = (context && context.lockedUnits) || units.filter((item) => !item.canCheck); + const totalRounds = Number(context && (context.totalCheckins || context.totalRounds)) || 0; + const currentRound = Number(context && (context.currentCheckinNo || context.currentRound)) || 0; + const rounds = Array.from({ length: totalRounds }, (_, index) => { + const number = index + 1; + return { number, state: number < currentRound ? 'done' : number === currentRound ? 'active' : '', label: number < currentRound ? '✓' : String(number) }; + }); + const checkedCount = units.filter((item) => item.checked).length; + const checkedGroups = []; + checkedUnits.forEach((item) => { + const key = `${item.eventProjectId || item.projectId}-${item.groupId || item.groupName}`; + let group = checkedGroups.find((entry) => entry.key === key); + if (!group) { + group = { + key, + projectName: item.projectName || '未命名项目', + groupName: item.groupName || '未分组', + units: [] + }; + checkedGroups.push(group); + } + group.units.push(item); + }); + this.setData({ + context: context ? Object.assign({}, context, { rounds }) : null, + projects, + projectNames: projects.map((item) => item.projectName), + projectIndex, + groups, + groupNames: groups.map((item) => item.groupName), + groupIndex, + units, + pendingUnits, + checkedUnits, + checkedGroups, + lockedUnits, + checkedCount, + pendingCount: pendingUnits.length, + progressPercent: units.length ? Math.round(checkedCount * 100 / units.length) : 0 + }); + }, + + onProjectTap(e) { + if (this.data.loading) return; + const projectIndex = Number(e.currentTarget.dataset.index) || 0; + if (projectIndex === this.data.projectIndex) return; + const project = this.data.projects[projectIndex]; + const group = project && project.groups && project.groups[0]; + if (project && group) this.loadContext(project.eventProjectId, group.id); + }, + + onGroupTap(e) { + if (this.data.loading) return; + const groupIndex = Number(e.currentTarget.dataset.index) || 0; + if (groupIndex === this.data.groupIndex) return; + const group = this.data.groups[groupIndex]; + if (group) this.loadContext(this.currentProjectId(), group.id); + }, + + onToggleUnit(e) { + const unitKey = e.currentTarget.dataset.key; + const current = this.data.units.find((item) => item.unitKey === unitKey) + || this.data.checkedUnits.find((item) => item.unitKey === unitKey); + if (!current || !current.canCheck || this.data.updatingKey) return; + const checked = !current.checked; + if (current.checked) { + wx.showModal({ + title: '取消检录', + content: `确认取消${current.teamName || current.athletes.map((item) => item.name).join('、')}的当前检录吗?`, + confirmText: '确认取消', + confirmColor: '#d94b4b', + success: (res) => { + if (res.confirm) this.updateUnitStatus(current, checked); + } + }); + return; + } + this.updateUnitStatus(current, checked); + }, + + updateUnitStatus(current, checked) { + const unitKey = current.unitKey; + this.setData({ updatingKey: unitKey }); + competition.updateCheckinStatus({ + unitKey, + eventProjectId: this.currentProjectId(), + groupId: this.currentGroupId(), + checked + }).then((res) => { + this.applyContext(res); + wx.showToast({ title: checked ? '该列已完成检录' : '已取消该列检录', icon: 'none' }); + }).catch((err) => wx.showToast({ title: (err && err.message) || '操作失败', icon: 'none' })) + .finally(() => this.setData({ updatingKey: '' })); + } +}); diff --git a/subpackages/mine/checkin/checkin.json b/subpackages/mine/checkin/checkin.json new file mode 100644 index 0000000..06d2d54 --- /dev/null +++ b/subpackages/mine/checkin/checkin.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "检录工作台", + "enablePullDownRefresh": true, + "backgroundColor": "#f5f6f8" +} diff --git a/subpackages/mine/checkin/checkin.wxml b/subpackages/mine/checkin/checkin.wxml new file mode 100644 index 0000000..49ffad9 --- /dev/null +++ b/subpackages/mine/checkin/checkin.wxml @@ -0,0 +1,113 @@ + + + + 检录进行中 + {{context.currentCheckinNo}} / {{context.totalCheckins}} 检 + + {{context.eventName}} + + + 当前检次 + {{context.currentCheckinName}} + + + + 当前组别 + {{context.currentGroupName || '请选择组别'}} + + + + + + + 1 + 选择参赛项目左右滑动查看更多项目 + + + + + + {{item.projectName}}已完成 + {{item.checkedUnits}} / {{item.totalUnits}} 项{{item.progressPercent}}% + + + + + + + + + 2 + 选择参赛组别点击后立即切换检录名单 + + + + + + {{item.groupName}}{{item.checkedUnits}}/{{item.totalUnits}} · {{item.isCompleted ? '已完成' : item.progressPercent + '%'}} + + + + + + + + + + 当前组别进度{{checkedCount}} / {{units.length}} 项 + + 已检录 {{checkedCount}} 项待检录 {{pendingCount}} 项 + + + 待检录按秩序册顺序 + + {{item.teamName || '个人参赛'}}{{item.venueName}} + 单位{{item.unitName}} + {{athlete.name}} + 确认检录 + + 当前组别暂无待检录项 + + 等待前序检录前一检通过后可操作 + + {{item.teamName || '个人参赛'}}{{item.venueName}} + 单位{{item.unitName}} + {{athlete.name}} + {{item.lockReason}} + + + + 已检录{{checkedUnits.length}} 项 + 仅展示当前选中项目和组别的已检录结果 + + + + {{checkedGroup.projectName}}{{checkedGroup.groupName}} + {{checkedGroup.units.length}} 项 + + + {{item.teamName || '个人参赛'}}{{item.venueName}} + 单位{{item.unitName}} + {{athlete.name}} + + ✓已完成当前检录 + 取消检录 + + + + 暂无已检录项 + + + diff --git a/subpackages/mine/checkin/checkin.wxss b/subpackages/mine/checkin/checkin.wxss new file mode 100644 index 0000000..1dbb532 --- /dev/null +++ b/subpackages/mine/checkin/checkin.wxss @@ -0,0 +1,39 @@ +page { min-height: 100%; background: #f5f6f8; color: #18202e; } +.checkin-page { min-height: 100vh; padding: 22rpx 22rpx env(safe-area-inset-bottom); box-sizing: border-box; } +.top-card { padding: 24rpx 26rpx; border-radius: 22rpx; color: #fff; background: linear-gradient(145deg,#16498f 0%,#1f67be 72%,#2d7bd0 100%); box-shadow: 0 9rpx 26rpx rgba(23,76,145,.2); } +.top-line { display: flex; justify-content: space-between; align-items: center; } +.live-pill { padding: 7rpx 16rpx; border-radius: 999rpx; display: flex; align-items: center; background: rgba(255,255,255,.15); font-size: 21rpx; } +.live-dot { width: 12rpx; height: 12rpx; margin-right: 10rpx; border-radius: 50%; background: #54e4a7; box-shadow: 0 0 0 5rpx rgba(84,228,167,.15); } +.checkin-sequence { color: rgba(255,255,255,.76); font-size: 21rpx; font-weight: 600; }.event-title { display: block; margin-top: 16rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 29rpx; line-height: 1.4; font-weight: 700; } +.compact-status { margin-top: 19rpx; padding: 17rpx 20rpx; border: 1rpx solid rgba(255,255,255,.13); border-radius: 15rpx; display: flex; align-items: stretch; background: rgba(255,255,255,.1); }.compact-status-item { min-width: 0; display: flex; flex-direction: column; justify-content: center; }.checkin-status-item { width: 160rpx; }.group-status-item { flex: 1; padding-left: 22rpx; }.compact-divider { width: 1rpx; margin: 2rpx 0; background: rgba(255,255,255,.2); }.compact-label { color: rgba(255,255,255,.65); font-size: 18rpx; }.compact-value { margin-top: 7rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 23rpx; font-weight: 650; }.checkin-name { font-size: 27rpx; } +.progress-card { margin-top: 22rpx; padding: 27rpx 28rpx; border-radius: 20rpx; background: #fff; box-shadow: 0 5rpx 20rpx rgba(30,52,84,.06); } +.progress-top,.progress-bottom,.list-heading { display: flex; justify-content: space-between; align-items: center; }.section-title { font-size: 29rpx; font-weight: 700; } +.progress-count { color: #8e97a5; font-size: 24rpx; }.progress-count-main { color: #1a57b2; font-size: 34rpx; font-weight: 800; } +.progress-track { height: 14rpx; margin-top: 23rpx; overflow: hidden; border-radius: 999rpx; background: #e9eef5; }.progress-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg,#1a57b2,#45a0e8); transition: width .25s; } +.progress-bottom { margin-top: 14rpx; color: #8d96a4; font-size: 21rpx; } +.list-heading { margin: 34rpx 6rpx 18rpx; }.list-tip { color: #9aa2ae; font-size: 20rpx; } +.athlete-card { min-height: 108rpx; margin-bottom: 14rpx; padding: 18rpx 20rpx; border-radius: 18rpx; box-sizing: border-box; display: flex; align-items: center; background: #fff; box-shadow: 0 4rpx 18rpx rgba(30,52,84,.055); } +.bib-box { width: 76rpx; height: 62rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #1a57b2; background: #edf4ff; font-size: 23rpx; font-weight: 700; } +.athlete-main { flex: 1; min-width: 0; margin-left: 18rpx; display: flex; flex-direction: column; }.athlete-name { font-size: 28rpx; font-weight: 650; }.team-name { margin-top: 7rpx; color: #969eaa; font-size: 20rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.call-btn { margin-right: 12rpx; padding: 11rpx 14rpx; color: #758198; background: #f4f6f9; border-radius: 10rpx; font-size: 20rpx; } +.status-btn { width: 112rpx; height: 58rpx; border: 2rpx solid #d9dfe8; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #7c8695; background: #fff; font-size: 22rpx; box-sizing: border-box; } +.status-btn.checked { color: #00875a; border-color: #bdebd9; background: #e8f8f2; }.status-btn.disabled { opacity: .55; } +.next-tip { margin-top: 24rpx; padding: 25rpx 26rpx; border-radius: 18rpx; display: flex; align-items: center; background: #edf3fa; }.next-label { padding: 5rpx 12rpx; border-radius: 8rpx; color: #fff; background: #7491b2; font-size: 19rpx; }.next-name { flex: 1; margin-left: 16rpx; color: #4c596b; font-size: 24rpx; }.next-arrow { color: #8090a4; font-size: 38rpx; } +.skeleton { padding: 30rpx; border-radius: 22rpx; background: #fff; }.skeleton view { height: 28rpx; margin-top: 24rpx; border-radius: 8rpx; background: #e9edf2; }.skeleton view:first-child { margin-top: 0; width: 60%; } +.safe-space { height: 28rpx; } +.selector-card { margin-top: 22rpx; padding: 26rpx 0; overflow: hidden; border-radius: 22rpx; background: #fff; box-shadow: 0 5rpx 20rpx rgba(30,52,84,.06); transition: opacity .2s; }.selector-card.selector-loading { opacity: .72; } +.selector-heading { padding: 0 26rpx; display: flex; align-items: center; }.selector-step { width: 38rpx; height: 38rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #fff; background: linear-gradient(145deg,#1a57b2,#438bd8); box-shadow: 0 5rpx 12rpx rgba(26,87,178,.2); font-size: 20rpx; font-weight: 750; }.selector-heading-text { margin-left: 14rpx; display: flex; flex-direction: column; }.selector-title { color: #283448; font-size: 25rpx; font-weight: 700; }.selector-hint { margin-top: 4rpx; color: #9ba4b1; font-size: 18rpx; } +.project-scroll { width: 100%; height: 174rpx; margin-top: 14rpx; white-space: nowrap; }.project-options { height: 174rpx; padding: 9rpx 26rpx 20rpx; box-sizing: border-box; display: inline-flex; align-items: flex-start; gap: 14rpx; }.project-option { position: relative; width: 270rpx; height: 145rpx; padding: 19rpx 20rpx; overflow: hidden; box-sizing: border-box; border: 2rpx solid #e7ebf1; border-radius: 17rpx; display: flex; flex-direction: column; justify-content: center; background: #fafbfc; transition: border-color .18s, background .18s, box-shadow .18s; }.project-option.active { border-color: #4b83cc; background: linear-gradient(145deg,#edf5ff,#f7fbff); box-shadow: 0 6rpx 14rpx rgba(36,103,184,.13); }.project-option.completed { border-color: #8bd1b5; background: linear-gradient(145deg,#ecfaf4,#f7fcfa); }.project-option.completed.active { border-color: #28a574; box-shadow: 0 6rpx 14rpx rgba(34,168,121,.15); }.project-active-mark { position: absolute; left: 0; top: 22rpx; bottom: 22rpx; width: 5rpx; border-radius: 0 6rpx 6rpx 0; background: transparent; }.project-option.active .project-active-mark { background: #2870c8; }.project-option.completed .project-active-mark { background: #25a474; }.project-option-top { display: flex; align-items: center; min-width: 0; }.project-option-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #344156; font-size: 24rpx; font-weight: 650; }.project-option.active .project-option-name { color: #174f9d; }.project-option.completed .project-option-name { color: #176647; }.project-complete-label { margin-left: 8rpx; padding: 3rpx 7rpx; border-radius: 6rpx; color: #08704d; background: #d8f2e7; font-size: 16rpx; }.project-option-summary { margin-top: 13rpx; display: flex; align-items: center; justify-content: space-between; color: #8d98a7; font-size: 18rpx; }.project-option.completed .project-option-summary { color: #55907a; }.project-progress { height: 7rpx; margin-top: 9rpx; overflow: hidden; border-radius: 999rpx; background: #e5eaf0; }.project-progress-value { height: 100%; border-radius: inherit; background: linear-gradient(90deg,#438bd8,#1f67be); }.project-option.completed .project-progress-value { background: linear-gradient(90deg,#48c18f,#22a879); } +.selector-separator { height: 1rpx; margin: 24rpx 26rpx; background: #eef1f5; }.group-heading { margin-top: 0; }.group-scroll { width: 100%; margin-top: 19rpx; white-space: nowrap; }.group-options { padding: 0 26rpx 4rpx; display: inline-flex; gap: 12rpx; }.group-option { min-width: 180rpx; min-height: 76rpx; padding: 10rpx 19rpx; box-sizing: border-box; border: 2rpx solid #e3e7ed; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; color: #687486; background: #fafbfc; transition: color .18s, border-color .18s, background .18s; }.group-active-dot { width: 0; height: 0; margin-right: 0; flex-shrink: 0; border-radius: 50%; background: #fff; transition: width .18s, height .18s, margin .18s; }.group-option-content { min-width: 0; display: flex; flex-direction: column; }.group-option-name { max-width: 220rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 22rpx; font-weight: 650; }.group-option-progress { margin-top: 5rpx; color: #9aa4b2; font-size: 17rpx; }.group-option.active { color: #fff; border-color: #1f67be; background: linear-gradient(100deg,#1a57b2,#347dca); box-shadow: 0 6rpx 14rpx rgba(26,87,178,.18); }.group-option.active .group-option-progress { color: rgba(255,255,255,.75); }.group-option.active .group-active-dot { width: 10rpx; height: 10rpx; margin-right: 10rpx; }.group-option.completed { color: #176647; border-color: #8bd1b5; background: #edf9f4; }.group-option.completed .group-option-progress { color: #55907a; }.group-option.completed.active { color: #fff; border-color: #23996d; background: linear-gradient(100deg,#159368,#2db481); box-shadow: 0 6rpx 14rpx rgba(34,168,121,.18); }.group-option.completed.active .group-option-progress { color: rgba(255,255,255,.78); } +.unit-card { margin-bottom: 16rpx; padding: 24rpx; border-radius: 20rpx; background: #fff; box-shadow: 0 4rpx 18rpx rgba(30,52,84,.055); }.unit-card.locked { opacity: .68; } +.unit-head { display: flex; justify-content: space-between; align-items: center; }.unit-title { flex: 1; min-width: 0; margin-right: 18rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #253247; font-size: 27rpx; font-weight: 700; }.venue-badge { padding: 8rpx 14rpx; border-radius: 999rpx; flex-shrink: 0; color: #925b00; background: #fff3d5; font-size: 21rpx; font-weight: 650; } +.unit-meta { margin-top: 15rpx; display: flex; align-items: center; color: #697587; font-size: 22rpx; }.unit-meta-label { margin-right: 12rpx; padding: 4rpx 10rpx; border-radius: 7rpx; color: #4f6f9f; background: #edf4ff; font-size: 19rpx; }.unit-meta-value { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.unit-athletes { margin-top: 16rpx; display: flex; flex-wrap: wrap; gap: 10rpx; } +.athlete-chip { padding: 9rpx 16rpx; border-radius: 10rpx; color: #34435a; background: #f1f4f8; font-size: 23rpx; }.athlete-chip.checked-chip { color: #08704d; background: #e8f8f2; } +.unit-action { margin-top: 20rpx; height: 64rpx; border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; background: linear-gradient(90deg,#1a57b2,#2d7bd0); font-size: 24rpx; font-weight: 650; }.unit-action.checked { color: #08704d; background: #e8f8f2; }.unit-action.disabled { opacity: .5; } +.empty-tip { padding: 32rpx; border-radius: 18rpx; color: #9aa2ae; background: #fff; text-align: center; font-size: 22rpx; } +.checked-section-head { margin: 34rpx 0 18rpx; padding: 24rpx; border-radius: 20rpx; background: linear-gradient(135deg,#eff9f5,#f7fcfa); border: 1rpx solid #d9eee5; } +.checked-section-title { display: flex; align-items: center; justify-content: space-between; }.checked-count { padding: 5rpx 13rpx; border-radius: 999rpx; color: #08704d; background: #daf2e8; font-size: 20rpx; font-weight: 650; } +.scope-tip { display: block; margin-top: 14rpx; color: #7b9188; font-size: 19rpx; } +.checked-group-head { margin: 24rpx 6rpx 14rpx; display: flex; align-items: center; justify-content: space-between; }.checked-group-names { min-width: 0; display: flex; align-items: center; }.checked-project-name { max-width: 340rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #26364d; font-size: 26rpx; font-weight: 700; }.checked-group-name { margin-left: 12rpx; padding: 5rpx 11rpx; border-radius: 8rpx; color: #765d18; background: #fff2c9; font-size: 19rpx; }.checked-group-count { color: #839088; font-size: 20rpx; } +.checked-actions { margin-top: 20rpx; padding-top: 18rpx; border-top: 1rpx solid #edf1ef; display: flex; align-items: center; justify-content: space-between; }.checked-state { display: flex; align-items: center; color: #08704d; font-size: 22rpx; font-weight: 600; }.checked-mark { width: 34rpx; height: 34rpx; margin-right: 10rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; background: #22a879; font-size: 19rpx; }.cancel-checkin-btn { padding: 10rpx 16rpx; border: 1rpx solid #e7c8c8; border-radius: 10rpx; color: #b54b4b; background: #fff8f8; font-size: 20rpx; }.cancel-checkin-btn.disabled { opacity: .45; } diff --git a/subpackages/mine/login/login.wxml b/subpackages/mine/login/login.wxml index ff60f11..9f4323b 100644 --- a/subpackages/mine/login/login.wxml +++ b/subpackages/mine/login/login.wxml @@ -34,15 +34,15 @@ - 短信登录 + 手机号登录 - + - 账号 + 手机号 0) return; @@ -106,7 +120,7 @@ Page({ onSubmitTap() { if (this.data.submitting) return; - const { isReset, mobile, password, password2, verificationCode, nickName } = this.data; + const { isReset, mobile, password, password2, verificationCode, nickName, selectedRole } = this.data; if (!PHONE_RE.test(mobile)) { wx.showToast({ title: '请输入正确手机号', icon: 'none' }); @@ -130,12 +144,13 @@ Page({ const apiCall = isReset ? auth.resetPassword({ mobile, password, verificationCode }) - : auth.registerWithVerification({ mobile, password, verificationCode, nickName }); + : auth.registerWithVerification({ mobile, password, verificationCode, nickName, role: selectedRole }); const successMsg = isReset ? '密码重置成功' : '注册成功'; apiCall .then(() => { + if (!isReset) wx.setStorageSync('registeredRole', selectedRole); wx.showToast({ title: successMsg, icon: 'success' }); setTimeout(() => { if (isReset) { diff --git a/subpackages/mine/register/register.wxml b/subpackages/mine/register/register.wxml index 08f6dcc..2b5a30b 100644 --- a/subpackages/mine/register/register.wxml +++ b/subpackages/mine/register/register.wxml @@ -17,6 +17,16 @@ {{isReset ? '重置密码' : '账号注册'}} + + 注册角色 + + + {{item.label}} + ✓ + + + {{selectedRoleDescription}} + 手机号 @@ -29,9 +39,9 @@ 验证码 - + @@ -72,4 +82,4 @@ - \ No newline at end of file + diff --git a/subpackages/mine/register/register.wxss b/subpackages/mine/register/register.wxss index faa841d..d1644e0 100644 --- a/subpackages/mine/register/register.wxss +++ b/subpackages/mine/register/register.wxss @@ -120,6 +120,54 @@ color: #1a1a1a; } +.role-field { + margin-top: 34rpx; +} + +.role-switch { + display: flex; + gap: 14rpx; +} + +.role-option { + flex: 1; + height: 76rpx; + border: 2rpx solid #e8edf5; + border-radius: 14rpx; + background: #f8faff; + color: #606a7b; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + position: relative; +} + +.role-option.active { + border-color: #1a57b2; + color: #1a57b2; + background: #edf4ff; + box-shadow: 0 4rpx 16rpx rgba(26, 87, 178, 0.12); +} + +.role-name { + font-size: 26rpx; + font-weight: 600; +} + +.role-check { + margin-left: 8rpx; + font-size: 22rpx; + font-weight: 700; +} + +.role-desc { + display: block; + margin-top: 14rpx; + font-size: 22rpx; + color: #9299a6; +} + /* ===== 表单字段 ===== */ .field { margin-top: 40rpx; diff --git a/subpackages/mine/scores/scores.js b/subpackages/mine/scores/scores.js new file mode 100644 index 0000000..3545af5 --- /dev/null +++ b/subpackages/mine/scores/scores.js @@ -0,0 +1,79 @@ +const competition = require('../../../utils/competition'); + +Page({ + data: { + events: [], + eventNames: [], + eventIndex: 0, + selectedEventId: '', + keyword: '', + rows: [], + searched: false, + loading: true, + expandedId: '' + }, + + onLoad() { + this.loadEvents(); + }, + + onPullDownRefresh() { + this.loadEvents().finally(() => wx.stopPullDownRefresh()); + }, + + loadEvents() { + this.setData({ loading: true }); + return competition.fetchScoreEvents() + .then((events) => { + const list = Array.isArray(events) ? events : (events && events.rows) || []; + this.setData({ + events: list, + eventNames: list.map((item) => item.name), + eventIndex: 0, + selectedEventId: list[0] ? list[0].id : '' + }); + return this.queryScores(); + }) + .catch((err) => { + this.setData({ rows: [], searched: true }); + wx.showToast({ title: (err && err.message) || '赛事加载失败', icon: 'none' }); + }) + .finally(() => this.setData({ loading: false })); + }, + + onEventChange(e) { + const eventIndex = Number(e.detail.value) || 0; + const selected = this.data.events[eventIndex]; + this.setData({ eventIndex, selectedEventId: selected ? selected.id : '' }); + }, + + onKeywordInput(e) { + this.setData({ keyword: e.detail.value || '' }); + }, + + onClearKeyword() { + this.setData({ keyword: '' }); + }, + + onSearch() { + this.queryScores(); + }, + + queryScores() { + if (this.data.loading && this.data.searched) return Promise.resolve(); + this.setData({ loading: true, expandedId: '' }); + return competition.queryScores({ + eventId: this.data.selectedEventId, + keyword: this.data.keyword + }).then((res) => { + this.setData({ rows: (res && res.rows) || [], searched: true }); + }).catch((err) => { + wx.showToast({ title: (err && err.message) || '查询失败', icon: 'none' }); + }).finally(() => this.setData({ loading: false })); + }, + + onToggleDetail(e) { + const id = e.currentTarget.dataset.id; + this.setData({ expandedId: this.data.expandedId === id ? '' : id }); + } +}); diff --git a/subpackages/mine/scores/scores.json b/subpackages/mine/scores/scores.json new file mode 100644 index 0000000..675d2cd --- /dev/null +++ b/subpackages/mine/scores/scores.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "成绩查询", + "enablePullDownRefresh": true, + "backgroundColor": "#f5f6f8" +} diff --git a/subpackages/mine/scores/scores.wxml b/subpackages/mine/scores/scores.wxml new file mode 100644 index 0000000..32ba716 --- /dev/null +++ b/subpackages/mine/scores/scores.wxml @@ -0,0 +1,74 @@ + + + + + 成绩查询 + 选择赛事,输入姓名或参赛编号查询 + + + + 赛事 + + + {{eventNames[eventIndex] || '暂无可查询赛事'}} + + + + 参赛人 + + ⌕ + + × + + + + + + 查询结果 + 共 {{rows.length}} 条 + + + + + + + + + + 分 + 未查询到成绩 + 请核对赛事、姓名或参赛编号后重试 + + + + + + {{item.rank}} + + {{item.athleteName}}{{item.athleteNo}} + {{item.eventName}} + + + + {{item.totalScore}} + 总分 + + + + 比赛项目{{item.category}} + 参赛组别{{item.groupName}} + 比赛轮次{{item.roundName}} + 成绩状态{{item.status}} + + + + {{item.label}}{{item.score}} + + + + {{expandedId === item.id ? '收起评分明细' : '查看评分明细'}} + + + + + diff --git a/subpackages/mine/scores/scores.wxss b/subpackages/mine/scores/scores.wxss new file mode 100644 index 0000000..878aaf6 --- /dev/null +++ b/subpackages/mine/scores/scores.wxss @@ -0,0 +1,53 @@ +page { min-height: 100%; background: #f5f6f8; color: #18202e; } +.score-page { min-height: 100vh; padding-bottom: env(safe-area-inset-bottom); } +.hero { height: 244rpx; padding: 52rpx 36rpx 0; box-sizing: border-box; color: #fff; background: linear-gradient(135deg,#154a9a 0%,#2871cf 100%); position: relative; overflow: hidden; } +.hero-orb { position: absolute; border-radius: 50%; background: rgba(255,255,255,.09); } +.hero-orb--one { width: 210rpx; height: 210rpx; right: -42rpx; top: -64rpx; } +.hero-orb--two { width: 120rpx; height: 120rpx; right: 150rpx; bottom: -72rpx; } +.hero-title,.hero-subtitle { display: block; position: relative; } +.hero-title { font-size: 42rpx; font-weight: 700; letter-spacing: 2rpx; } +.hero-subtitle { margin-top: 14rpx; font-size: 24rpx; color: rgba(255,255,255,.76); } +.search-card { margin: -54rpx 24rpx 0; padding: 30rpx; background: #fff; border-radius: 22rpx; position: relative; box-shadow: 0 10rpx 32rpx rgba(26,87,178,.12); } +.field-label { display: block; margin-bottom: 13rpx; color: #3d4655; font-size: 25rpx; font-weight: 600; } +.field-label--keyword { margin-top: 24rpx; } +.picker-value,.keyword-box { height: 82rpx; padding: 0 22rpx; display: flex; align-items: center; box-sizing: border-box; border: 2rpx solid #e7ebf2; border-radius: 14rpx; background: #fafbfc; } +.picker-text { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 26rpx; } +.picker-arrow { width: 14rpx; height: 14rpx; margin: -7rpx 5rpx 0 18rpx; flex-shrink: 0; border-right: 3rpx solid #8792a4; border-bottom: 3rpx solid #8792a4; border-radius: 1rpx; transform: rotate(45deg); } +.search-symbol { margin-right: 14rpx; color: #758196; font-size: 36rpx; } +.keyword-input { flex: 1; height: 80rpx; font-size: 26rpx; } +.placeholder { color: #aeb5c0; } +.clear-btn { padding: 12rpx; color: #a3aab5; font-size: 32rpx; } +.search-btn { margin-top: 28rpx; height: 84rpx; line-height: 84rpx; border: 0; border-radius: 16rpx; color: #fff; background: #1a57b2; font-size: 29rpx; font-weight: 600; } +.search-btn::after { border: 0; } +.search-btn[disabled] { color: rgba(255,255,255,.8); background: #8cadd9; } +.result-header { margin: 34rpx 28rpx 20rpx; display: flex; align-items: baseline; justify-content: space-between; } +.result-title { font-size: 31rpx; font-weight: 700; } +.result-count { color: #9299a5; font-size: 23rpx; } +.result-card,.skeleton-card,.empty { margin: 0 24rpx 22rpx; background: #fff; border-radius: 20rpx; box-shadow: 0 6rpx 22rpx rgba(30,52,84,.06); } +.result-top { padding: 28rpx; display: flex; justify-content: space-between; align-items: center; } +.athlete-info { display: flex; align-items: center; min-width: 0; } +.rank-badge { width: 58rpx; height: 58rpx; border-radius: 16rpx; background: #edf4ff; color: #1a57b2; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 800; flex-shrink: 0; } +.athlete-text { margin-left: 18rpx; min-width: 0; } +.athlete-name { font-size: 30rpx; font-weight: 700; } +.athlete-no { margin-left: 12rpx; padding: 3rpx 10rpx; border-radius: 7rpx; background: #f0f2f5; color: #778195; font-size: 20rpx; } +.event-name { display: block; max-width: 400rpx; margin-top: 9rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #8b94a3; font-size: 21rpx; } +.score-main { margin-left: 12rpx; text-align: right; } +.score-num { display: block; color: #1a57b2; font-size: 42rpx; font-weight: 800; } +.score-unit { display: block; margin-top: 2rpx; color: #969eaa; font-size: 20rpx; } +.meta-grid { margin: 0 28rpx; padding: 24rpx 0; border-top: 1rpx solid #eef0f4; display: grid; grid-template-columns: 1fr 1fr; row-gap: 22rpx; } +.meta-item { display: flex; flex-direction: column; } +.meta-label { color: #9ba3af; font-size: 21rpx; } +.meta-value { margin-top: 7rpx; color: #3d4654; font-size: 24rpx; font-weight: 500; } +.meta-value--success { color: #009663; } +.score-breakdown { margin: 0 28rpx; padding: 20rpx 22rpx; border-radius: 14rpx; background: #f7f9fc; display: flex; justify-content: space-around; } +.breakdown-item { display: flex; flex-direction: column; align-items: center; } +.breakdown-label { color: #8993a2; font-size: 20rpx; } +.breakdown-score { margin-top: 8rpx; font-size: 27rpx; font-weight: 700; } +.detail-toggle { height: 72rpx; color: #1a57b2; display: flex; align-items: center; justify-content: center; font-size: 23rpx; } +.toggle-arrow { width: 12rpx; height: 12rpx; margin: -6rpx 0 0 14rpx; border-right: 3rpx solid currentColor; border-bottom: 3rpx solid currentColor; border-radius: 1rpx; transform: rotate(45deg); transition: transform .2s ease, margin .2s ease; }.toggle-arrow.open { margin-top: 6rpx; transform: rotate(225deg); } +.empty { padding: 70rpx 30rpx; display: flex; flex-direction: column; align-items: center; } +.empty-icon { width: 82rpx; height: 82rpx; border-radius: 24rpx; background: #edf4ff; color: #1a57b2; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 700; } +.empty-title { margin-top: 24rpx; font-size: 29rpx; font-weight: 600; }.empty-desc { margin-top: 10rpx; color: #9aa2ae; font-size: 23rpx; } +.skeleton-card { margin-top: 28rpx; padding: 30rpx; }.skeleton-line { height: 26rpx; margin-top: 20rpx; border-radius: 8rpx; background: linear-gradient(90deg,#f0f2f5 25%,#e5e8ed 50%,#f0f2f5 75%); background-size: 200% 100%; animation: shine 1.4s infinite; }.skeleton-line:first-child { margin-top: 0; }.skeleton-line--short { width: 42%; }.skeleton-line--middle { width: 68%; } +@keyframes shine { from { background-position: 200% 0; } to { background-position: -200% 0; } } +.safe-space { height: 30rpx; } diff --git a/utils/auth.js b/utils/auth.js index fe088f9..1009a9d 100644 --- a/utils/auth.js +++ b/utils/auth.js @@ -244,7 +244,7 @@ async function sendVerificationCode(phone) { }); } -async function registerWithVerification({ mobile, password, verificationCode, nickName, avatar }) { +async function registerWithVerification({ mobile, password, verificationCode, nickName, avatar, role }) { const data = { mobile, password, @@ -252,6 +252,12 @@ async function registerWithVerification({ mobile, password, verificationCode, ni }; if (nickName) data.nickName = nickName; if (avatar) data.avatar = avatar; + if (role) { + // 同时保留语义值与数值枚举,联调时可按后端契约删去其中一个字段。 + const roleTypeMap = { participant: 1, referee: 2 }; + data.role = role; + data.roleType = roleTypeMap[role] || 1; + } return request({ path: API.oauthRegister, diff --git a/utils/competition.js b/utils/competition.js new file mode 100644 index 0000000..220bc46 --- /dev/null +++ b/utils/competition.js @@ -0,0 +1,161 @@ +const { API } = require('./constants'); +const { request } = require('./http'); + +// 已对接 YD_Event.Admin;如需离线演示可临时改回 true。 +const USE_LOCAL_MOCK = false; +const CHECKIN_STORAGE_KEY = 'mockCheckinAthletes'; +const REFEREE_APPLICATIONS_KEY = 'mockRefereeApplications'; + +const MOCK_SCORE_EVENTS = [ + { id: 'event-2026-summer', name: '2026 跃享跳花样跳绳公开赛' }, + { id: 'event-2026-city', name: '跃享跳城市挑战赛' } +]; + +const MOCK_SCORES = [ + { + id: 'score-001', eventId: 'event-2026-summer', eventName: '2026 跃享跳花样跳绳公开赛', + athleteName: '陈跃', athleteNo: 'A018', category: '个人花样', groupName: 'U12 男子组', + roundName: '决赛', rank: 1, totalScore: '86.50', difficultyScore: '28.50', + executionScore: '30.00', creativityScore: '28.00', status: '成绩有效', + judgeScores: [ + { label: '难度裁判', score: '28.50' }, { label: '完成裁判', score: '30.00' }, + { label: '创意裁判', score: '28.00' } + ] + }, + { + id: 'score-002', eventId: 'event-2026-summer', eventName: '2026 跃享跳花样跳绳公开赛', + athleteName: '林晓', athleteNo: 'A026', category: '个人花样', groupName: 'U12 女子组', + roundName: '决赛', rank: 2, totalScore: '84.20', difficultyScore: '27.70', + executionScore: '29.00', creativityScore: '27.50', status: '成绩有效', + judgeScores: [ + { label: '难度裁判', score: '27.70' }, { label: '完成裁判', score: '29.00' }, + { label: '创意裁判', score: '27.50' } + ] + }, + { + id: 'score-003', eventId: 'event-2026-city', eventName: '跃享跳城市挑战赛', + athleteName: '跃动少年队', athleteNo: 'T009', category: '团体花样', groupName: '公开组', + roundName: '预赛', rank: 3, totalScore: '81.80', difficultyScore: '26.80', + executionScore: '28.00', creativityScore: '27.00', status: '成绩有效', + judgeScores: [ + { label: '难度裁判', score: '26.80' }, { label: '完成裁判', score: '28.00' }, + { label: '创意裁判', score: '27.00' } + ] + } +]; + +const DEFAULT_CHECKIN_ATHLETES = [ + { id: 'athlete-01', userId: 'user-01', bibNo: 'A018', name: '陈跃', teamName: '跃动俱乐部', checked: true, canCheck: true, orderBookSessionName: '第1场 · 30秒单摇' }, + { id: 'athlete-02', userId: 'user-02', bibNo: 'A021', name: '周航', teamName: '星火跳绳队', checked: true, canCheck: true, orderBookSessionName: '第1场 · 30秒单摇' }, + { id: 'athlete-03', userId: 'user-03', bibNo: 'A024', name: '许嘉', teamName: '青苗体育', checked: false, canCheck: true, orderBookSessionName: '第2场 · 花样跳绳' }, + { id: 'athlete-04', userId: 'user-04', bibNo: 'A027', name: '江晨', teamName: '跃动俱乐部', checked: false, canCheck: true, orderBookSessionName: '第2场 · 花样跳绳' } +]; + +function clone(data) { + return JSON.parse(JSON.stringify(data)); +} + +function mockResolve(data) { + return new Promise((resolve) => setTimeout(() => resolve(clone(data)), 180)); +} + +function fetchScoreEvents() { + if (USE_LOCAL_MOCK) return mockResolve(MOCK_SCORE_EVENTS); + return request({ path: API.scoreEvents, method: 'GET', needAuth: false }); +} + +function queryScores({ eventId, keyword } = {}) { + if (!USE_LOCAL_MOCK) { + return request({ path: API.scoreQuery, method: 'GET', data: { eventId, keyword }, needAuth: false }); + } + const search = String(keyword || '').trim().toLowerCase(); + const rows = MOCK_SCORES.filter((item) => { + const eventMatched = !eventId || item.eventId === eventId; + const keywordMatched = !search || [item.athleteName, item.athleteNo, item.groupName] + .some((value) => String(value || '').toLowerCase().includes(search)); + return eventMatched && keywordMatched; + }); + return mockResolve({ rows, total: rows.length }); +} + +function fetchCurrentCheckin({ eventProjectId, groupId } = {}) { + if (!USE_LOCAL_MOCK) { + const data = {}; + if (eventProjectId !== undefined && eventProjectId !== null && eventProjectId !== '') { + data.eventProjectId = eventProjectId; + } + if (groupId !== undefined && groupId !== null && groupId !== '') { + data.groupId = groupId; + } + return request({ path: API.checkinContext, method: 'GET', data, needAuth: true }); + } + const stored = wx.getStorageSync(CHECKIN_STORAGE_KEY); + const athletes = Array.isArray(stored) ? stored : clone(DEFAULT_CHECKIN_ATHLETES); + const rounds = [{ round: 1, order: 1 }, { round: 2, order: 2 }]; + const pendingAthletes = athletes.filter((item) => !item.checked && item.canCheck); + const checkedAthletes = athletes.filter((item) => item.checked); + const lockedAthletes = athletes.filter((item) => !item.checked && !item.canCheck); + return mockResolve({ + eventId: 'event-2026-summer', eventName: '2026 跃享跳花样跳绳公开赛', venue: '一号检录区', + totalCheckins: 2, currentCheckinNo: 1, currentCheckinName: '一检', + totalRounds: 2, currentRound: 1, roundName: '一检', currentSessionVenue: '一号检录区', + rounds, athletes, pendingAthletes, checkedAthletes, lockedAthletes + }); +} + +function updateCheckinStatus({ unitKey, eventProjectId, groupId, checked }) { + if (!USE_LOCAL_MOCK) { + const data = { unitKey, checked }; + if (eventProjectId !== undefined && eventProjectId !== null && eventProjectId !== '') { + data.eventProjectId = eventProjectId; + } + if (groupId !== undefined && groupId !== null && groupId !== '') { + data.groupId = groupId; + } + return request({ path: API.checkinStatus, method: 'POST', data, needAuth: true }); + } + return fetchCurrentCheckin().then((context) => { + const units = (context.units || []).map((item) => item.unitKey === unitKey ? Object.assign({}, item, { checked: !!checked }) : item); + return Object.assign({}, context, { units }); + }); +} + +function fetchRefereeApplications() { + if (!USE_LOCAL_MOCK) { + return request({ path: API.refereeApplications, method: 'GET', needAuth: true }); + } + const stored = wx.getStorageSync(REFEREE_APPLICATIONS_KEY); + return mockResolve({ rows: Array.isArray(stored) ? stored : [], total: Array.isArray(stored) ? stored.length : 0 }); +} + +function applyForReferee(event) { + const payload = event && typeof event === 'object' ? event : {}; + if (!USE_LOCAL_MOCK) { + return request({ path: API.refereeApply, method: 'POST', data: { eventId: payload.eventId }, needAuth: true }); + } + return fetchRefereeApplications().then((res) => { + const rows = (res && res.rows) || []; + const exists = rows.some((item) => String(item.eventId) === String(payload.eventId)); + if (!exists) { + rows.unshift(Object.assign({}, payload, { + id: `referee-${payload.eventId}`, + applicationId: `mock-${Date.now()}`, + applicationStatus: 1, + applicationStatusText: '已报名执裁', + appliedAt: new Date().toISOString() + })); + wx.setStorageSync(REFEREE_APPLICATIONS_KEY, rows); + } + return { success: true, alreadyApplied: exists, rows }; + }); +} + +module.exports = { + USE_LOCAL_MOCK, + fetchScoreEvents, + queryScores, + fetchCurrentCheckin, + updateCheckinStatus, + fetchRefereeApplications, + applyForReferee +}; diff --git a/utils/constants.js b/utils/constants.js index 4aefced..b6deeca 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -35,6 +35,13 @@ const API = { myOrders: '/api/my/orders', myTeams: '/api/my/teams', myCertificates: '/api/my/certificates', + // 成绩查询、检录接口(后端接入时保持页面层无需改动) + scoreEvents: '/api/eventclient/scores/events', + scoreQuery: '/api/eventclient/scores/query', + checkinContext: '/api/checkin/current', + checkinStatus: '/api/checkin/status', + refereeApplications: '/api/referee/applications', + refereeApply: '/api/referee/applications/apply', fileUpload: '/api/file/upload', uploadMaterial: '/api/eventclient/upload-material', ocrIdCard: '/api/ocr/recognize/idcard', diff --git a/utils/http.js b/utils/http.js index 31e0592..1be899e 100644 --- a/utils/http.js +++ b/utils/http.js @@ -113,6 +113,33 @@ function clearAllTokensAndRedirect() { }); } +/** + * 清理请求参数,避免微信请求层把 undefined/null 转成 URL 中的字符串。 + * GET 会移除无法被后端模型绑定的空参数;POST 仅移除 undefined,保留合法 null。 + */ +function sanitizeRequestData(value, isGet) { + if (value === undefined) return undefined; + if (isGet && (value === null || value === 'undefined' || value === 'null')) return undefined; + if (typeof value === 'number' && !Number.isFinite(value)) return undefined; + + if (Array.isArray(value)) { + return value + .map((item) => sanitizeRequestData(item, isGet)) + .filter((item) => item !== undefined); + } + + if (value && typeof value === 'object') { + const result = {}; + Object.keys(value).forEach((key) => { + const cleaned = sanitizeRequestData(value[key], isGet); + if (cleaned !== undefined) result[key] = cleaned; + }); + return result; + } + + return value; +} + function request({ path, method = 'GET', @@ -126,6 +153,8 @@ function request({ }) { const baseUrl = getBaseUrl(); const url = `${baseUrl}${path}`; + const upperMethod = method.toUpperCase(); + const requestData = sanitizeRequestData(data, upperMethod === 'GET'); // 正常请求:同时带上 access token 和 refresh token // 刷新重试请求:只使用 refresh token @@ -136,8 +165,8 @@ function request({ return new Promise((resolve, reject) => { wx.request({ url, - method: method.toUpperCase(), - data: method.toUpperCase() === 'GET' ? data : data, + method: upperMethod, + data: requestData, header: header, success: async (res) => { // 如果后端在 Response Headers 下发新的 token,就替换本地 token diff --git a/utils/role.js b/utils/role.js new file mode 100644 index 0000000..3254d4f --- /dev/null +++ b/utils/role.js @@ -0,0 +1,61 @@ +const ROLE = { + PARTICIPANT: 'participant', + REFEREE: 'referee', + CHECKER: 'checker' +}; + +const ROLE_META = { + participant: { value: 'participant', label: '参赛者', description: '报名参赛并查询个人成绩' }, + referee: { value: 'referee', label: '裁判', description: '完成裁判认证后参与执裁' }, + checker: { value: 'checker', label: '检录员', description: '查看检录轮次并完成现场检录' } +}; + +function normalizeRole(value) { + if (value == null || value === '') return ROLE.PARTICIPANT; + const raw = String(value).toLowerCase(); + const map = { + '1': ROLE.PARTICIPANT, + participant: ROLE.PARTICIPANT, + athlete: ROLE.PARTICIPANT, + user: ROLE.PARTICIPANT, + player: ROLE.PARTICIPANT, + '参赛者': ROLE.PARTICIPANT, + '2': ROLE.REFEREE, + referee: ROLE.REFEREE, + judge: ROLE.REFEREE, + '裁判': ROLE.REFEREE, + '3': ROLE.CHECKER, + checker: ROLE.CHECKER, + checkin: ROLE.CHECKER, + inspector: ROLE.CHECKER, + '检录员': ROLE.CHECKER + }; + return map[raw] || ROLE.PARTICIPANT; +} + +function getProfileRole(profile) { + const p = profile && typeof profile === 'object' ? profile : {}; + const raw = p.role != null ? p.role + : p.userRole != null ? p.userRole + : p.accountRole != null ? p.accountRole + : p.roleType != null ? p.roleType + : p.RoleType != null ? p.RoleType + : p.userType != null ? p.userType + : p.identityType; + if (raw != null && raw !== '') return normalizeRole(raw); + return normalizeRole(wx.getStorageSync('registeredRole') || ROLE.PARTICIPANT); +} + +function updateEventTabBar(role) { + if (typeof wx === 'undefined' || typeof wx.setTabBarItem !== 'function') return; + const isChecker = normalizeRole(role) === ROLE.CHECKER; + wx.setTabBarItem({ + index: 0, + text: isChecker ? '检录' : '赛事', + iconPath: isChecker ? 'assets/tab_scan.png' : 'assets/tab_home.png', + selectedIconPath: isChecker ? 'assets/tab_scan_sel.png' : 'assets/tab_home_sel.png', + fail() {} + }); +} + +module.exports = { ROLE, ROLE_META, normalizeRole, getProfileRole, updateEventTabBar };