feat: 完善多角色赛事服务功能
- 新增参赛者、裁判、检录员角色识别及角色化首页与个人中心 - 新增裁判注册、身份认证提示、赛事执裁报名及报名记录查询 - 新增检录工作台,支持检录任务、轮次状态与运动员检录操作 - 新增赛事成绩查询页面及成绩详情展示 - 封装成绩、检录和裁判报名接口,补充请求参数清理逻辑 - 新增检录 Tab 图标、页面路由及本地开发配置 - 新增 Git 忽略规则,排除依赖、构建缓存、日志和本地工具文件
This commit is contained in:
parent
5df0380110
commit
d17f43135d
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -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
|
||||
4
app.js
4
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: '',
|
||||
|
||||
4
app.json
4
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"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
BIN
assets/tab_scan.png
Normal file
BIN
assets/tab_scan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 959 B |
BIN
assets/tab_scan_sel.png
Normal file
BIN
assets/tab_scan_sel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@ -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 {
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
|
||||
<view class="info-list">
|
||||
<view class="info-row info-row1">
|
||||
<text class="info-label">报名方式:</text>
|
||||
<text class="info-value">{{registrationMethodText || '—'}}</text>
|
||||
<text class="info-label">{{isReferee ? '执裁报名' : '报名方式'}}:</text>
|
||||
<text class="info-value">{{isReferee ? '裁判在线报名' : (registrationMethodText || '—')}}</text>
|
||||
</view>
|
||||
<view class="info-row info-row1">
|
||||
<text class="info-label">收费模式:</text>
|
||||
|
||||
@ -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' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,29 @@
|
||||
<view class="page-home">
|
||||
<scroll-view class="home-scroll" scroll-y="{{true}}" enhanced show-scrollbar="{{false}}" lower-threshold="100" bindscrolltolower="onScrollToLower" refresher-enabled="{{true}}" refresher-threshold="{{80}}" refresher-default-style="black" refresher-triggered="{{refresherTriggered}}" bindrefresherrefresh="onRefresherRefresh">
|
||||
<scroll-view wx:if="{{isChecker}}" class="home-scroll checker-home" scroll-y="{{true}}" enhanced show-scrollbar="{{false}}" refresher-enabled="{{true}}" refresher-triggered="{{refresherTriggered}}" bindrefresherrefresh="onRefresherRefresh">
|
||||
<view class="checker-hero">
|
||||
<view class="checker-hero-top">
|
||||
<view class="checker-live"><view class="checker-live-dot"></view><text>检录任务</text></view>
|
||||
<text class="checker-venue">{{checkerContext.venue || '等待分配场地'}}</text>
|
||||
</view>
|
||||
<text class="checker-event">{{checkerContext.eventName || '当前暂无检录任务'}}</text>
|
||||
<text class="checker-welcome">检录员工作台</text>
|
||||
</view>
|
||||
<view wx:if="{{checkerContext}}" class="checker-task-card">
|
||||
<view class="checker-task-head">
|
||||
<view><text class="checker-task-label">当前检次</text><text class="checker-task-round">{{checkerContext.currentCheckinName || checkerContext.roundName}} · 共 {{checkerContext.totalCheckins || checkerContext.totalRounds}} 检</text></view>
|
||||
<view class="checker-task-status">进行中</view>
|
||||
</view>
|
||||
<view class="checker-group-box">
|
||||
<text class="checker-task-label">检录范围</text>
|
||||
<text class="checker-group-name">全部运动员</text>
|
||||
<text class="checker-group-order">后一检仅接收已通过前一检的运动员</text>
|
||||
</view>
|
||||
<button class="checker-enter-btn" bindtap="goToCheckin">进入检录工作台</button>
|
||||
</view>
|
||||
<view wx:elif="{{checkerLoading}}" class="checker-empty">正在加载检录任务...</view>
|
||||
<view wx:else class="checker-empty">当前暂无检录任务,下拉可刷新</view>
|
||||
</scroll-view>
|
||||
<scroll-view wx:else class="home-scroll" scroll-y="{{true}}" enhanced show-scrollbar="{{false}}" lower-threshold="100" bindscrolltolower="onScrollToLower" refresher-enabled="{{true}}" refresher-threshold="{{80}}" refresher-default-style="black" refresher-triggered="{{refresherTriggered}}" bindrefresherrefresh="onRefresherRefresh">
|
||||
<swiper class="banner" indicator-dots="{{banners.length > 1}}" autoplay="{{banners.length > 1}}" interval="4000" circular="{{banners.length > 1}}" indicator-color="rgba(255,255,255,0.55)" indicator-active-color="#1a57b2">
|
||||
<block wx:for="{{banners}}" wx:key="_bannerKey">
|
||||
<swiper-item>
|
||||
@ -20,11 +44,11 @@
|
||||
</view>
|
||||
<view class="section-tabs">
|
||||
<view class="section-tab {{sectionTab === 'list' ? 'is-active' : ''}}" data-tab="list" bindtap="onSectionTab">
|
||||
<text class="section-tab-text">赛事报名</text>
|
||||
<text class="section-tab-text">{{isReferee ? '报名执裁' : '赛事报名'}}</text>
|
||||
<image class="section-tab-img" wx:if="{{sectionTab === 'list'}}" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/home-title-b.png" />
|
||||
</view>
|
||||
<view class="section-tab {{sectionTab === 'registered' ? 'is-active' : ''}}" data-tab="registered" bindtap="onSectionTab">
|
||||
<text class="section-tab-text">已报赛事</text>
|
||||
<text class="section-tab-text">{{isReferee ? '已报名执裁' : '已报赛事'}}</text>
|
||||
<image class="section-tab-img" wx:if="{{sectionTab === 'registered'}}" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/home-title-b.png" />
|
||||
</view>
|
||||
</view>
|
||||
@ -43,7 +67,7 @@
|
||||
<view class="list-section">
|
||||
<block wx:if="{{sectionTab === 'registered' && !loggedIn}}">
|
||||
<view class="login-hint">
|
||||
<text class="login-hint-text">登录后可查看已报名赛事</text>
|
||||
<text class="login-hint-text">登录后可查看{{isReferee ? '已报名执裁赛事' : '已报名赛事'}}</text>
|
||||
<button class="login-hint-btn" type="primary" bindtap="goLogin">去登录</button>
|
||||
</view>
|
||||
</block>
|
||||
@ -86,7 +110,8 @@
|
||||
<view class="meta-line">比赛时间:{{item.matchStartText}}~{{item.matchEndText}}</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="meta-line">报名时间:{{item.registrationTimeText}}</view>
|
||||
<view class="meta-line">{{isReferee ? '报名执裁时间' : '报名时间'}}:{{item.registrationTimeText}}</view>
|
||||
<view class="meta-line" wx:if="{{isReferee && item.matchStartText}}">比赛时间:{{item.matchStartText}}~{{item.matchEndText}}</view>
|
||||
<view class="meta-line" wx:if="{{item.projectCount != null}}">
|
||||
已报项目:{{item.projectCount}} 项
|
||||
</view>
|
||||
@ -95,7 +120,10 @@
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="card-actions" wx:if="{{sectionTab === 'registered'}}" catchtap>
|
||||
<view class="card-actions" wx:if="{{sectionTab === 'registered' && isReferee}}" catchtap>
|
||||
<view class="action-btn action-btn--referee" catchtap>已报名执裁</view>
|
||||
</view>
|
||||
<view class="card-actions" wx:elif="{{sectionTab === 'registered'}}" catchtap>
|
||||
<view wx:if="{{item.isNeedMaterial}}" class="action-btn action-btn--ghost" catchtap="onUploadMaterialTap" data-registrationid="{{item.registrationId}}" data-matstarttime="{{item.matStartTime}}" data-matendtime="{{item.matEndTime}}" data-materialrangetext="{{item.materialRangeText}}">
|
||||
上传素材
|
||||
</view>
|
||||
@ -131,4 +159,4 @@
|
||||
<view wx:elif="{{!hasMore && rows.length > 0}}" class="loading">没有更多了</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: '未认证' });
|
||||
}
|
||||
});
|
||||
|
||||
@ -22,11 +22,12 @@
|
||||
</view>
|
||||
</button>
|
||||
<text class="phone-text">{{maskedPhone}}</text>
|
||||
<view class="role-pill">{{roleLabel}}</view>
|
||||
</view>
|
||||
<!-- 内容区 -->
|
||||
<view class="content">
|
||||
<!-- 积分 & 证书双卡 -->
|
||||
<view class="stat-row">
|
||||
<view wx:if="{{isParticipant}}" class="stat-row">
|
||||
<view class="stat-card" bindtap="goToPoints">
|
||||
<view class="stat-left">
|
||||
<text class="stat-num">{{profile.points || 0}}</text>
|
||||
@ -43,10 +44,10 @@
|
||||
</view>
|
||||
</view>
|
||||
<!-- 身份认证条 -->
|
||||
<view class="auth-bar" bindtap="goToAuth">
|
||||
<view wx:if="{{!isChecker}}" class="auth-bar" bindtap="goToAuth">
|
||||
<view class="auth-left">
|
||||
<text class="auth-title">身份认证</text>
|
||||
<text class="auth-desc">点击进行身份认证</text>
|
||||
<text class="auth-title">{{isReferee ? '裁判身份认证' : '身份认证'}}</text>
|
||||
<text class="auth-desc">{{isReferee ? '完成认证后可参与赛事执裁' : '点击进行身份认证'}}</text>
|
||||
</view>
|
||||
<view class="auth-badge auth-badge-{{authStatus === 2 ? 'y' : 'n'}}">
|
||||
<image class="auth-badge-icon" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/auth-{{authStatus === 2 ? 'y' : 'n'}}.png" mode="aspectFit" />
|
||||
@ -55,6 +56,26 @@
|
||||
</view>
|
||||
<!-- 功能入口:三个独立阴影卡片 -->
|
||||
<view class="func-list">
|
||||
<view class="func-menu-card" bindtap="goToScores">
|
||||
<view class="func-line-icon score-line-icon">
|
||||
<view class="score-line-bars"><view class="score-line-bar bar-one"></view><view class="score-line-bar bar-two"></view><view class="score-line-bar bar-three"></view></view>
|
||||
</view>
|
||||
<text class="func-menu-text">成绩查询</text>
|
||||
<text class="func-menu-arrow">›</text>
|
||||
</view>
|
||||
<view wx:if="{{isChecker}}" class="func-menu-card func-menu-card--primary" bindtap="goToCheckin">
|
||||
<view class="func-line-icon scan-line-icon">
|
||||
<view class="scan-corner corner-tl"></view><view class="scan-corner corner-tr"></view><view class="scan-corner corner-bl"></view><view class="scan-corner corner-br"></view>
|
||||
<view class="scan-center-line"></view>
|
||||
<view class="scan-person"><view class="scan-person-head"></view><view class="scan-person-body"></view></view>
|
||||
</view>
|
||||
<view class="func-menu-main">
|
||||
<text class="func-menu-text">检录工作台</text>
|
||||
<text class="func-menu-sub">查看当前检录轮次与组别</text>
|
||||
</view>
|
||||
<text class="func-menu-arrow">›</text>
|
||||
</view>
|
||||
<block wx:if="{{isParticipant}}">
|
||||
<view class="func-menu-card" bindtap="goToOrders">
|
||||
<image class="func-menu-icon" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/order.png" mode="aspectFit" />
|
||||
<text class="func-menu-text">我的订单</text>
|
||||
@ -70,8 +91,9 @@
|
||||
<text class="func-menu-text">我的团队</text>
|
||||
<text class="func-menu-arrow">›</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<!-- 退出登录 -->
|
||||
<button class="logout-btn" bindtap="onLogout">退出登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
<view class="auth-page">
|
||||
<view wx:if="{{isReferee && !loading}}" class="referee-auth-tip">
|
||||
<text class="referee-auth-title">裁判身份认证</text>
|
||||
<text class="referee-auth-desc">请提交本人有效证件,审核通过后即可获得裁判工作权限</text>
|
||||
</view>
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item {{activeTab === 1 ? 'active' : ''}}" bindtap="onSwitchTab" data-tab="1">
|
||||
<text class="tab-text">身份证认证</text>
|
||||
@ -151,4 +155,4 @@
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
152
subpackages/mine/checkin/checkin.js
Normal file
152
subpackages/mine/checkin/checkin.js
Normal file
@ -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: '' }));
|
||||
}
|
||||
});
|
||||
5
subpackages/mine/checkin/checkin.json
Normal file
5
subpackages/mine/checkin/checkin.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "检录工作台",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#f5f6f8"
|
||||
}
|
||||
113
subpackages/mine/checkin/checkin.wxml
Normal file
113
subpackages/mine/checkin/checkin.wxml
Normal file
@ -0,0 +1,113 @@
|
||||
<view class="checkin-page">
|
||||
<view class="top-card" wx:if="{{context}}">
|
||||
<view class="top-line">
|
||||
<view class="live-pill"><view class="live-dot"></view><text>检录进行中</text></view>
|
||||
<text class="checkin-sequence">{{context.currentCheckinNo}} / {{context.totalCheckins}} 检</text>
|
||||
</view>
|
||||
<text class="event-title">{{context.eventName}}</text>
|
||||
<view class="compact-status">
|
||||
<view class="compact-status-item checkin-status-item">
|
||||
<text class="compact-label">当前检次</text>
|
||||
<text class="compact-value checkin-name">{{context.currentCheckinName}}</text>
|
||||
</view>
|
||||
<view class="compact-divider"></view>
|
||||
<view class="compact-status-item group-status-item">
|
||||
<text class="compact-label">当前组别</text>
|
||||
<text class="compact-value">{{context.currentGroupName || '请选择组别'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="selector-card {{loading ? 'selector-loading' : ''}}" wx:if="{{context}}">
|
||||
<view class="selector-heading">
|
||||
<view class="selector-step">1</view>
|
||||
<view class="selector-heading-text"><text class="selector-title">选择参赛项目</text><text class="selector-hint">左右滑动查看更多项目</text></view>
|
||||
</view>
|
||||
<scroll-view class="project-scroll" scroll-x enhanced show-scrollbar="{{false}}" scroll-with-animation>
|
||||
<view class="project-options">
|
||||
<view
|
||||
wx:for="{{projects}}"
|
||||
wx:key="eventProjectId"
|
||||
class="project-option {{index === projectIndex ? 'active' : ''}} {{item.isCompleted ? 'completed' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onProjectTap"
|
||||
>
|
||||
<view class="project-active-mark"></view>
|
||||
<view class="project-option-top"><text class="project-option-name">{{item.projectName}}</text><text class="project-complete-label" wx:if="{{item.isCompleted}}">已完成</text></view>
|
||||
<view class="project-option-summary"><text>{{item.checkedUnits}} / {{item.totalUnits}} 项</text><text>{{item.progressPercent}}%</text></view>
|
||||
<view class="project-progress"><view class="project-progress-value" style="width: {{item.progressPercent}}%;"></view></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="selector-separator"></view>
|
||||
|
||||
<view class="selector-heading group-heading">
|
||||
<view class="selector-step">2</view>
|
||||
<view class="selector-heading-text"><text class="selector-title">选择参赛组别</text><text class="selector-hint">点击后立即切换检录名单</text></view>
|
||||
</view>
|
||||
<scroll-view class="group-scroll" scroll-x enhanced show-scrollbar="{{false}}" scroll-with-animation>
|
||||
<view class="group-options">
|
||||
<view
|
||||
wx:for="{{groups}}"
|
||||
wx:key="id"
|
||||
class="group-option {{index === groupIndex ? 'active' : ''}} {{item.isCompleted ? 'completed' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onGroupTap"
|
||||
>
|
||||
<view class="group-active-dot"></view>
|
||||
<view class="group-option-content"><text class="group-option-name">{{item.groupName}}</text><text class="group-option-progress">{{item.checkedUnits}}/{{item.totalUnits}} · {{item.isCompleted ? '已完成' : item.progressPercent + '%'}}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="skeleton" wx:if="{{loading && !context}}"><view></view><view></view><view></view></view>
|
||||
|
||||
<block wx:if="{{context}}">
|
||||
<view class="progress-card">
|
||||
<view class="progress-top"><text class="section-title">当前组别进度</text><text class="progress-count"><text class="progress-count-main">{{checkedCount}}</text> / {{units.length}} 项</text></view>
|
||||
<view class="progress-track"><view class="progress-fill" style="width: {{progressPercent}}%;"></view></view>
|
||||
<view class="progress-bottom"><text>已检录 {{checkedCount}} 项</text><text>待检录 {{pendingCount}} 项</text></view>
|
||||
</view>
|
||||
|
||||
<view class="list-heading"><text class="section-title">待检录</text><text class="list-tip">按秩序册顺序</text></view>
|
||||
<view class="unit-card" wx:for="{{pendingUnits}}" wx:key="unitKey">
|
||||
<view class="unit-head"><text class="unit-title">{{item.teamName || '个人参赛'}}</text><text class="venue-badge">{{item.venueName}}</text></view>
|
||||
<view class="unit-meta" wx:if="{{item.unitName}}"><text class="unit-meta-label">单位</text><text class="unit-meta-value">{{item.unitName}}</text></view>
|
||||
<view class="unit-athletes"><text class="athlete-chip" wx:for="{{item.athletes}}" wx:for-item="athlete" wx:key="id">{{athlete.name}}</text></view>
|
||||
<view class="unit-action {{updatingKey === item.unitKey ? 'disabled' : ''}}" data-key="{{item.unitKey}}" bindtap="onToggleUnit">确认检录</view>
|
||||
</view>
|
||||
<view wx:if="{{!pendingUnits.length}}" class="empty-tip">当前组别暂无待检录项</view>
|
||||
|
||||
<view class="list-heading" wx:if="{{lockedUnits.length}}"><text class="section-title">等待前序检录</text><text class="list-tip">前一检通过后可操作</text></view>
|
||||
<view class="unit-card locked" wx:for="{{lockedUnits}}" wx:key="unitKey">
|
||||
<view class="unit-head"><text class="unit-title">{{item.teamName || '个人参赛'}}</text><text class="venue-badge">{{item.venueName}}</text></view>
|
||||
<view class="unit-meta" wx:if="{{item.unitName}}"><text class="unit-meta-label">单位</text><text class="unit-meta-value">{{item.unitName}}</text></view>
|
||||
<view class="unit-athletes"><text class="athlete-chip" wx:for="{{item.athletes}}" wx:for-item="athlete" wx:key="id">{{athlete.name}}</text></view>
|
||||
<view class="unit-action disabled">{{item.lockReason}}</view>
|
||||
</view>
|
||||
|
||||
<view class="checked-section-head">
|
||||
<view class="checked-section-title"><text class="section-title">已检录</text><text class="checked-count">{{checkedUnits.length}} 项</text></view>
|
||||
<text class="scope-tip">仅展示当前选中项目和组别的已检录结果</text>
|
||||
</view>
|
||||
<block wx:for="{{checkedGroups}}" wx:for-item="checkedGroup" wx:key="key">
|
||||
<view class="checked-group-head">
|
||||
<view class="checked-group-names"><text class="checked-project-name">{{checkedGroup.projectName}}</text><text class="checked-group-name">{{checkedGroup.groupName}}</text></view>
|
||||
<text class="checked-group-count">{{checkedGroup.units.length}} 项</text>
|
||||
</view>
|
||||
<view class="unit-card" wx:for="{{checkedGroup.units}}" wx:for-item="item" wx:key="unitKey">
|
||||
<view class="unit-head"><text class="unit-title">{{item.teamName || '个人参赛'}}</text><text class="venue-badge">{{item.venueName}}</text></view>
|
||||
<view class="unit-meta" wx:if="{{item.unitName}}"><text class="unit-meta-label">单位</text><text class="unit-meta-value">{{item.unitName}}</text></view>
|
||||
<view class="unit-athletes"><text class="athlete-chip checked-chip" wx:for="{{item.athletes}}" wx:for-item="athlete" wx:key="id">{{athlete.name}}</text></view>
|
||||
<view class="checked-actions">
|
||||
<view class="checked-state"><text class="checked-mark">✓</text><text>已完成当前检录</text></view>
|
||||
<view class="cancel-checkin-btn {{updatingKey === item.unitKey ? 'disabled' : ''}}" data-key="{{item.unitKey}}" bindtap="onToggleUnit">取消检录</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view wx:if="{{!checkedUnits.length}}" class="empty-tip">暂无已检录项</view>
|
||||
</block>
|
||||
<view class="safe-space"></view>
|
||||
</view>
|
||||
39
subpackages/mine/checkin/checkin.wxss
Normal file
39
subpackages/mine/checkin/checkin.wxss
Normal file
@ -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; }
|
||||
@ -34,15 +34,15 @@
|
||||
<view class="tab-line" wx:if="{{loginMode === 'password'}}"></view>
|
||||
</view>
|
||||
<view class="tab-item {{loginMode === 'code' ? 'active' : ''}}" bindtap="onSwitchMode" data-mode="code">
|
||||
<text class="tab-text">短信登录</text>
|
||||
<text class="tab-text">手机号登录</text>
|
||||
<view class="tab-line" wx:if="{{loginMode === 'code'}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 账号登录模式 -->
|
||||
<!-- 手机号密码登录模式 -->
|
||||
<block wx:if="{{loginMode === 'password'}}">
|
||||
<view class="field">
|
||||
<text class="field-label">账号</text>
|
||||
<text class="field-label">手机号</text>
|
||||
<view class="field-input-box">
|
||||
<input
|
||||
class="field-input"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const auth = require('../../../utils/auth');
|
||||
const { ROLE_META } = require('../../../utils/role');
|
||||
|
||||
const PHONE_RE = /^1[3456789]\d{9}$/;
|
||||
|
||||
@ -10,6 +11,9 @@ Page({
|
||||
password2: '',
|
||||
verificationCode: '',
|
||||
nickName: '',
|
||||
selectedRole: 'participant',
|
||||
selectedRoleDescription: ROLE_META.participant.description,
|
||||
roleOptions: [ROLE_META.participant, ROLE_META.referee],
|
||||
submitting: false,
|
||||
codeSending: false,
|
||||
countdown: 0,
|
||||
@ -60,6 +64,16 @@ Page({
|
||||
this.setData({ nickName: (e.detail.value || '').trim() });
|
||||
},
|
||||
|
||||
onRoleTap(e) {
|
||||
if (this.data.isReset) return;
|
||||
const role = e.currentTarget.dataset.role;
|
||||
if (!ROLE_META[role]) return;
|
||||
this.setData({
|
||||
selectedRole: role,
|
||||
selectedRoleDescription: ROLE_META[role].description
|
||||
});
|
||||
},
|
||||
|
||||
onSendCodeTap() {
|
||||
const { mobile, countdown, codeSending } = this.data;
|
||||
if (codeSending || countdown > 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) {
|
||||
|
||||
@ -17,6 +17,16 @@
|
||||
<view class="card-title-row">
|
||||
<text class="card-title">{{isReset ? '重置密码' : '账号注册'}}</text>
|
||||
</view>
|
||||
<view wx:if="{{!isReset}}" class="role-field">
|
||||
<text class="field-label">注册角色</text>
|
||||
<view class="role-switch">
|
||||
<view wx:for="{{roleOptions}}" wx:key="value" class="role-option {{selectedRole === item.value ? 'active' : ''}}" data-role="{{item.value}}" bindtap="onRoleTap">
|
||||
<text class="role-name">{{item.label}}</text>
|
||||
<text class="role-check" wx:if="{{selectedRole === item.value}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="role-desc">{{selectedRoleDescription}}</text>
|
||||
</view>
|
||||
<!-- 手机号 -->
|
||||
<view class="field">
|
||||
<text class="field-label">手机号</text>
|
||||
@ -29,9 +39,9 @@
|
||||
<text class="field-label">验证码</text>
|
||||
<view class="field-input-box">
|
||||
<input class="field-input" type="number" maxlength="8" placeholder="请输入短信验证码" placeholder-class="placeholder" value="{{verificationCode}}" bindinput="onCodeInput" />
|
||||
<div class="send-code-link {{(codeSending || countdown > 0) ? 'disabled' : ''}}" bindtap="onSendCodeTap">
|
||||
<view class="send-code-link {{(codeSending || countdown > 0) ? 'disabled' : ''}}" bindtap="onSendCodeTap">
|
||||
{{countdown > 0 ? countdown + 's 后重发' : '获取验证码'}}
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 密码 -->
|
||||
@ -72,4 +82,4 @@
|
||||
</navigator>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -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;
|
||||
|
||||
79
subpackages/mine/scores/scores.js
Normal file
79
subpackages/mine/scores/scores.js
Normal file
@ -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 });
|
||||
}
|
||||
});
|
||||
5
subpackages/mine/scores/scores.json
Normal file
5
subpackages/mine/scores/scores.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "成绩查询",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#f5f6f8"
|
||||
}
|
||||
74
subpackages/mine/scores/scores.wxml
Normal file
74
subpackages/mine/scores/scores.wxml
Normal file
@ -0,0 +1,74 @@
|
||||
<view class="score-page">
|
||||
<view class="hero">
|
||||
<view class="hero-orb hero-orb--one"></view>
|
||||
<view class="hero-orb hero-orb--two"></view>
|
||||
<text class="hero-title">成绩查询</text>
|
||||
<text class="hero-subtitle">选择赛事,输入姓名或参赛编号查询</text>
|
||||
</view>
|
||||
|
||||
<view class="search-card">
|
||||
<text class="field-label">赛事</text>
|
||||
<picker mode="selector" range="{{eventNames}}" value="{{eventIndex}}" bindchange="onEventChange">
|
||||
<view class="picker-value">
|
||||
<text class="picker-text">{{eventNames[eventIndex] || '暂无可查询赛事'}}</text>
|
||||
<view class="picker-arrow"></view>
|
||||
</view>
|
||||
</picker>
|
||||
<text class="field-label field-label--keyword">参赛人</text>
|
||||
<view class="keyword-box">
|
||||
<text class="search-symbol">⌕</text>
|
||||
<input class="keyword-input" value="{{keyword}}" placeholder="姓名 / 参赛编号" placeholder-class="placeholder" confirm-type="search" bindinput="onKeywordInput" bindconfirm="onSearch" />
|
||||
<text wx:if="{{keyword}}" class="clear-btn" bindtap="onClearKeyword">×</text>
|
||||
</view>
|
||||
<button class="search-btn" loading="{{loading}}" disabled="{{loading || !selectedEventId}}" bindtap="onSearch">查询成绩</button>
|
||||
</view>
|
||||
|
||||
<view class="result-header" wx:if="{{searched && !loading}}">
|
||||
<text class="result-title">查询结果</text>
|
||||
<text class="result-count">共 {{rows.length}} 条</text>
|
||||
</view>
|
||||
|
||||
<view class="skeleton-card" wx:if="{{loading}}" >
|
||||
<view class="skeleton-line skeleton-line--short"></view>
|
||||
<view class="skeleton-line"></view>
|
||||
<view class="skeleton-line skeleton-line--middle"></view>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:elif="{{searched && rows.length === 0}}">
|
||||
<view class="empty-icon">分</view>
|
||||
<text class="empty-title">未查询到成绩</text>
|
||||
<text class="empty-desc">请核对赛事、姓名或参赛编号后重试</text>
|
||||
</view>
|
||||
|
||||
<view class="result-card" wx:for="{{rows}}" wx:key="id">
|
||||
<view class="result-top">
|
||||
<view class="athlete-info">
|
||||
<view class="rank-badge">{{item.rank}}</view>
|
||||
<view class="athlete-text">
|
||||
<view><text class="athlete-name">{{item.athleteName}}</text><text class="athlete-no">{{item.athleteNo}}</text></view>
|
||||
<text class="event-name">{{item.eventName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="score-main">
|
||||
<text class="score-num">{{item.totalScore}}</text>
|
||||
<text class="score-unit">总分</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="meta-grid">
|
||||
<view class="meta-item"><text class="meta-label">比赛项目</text><text class="meta-value">{{item.category}}</text></view>
|
||||
<view class="meta-item"><text class="meta-label">参赛组别</text><text class="meta-value">{{item.groupName}}</text></view>
|
||||
<view class="meta-item"><text class="meta-label">比赛轮次</text><text class="meta-value">{{item.roundName}}</text></view>
|
||||
<view class="meta-item"><text class="meta-label">成绩状态</text><text class="meta-value meta-value--success">{{item.status}}</text></view>
|
||||
</view>
|
||||
<view wx:if="{{expandedId === item.id}}" class="score-breakdown">
|
||||
<view class="breakdown-item" wx:for="{{item.judgeScores}}" wx:key="label">
|
||||
<text class="breakdown-label">{{item.label}}</text><text class="breakdown-score">{{item.score}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-toggle" data-id="{{item.id}}" bindtap="onToggleDetail">
|
||||
<text>{{expandedId === item.id ? '收起评分明细' : '查看评分明细'}}</text>
|
||||
<view class="toggle-arrow {{expandedId === item.id ? 'open' : ''}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="safe-space"></view>
|
||||
</view>
|
||||
53
subpackages/mine/scores/scores.wxss
Normal file
53
subpackages/mine/scores/scores.wxss
Normal file
@ -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; }
|
||||
@ -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,
|
||||
|
||||
161
utils/competition.js
Normal file
161
utils/competition.js
Normal file
@ -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
|
||||
};
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
61
utils/role.js
Normal file
61
utils/role.js
Normal file
@ -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 };
|
||||
Loading…
x
Reference in New Issue
Block a user