ltx d17f43135d feat: 完善多角色赛事服务功能
- 新增参赛者、裁判、检录员角色识别及角色化首页与个人中心
- 新增裁判注册、身份认证提示、赛事执裁报名及报名记录查询
- 新增检录工作台,支持检录任务、轮次状态与运动员检录操作
- 新增赛事成绩查询页面及成绩详情展示
- 封装成绩、检录和裁判报名接口,补充请求参数清理逻辑
- 新增检录 Tab 图标、页面路由及本地开发配置
- 新增 Git 忽略规则,排除依赖、构建缓存、日志和本地工具文件
2026-08-07 11:28:38 +08:00

650 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) {
if (!raw || typeof raw !== 'object') {
return { imageUrl: '', linkType: '', eventId: null, linkUrl: '' };
}
const imageUrl = raw.imageUrl || raw.ImageUrl || '';
const linkUrl = String(raw.linkUrl || raw.LinkUrl || '').trim();
let linkType = raw.linkType != null ? raw.linkType : raw.LinkType;
const rawEventId = raw.eventId != null ? raw.eventId : raw.EventId;
const eventId =
rawEventId != null && rawEventId !== '' ? Number(rawEventId) : null;
const idOk = Number.isFinite(eventId) && eventId > 0;
if (typeof linkType === 'number') {
if (linkType === 1) linkType = 'Event';
else if (linkType === 2) linkType = 'Url';
else if (linkType === 0) linkType = '';
else linkType = String(linkType);
} else if (typeof linkType === 'string') {
const lower = linkType.toLowerCase();
if (lower === 'event') linkType = 'Event';
else if (lower === 'url' || lower === 'link' || lower === 'external') linkType = 'Url';
}
if (linkType == null || linkType === '' || linkType === '0') {
if (idOk) linkType = 'Event';
else if (linkUrl) linkType = 'Url';
else linkType = '';
}
return {
imageUrl,
linkType,
eventId: idOk ? eventId : null,
linkUrl
};
}
function canCancelByEventStatus(rawEventStatus) {
// 仅报名中(Enrolling / 1)允许取消
if (rawEventStatus === 'Enrolling') return true;
const n = Number(rawEventStatus);
if (!Number.isNaN(n)) return n === 1;
const s = rawEventStatus == null ? '' : String(rawEventStatus).toLowerCase();
return s === 'enrolling' || s === '1';
}
Page({
data: {
sectionTab: 'list',
loggedIn: false,
role: ROLE.PARTICIPANT,
isReferee: false,
isChecker: false,
checkerContext: null,
checkerLoading: false,
banners: [],
rows: [],
loading: false,
isFirstLoad: false,
loadingText: '加载中...',
pageNo: 1,
pageSize: 10,
hasMore: true,
/** 列表筛选:-1=全部(不传 status),0~4 与后端 EventStatus 一致;默认报名中 */
status: 1,
keyword: '',
refresherTriggered: false
},
onLoad() {
this._syncRoleContext();
if (this.data.isChecker) {
this.loadCheckerContext();
} else {
this.fetchBanners();
this.loadEvents(true);
}
},
onShow() {
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'
? this.loadEvents(true)
: this.loadMyEvents(true);
p.finally(() => {
this.setData({ refresherTriggered: false });
});
},
onPullDownRefresh() {
if (this.data.isChecker) {
this.loadCheckerContext().finally(() => wx.stopPullDownRefresh());
return;
}
this.fetchBanners();
const p =
this.data.sectionTab === 'list'
? this.loadEvents(true)
: this.loadMyEvents(true);
p.finally(() => {
wx.stopPullDownRefresh();
});
},
fetchBanners() {
request({
path: API.banners,
method: 'GET',
needAuth: false
})
.then((data) => {
const arr = Array.isArray(data) ? data : [];
const banners = arr.map((item, index) => ({
...normalizeBannerItem(item),
_bannerKey: index
}));
this.setData({ banners });
})
.catch(() => {
this.setData({ banners: [] });
});
},
onSectionTab(e) {
const tab = e.currentTarget.dataset.tab;
if (!tab || tab === this.data.sectionTab) return;
this.setData({
sectionTab: tab,
pageNo: 1,
rows: [],
hasMore: true
});
if (tab === 'list') {
this.loadEvents(true);
} else {
this.loadMyEvents(true);
}
},
onStatusTap(e) {
const status = Number(e.currentTarget.dataset.status);
if (Number.isNaN(status)) return;
this.setData({
status,
pageNo: 1,
rows: [],
hasMore: true
});
this.loadEvents(true);
},
onKeywordInput(e) {
this.setData({ keyword: e.detail.value || '' });
},
onSearch() {
if (this.data.sectionTab !== 'list') {
this.setData({ sectionTab: 'list', pageNo: 1, rows: [], hasMore: true });
}
this.setData({
pageNo: 1,
rows: [],
hasMore: true
});
this.loadEvents(true);
},
onScrollToLower() {
if (this.data.loading || !this.data.hasMore) return;
if (this.data.sectionTab === 'list') {
const next = this.data.pageNo + 1;
this.setData({ pageNo: next });
this.loadEvents(false);
} else {
if (!this.data.loggedIn) return;
const next = this.data.pageNo + 1;
this.setData({ pageNo: next });
this.loadMyEvents(false);
}
},
goLogin() {
wx.navigateTo({ url: '/subpackages/mine/login/login' });
},
onBannerTap(e) {
const linkType = e.currentTarget.dataset.linktype;
const eventId = Number(e.currentTarget.dataset.eventid);
const linkUrl = e.currentTarget.dataset.linkurl || '';
if (linkType === 'Event' && eventId > 0) {
wx.navigateTo({ url: `/pages/detail/detail?id=${eventId}` });
return;
}
if (linkUrl) {
wx.showToast({ title: '该轮播链接为外部链接/其他跳转,请在后台配置', icon: 'none' });
}
},
onEventTap(e) {
const id = Number(e.currentTarget.dataset.id);
if (!id) return;
wx.navigateTo({ url: `/pages/detail/detail?id=${id}` });
},
onCancelRegistrationTap(e) {
const canCancel = e.currentTarget.dataset.cancancel;
if (!canCancel) return;
const registrationId = Number(e.currentTarget.dataset.registrationid);
if (!registrationId) return;
wx.navigateTo({
url: `/subpackages/mine/cancel-registration/cancel-registration?registrationId=${registrationId}`
});
},
onUploadMaterialTap(e) {
const registrationId = Number(e.currentTarget.dataset.registrationid);
if (!registrationId) return;
const now = Date.now();
console.log('时间', e.currentTarget.dataset.matstarttime, e.currentTarget.dataset.matendtime , now);
const matStartTime = e.currentTarget.dataset.matstarttime;
const matEndTime = e.currentTarget.dataset.matendtime;
const rangeText = e.currentTarget.dataset.materialrangetext || '';
if (matStartTime && now < Number(matStartTime)) {
wx.showToast({
title: `素材上传未开始,开始时间:${rangeText.split(' ~ ')[0] || rangeText}`,
icon: 'none',
duration: 2500
});
return;
}
if (matEndTime && now > Number(matEndTime)) {
wx.showToast({
title: `素材上传已截止,截止时间:${rangeText.split(' ~ ')[1] || rangeText}`,
icon: 'none',
duration: 2500
});
return;
}
this.openUploadPicker({ kind: 'material', registrationId });
},
onUploadVoucherTap(e) {
const registrationId = Number(e.currentTarget.dataset.registrationid);
if (!registrationId) return;
this.openUploadPicker({ kind: 'voucher', registrationId });
},
openUploadPicker({ kind, registrationId }) {
const title = kind === 'voucher' ? '上传付款凭证' : '上传素材';
if (kind === 'material') {
wx.showActionSheet({
itemList: ['从相册选择图片/视频', '拍照/录视频', '选择音乐文件'],
success: (r) => {
if (!r || typeof r.tapIndex !== 'number') return;
if (r.tapIndex === 2) {
wx.chooseMessageFile({
count: 1,
type: 'file',
extension: ['mp3', 'wav', 'aac', 'flac', 'm4a', 'ogg'],
success: (res) => {
const file = res && res.tempFiles && res.tempFiles[0];
const tempFilePath = file ? file.path : '';
if (!tempFilePath) return;
this._confirmAndUploadMaterial(title, tempFilePath, registrationId);
}
});
} else {
const sourceType = r.tapIndex === 1 ? ['camera'] : ['album'];
wx.chooseMedia({
count: 1,
mediaType: ['image', 'video'],
sourceType,
success: (res) => {
const tempFilePath =
res && res.tempFiles && res.tempFiles[0] ? res.tempFiles[0].tempFilePath : '';
if (!tempFilePath) return;
this._confirmAndUploadMaterial(title, tempFilePath, registrationId);
}
});
}
}
});
} else {
wx.showActionSheet({
itemList: ['从相册选择', '拍照'],
success: (r) => {
if (!r || typeof r.tapIndex !== 'number') return;
const sourceType = r.tapIndex === 1 ? ['camera'] : ['album'];
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType,
success: (res) => {
const tempFilePath =
res && res.tempFiles && res.tempFiles[0] ? res.tempFiles[0].tempFilePath : '';
if (!tempFilePath) return;
wx.showModal({
title,
content: '确认上传该图片?',
cancelText: '取消',
confirmText: '确定',
success: (mr) => {
if (!mr || !mr.confirm) return;
// TODO: 接入付款凭证上传接口
wx.showToast({ title: '已选择', icon: 'success' });
}
});
}
});
}
});
}
},
_confirmAndUploadMaterial(title, tempFilePath, registrationId) {
wx.showModal({
title,
content: '确认上传该文件?',
cancelText: '取消',
confirmText: '确定',
success: (mr) => {
console.log('用户选择:', mr);
if (!mr || !mr.confirm) return;
wx.showLoading({ title: '上传中...', mask: true });
auth.uploadFile(tempFilePath)
.then((url) => {
return request({
path: API.uploadMaterial,
method: 'POST',
needAuth: true,
data: { registrationId, materialUrl: url }
});
})
.then(() => {
wx.showToast({ title: '上传成功', icon: 'success' });
})
.catch((err) => {
wx.showToast({ title: (err && err.message) || '上传失败', icon: 'none' });
});
}
});
},
loadEvents(reset) {
if (this.data.loading) return Promise.resolve();
if (reset) {
this.setData({
rows: [],
pageNo: 1,
hasMore: true
});
}
this.setData({ loading: true, isFirstLoad: !!reset });
const pageNo = reset ? 1 : this.data.pageNo;
const pageSize = this.data.pageSize;
const status = this.data.status;
const keyword = this.data.keyword;
const query = {
page: pageNo,
pagesize: pageSize,
PageNo: pageNo,
PageSize: pageSize,
keyword,
Keyword: keyword
};
if (status >= 0) {
query.status = status;
query.Status = status;
}
return request({
path: API.eventList,
method: 'GET',
needAuth: false,
data: query
})
.then((res) => {
const rows = (res && Array.isArray(res.rows) ? res.rows : []).map((item) => {
const id = item.id;
const rawEventStatus =
item.eventStatus != null
? item.eventStatus
: item.EventStatus != null
? item.EventStatus
: item.status != null
? item.status
: item.Status;
return {
...item,
rowKey: `e-${id}`,
registrationStartText: formatDateTime(item.registrationStartTime),
registrationEndText: formatDateTime(item.registrationEndTime),
matchStartText: formatDateTime(item.matchStartTime),
matchEndText: formatDateTime(item.matchEndTime),
eventStatusText: eventStatusText(rawEventStatus),
eventStatusPill: eventStatusForPill(rawEventStatus),
eventFormatText: eventFormatText(item.eventType || item.eventFormat)
};
});
const nextRows = reset ? rows : this.data.rows.concat(rows);
const pages = res && typeof res.pages === 'number' ? res.pages : 1;
const hasMore = pageNo < pages && rows.length > 0;
this.setData({
rows: nextRows,
hasMore,
loading: false,
isFirstLoad: false,
loadingText: hasMore ? '加载中...' : '没有更多了'
});
})
.catch((err) => {
this.setData({
loading: false,
isFirstLoad: false,
hasMore: false,
rows: reset ? [] : this.data.rows,
loadingText: err && err.message ? err.message : '加载失败'
});
});
},
loadMyEvents(reset) {
if (!this.data.loggedIn) {
this.setData({ rows: [], loading: false });
return Promise.resolve();
}
if (this.data.loading) return Promise.resolve();
if (this.data.isReferee) return this.loadMyRefereeEvents(reset);
if (reset) {
this.setData({
rows: [],
pageNo: 1,
hasMore: true
});
}
this.setData({ loading: true, isFirstLoad: !!reset });
const pageNo = reset ? 1 : this.data.pageNo;
const pageSize = this.data.pageSize;
return request({
path: API.eventMyList,
method: 'GET',
needAuth: true,
data: {
page: pageNo,
pagesize: pageSize,
PageNo: pageNo,
PageSize: pageSize
}
})
.then((res) => {
const rows = (res && Array.isArray(res.rows) ? res.rows : []).map((item) => {
const eid = item.eventId;
const rid = item.registrationId;
const rawEventStatus =
item.eventStatus != null
? item.eventStatus
: item.EventStatus != null
? item.EventStatus
: item.status != null
? item.status
: item.Status;
const eventStatusPill = eventStatusForPill(rawEventStatus);
const matStart =
item.materialUploadStartTime ||
item.MaterialUploadStartTime ||
item.uploadMaterialStartTime ||
item.UploadMaterialStartTime;
const matEnd =
item.materialUploadEndTime ||
item.MaterialUploadEndTime ||
item.uploadMaterialEndTime ||
item.UploadMaterialEndTime;
const matDeadlineText =
item.materialUploadDeadlineText || item.MaterialUploadDeadlineText || '';
const matStartFmt = formatDateTime(matStart);
const matEndFmt = formatDateTime(matEnd);
let materialRangeText = matDeadlineText;
if (!materialRangeText) {
if (matStartFmt && matStartFmt !== '-' && matEndFmt && matEndFmt !== '-') {
materialRangeText = `${matStartFmt} ~ ${matEndFmt}`;
} else if (matStartFmt && matStartFmt !== '-') {
materialRangeText = matStartFmt;
} else if (matEndFmt && matEndFmt !== '-') {
materialRangeText = matEndFmt;
}
}
return {
id: eid,
rowKey: `m-${rid}`,
registrationId: rid,
eventName: item.eventName,
coverImage: item.coverImage,
address: '',
isNeedMaterial: !!item.isNeedMaterial,
materialRangeText,
matStartTime: matStart ? new Date(matStart).getTime() : null,
matEndTime: matEnd ? new Date(matEnd).getTime() : null,
eventStatusPill,
eventStatusText: eventStatusText(rawEventStatus) || '已报名',
registrationTimeText: formatDateTime(item.registrationTime),
projectCount: item.projectCount,
eventFormatText: '',
paymentStatus: item.paymentStatus != null ? Number(item.paymentStatus) : null,
canCancel: canCancelByEventStatus(rawEventStatus)
};
});
const nextRows = reset ? rows : this.data.rows.concat(rows);
const pages = res && typeof res.pages === 'number' ? res.pages : 1;
const hasMore = pageNo < pages && rows.length > 0;
this.setData({
rows: nextRows,
hasMore,
loading: false,
isFirstLoad: false,
loadingText: hasMore ? '加载中...' : '没有更多了'
});
})
.catch(() => {
this.setData({
loading: false,
isFirstLoad: false,
hasMore: false,
rows: reset ? [] : this.data.rows,
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' });
});
}
});