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

423 lines
13 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 auth = require('../../../utils/auth');
const { ROLE, getProfileRole } = require('../../../utils/role');
const PHONE_RE = /^1[3456789]\d{9}$/;
function normalizeAuthStatus(v) {
if (v == null || v === '') return 0;
const n = Number(v);
return Number.isNaN(n) ? 0 : n;
}
/** 0 未认证 | 1 审核中 | 2 审核通过 | 3 审核失败 | 4 已取消 */
function authStatusText(status) {
const map = {
0: '未认证',
1: '审核中',
2: '审核通过',
3: '审核失败',
4: '已取消'
};
return map[normalizeAuthStatus(status)] || '未认证';
}
function canSubmitAuth(status) {
const s = normalizeAuthStatus(status);
return s === 0 || s === 3;
}
/** 仅 0 未认证 显示「提交认证」,仅 3 审核失败 显示「重新提交认证」 */
function submitAuthButtonText(status) {
return normalizeAuthStatus(status) === 0 ? '提交认证' : '重新提交认证';
}
/** authStatus 以 /api/my/profile 为准;profile 无字段时再尝试认证详情接口 */
function pickAuthStatus(profile, authDetail) {
const p = profile && typeof profile === 'object' ? profile : {};
const d = authDetail && typeof authDetail === 'object' ? authDetail : {};
const v =
p.authStatus != null
? p.authStatus
: p.AuthStatus != null
? p.AuthStatus
: d.authStatus != null
? d.authStatus
: d.AuthStatus;
return normalizeAuthStatus(v);
}
Page({
data: {
activeTab: 1, // 1=身份证 2=护照
// 上传图片 URL(远程)
idCardImageUrl: '',
idCardBackImageUrl: '',
passportImageUrl: '',
householdImageUrl: '',
// 识别字段(只读,OCR 自动填充)
name: '',
idCardNumber: '',
birthDate: '',
genderOptions: [
{ value: 0, label: '男' },
{ value: 1, label: '女' }
],
genderIndex: 0,
// 护照识别额外字段(只读)
passportNumber: '',
nationality: '',
issueDate: '',
expiryDate: '',
// 联系方式
mobile: '',
verificationCode: '',
// UI 状态
recognizing: false,
submitting: false,
codeSending: false,
countdown: 0,
ocrDone: false,
authStatus: 0,
authStatusText: '未认证',
canSubmit: true,
submitBtnText: '提交认证',
loading: true,
isReferee: false
},
_timer: null,
onLoad() {
this.loadAuth();
},
onShow() {
const accessToken = wx.getStorageSync('accessToken') || '';
if (!accessToken) {
wx.redirectTo({ url: '/subpackages/mine/login/login' });
}
},
onUnload() {
this._clearTimer();
},
onPullDownRefresh() {
this.loadAuth().finally(() => wx.stopPullDownRefresh());
},
onBack() {
if (getCurrentPages().length > 1) {
wx.navigateBack({ delta: 1 });
} else {
wx.switchTab({ url: '/pages/mine/mine' });
}
},
onSwitchTab(e) {
const tab = Number(e.currentTarget.dataset.tab);
if (tab === this.data.activeTab) return;
this.setData({
activeTab: tab,
verificationCode: '',
ocrDone: false,
name: '', idCardNumber: '', birthDate: '', genderIndex: 0,
passportNumber: '', nationality: '', issueDate: '', expiryDate: '',
idCardImageUrl: '', idCardBackImageUrl: '', passportImageUrl: '', householdImageUrl: ''
});
this.loadAuth();
},
loadAuth() {
this.setData({ loading: true });
return Promise.all([
auth.fetchMyAuth().catch(() => ({})),
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 || '';
}
const name = d.name || d.realName || '';
const mobile = d.mobile || d.phone || '';
const idCardNumber = d.idCardNumber || d.idNumber || '';
const idCardImageUrl = d.idCardImageUrl || d.idCardFrontImageUrl || '';
const idCardBackImageUrl = d.idCardBackImageUrl || '';
const passportImageUrl = d.passportPhotoUrl || d.passportImageUrl || '';
const householdImageUrl = d.householdPhotoUrl || d.householdImageUrl || '';
const birthDate = d.birthDate || '';
const rawGender = d.gender != null ? String(d.gender) : '';
const genderIndex = (rawGender === '1' || rawGender === 'F' || rawGender === '女') ? 1 : 0;
const ocrDone = Boolean(name || idCardNumber || passportImageUrl || householdImageUrl);
const authStatus = pickAuthStatus(profile, d);
const canSubmit = canSubmitAuth(authStatus);
const submitBtnText = submitAuthButtonText(authStatus);
const authStatusTextVal = authStatusText(authStatus);
const activeTab = Number(d.authType) && Number(d.authType) > 0 ? Number(d.authType) : this.data.activeTab;
const passportNumber = d.passportNumber || d.passportNo || '';
this.setData({
activeTab,
passportNumber,
name, mobile, idCardNumber,
idCardImageUrl, idCardBackImageUrl, passportImageUrl, householdImageUrl,
birthDate, genderIndex, ocrDone,
authStatus,
authStatusText: authStatusTextVal,
canSubmit,
submitBtnText,
isReferee
});
}).finally(() => {
this.setData({ loading: false });
});
},
onChooseImage(e) {
if (this.data.authStatus === 2) return;
const kind = e.currentTarget.dataset.kind;
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFiles && res.tempFiles[0] ? res.tempFiles[0].tempFilePath : '';
if (!tempFilePath) return;
this._uploadAndOcr(tempFilePath, kind);
}
});
},
async _uploadAndOcr(tempFilePath, kind) {
wx.showLoading({ title: '上传识别中...', mask: true });
this.setData({ recognizing: true });
try {
const imageUrl = await auth.uploadFile(tempFilePath);
if (kind === 'idFront') this.setData({ idCardImageUrl: imageUrl });
else if (kind === 'idBack') this.setData({ idCardBackImageUrl: imageUrl });
else if (kind === 'passport') this.setData({ passportImageUrl: imageUrl });
else if (kind === 'household') this.setData({ householdImageUrl: imageUrl });
if (kind === 'idFront') {
const ocr = await auth.ocrIdCard(imageUrl);
if (ocr && ocr.success === false) {
wx.showToast({ title: ocr.message || '身份证识别失败', icon: 'none' });
return;
}
const genderIndex = (ocr && (ocr.gender === '女' || ocr.gender === 'F' || ocr.gender === '1')) ? 1 : 0;
this.setData({
name: (ocr && ocr.name) || '',
idCardNumber: (ocr && ocr.idCardNumber) || '',
birthDate: (ocr && ocr.birthDate) || '',
genderIndex,
ocrDone: true
});
wx.showToast({ title: '识别成功', icon: 'success' });
} else if (kind === 'passport') {
const ocr = await auth.ocrPassport(imageUrl);
if (ocr && ocr.success === false) {
wx.showToast({ title: ocr.message || '护照识别失败', icon: 'none' });
return;
}
const genderIndex = (ocr && (ocr.gender === '女' || ocr.gender === 'F' || ocr.gender === '1')) ? 1 : 0;
this.setData({
name: (ocr && ocr.name) || '',
passportNumber: (ocr && ocr.passportNumber) || '',
nationality: (ocr && ocr.nationality) || '',
birthDate: (ocr && (ocr.birthDateYmd || ocr.birthDate)) || '',
issueDate: (ocr && (ocr.issueDate || ocr.issueDateYmd)) || '',
expiryDate: (ocr && (ocr.expiryDate || ocr.validToDate)) || '',
genderIndex,
ocrDone: true
});
wx.showToast({ title: '识别成功', icon: 'success' });
} else if (kind === 'household') {
const ocr = await auth.ocrHousehold(imageUrl);
if (ocr && ocr.success === false) {
wx.showToast({ title: ocr.message || '户口本识别失败', icon: 'none' });
return;
}
const genderIndex = (ocr && (ocr.gender === '女' || ocr.gender === 'F' || ocr.gender === '1')) ? 1 : 0;
this.setData({
name: (ocr && ocr.name) || '',
idCardNumber: (ocr && ocr.idCardNumber) || '',
birthDate: (ocr && ocr.birthDate) || '',
genderIndex,
ocrDone: true
});
wx.showToast({ title: '识别成功', icon: 'success' });
} else {
wx.showToast({ title: '上传成功', icon: 'success' });
}
} catch (err) {
wx.showToast({ title: (err && err.message) || '识别失败', icon: 'none' });
} finally {
this.setData({ recognizing: false });
// wx.hideLoading();
}
},
onMobileInput(e) {
this.setData({ mobile: (e.detail.value || '').trim() });
},
onCodeInput(e) {
this.setData({ verificationCode: (e.detail.value || '').trim() });
},
onSendCodeTap() {
const { mobile, countdown, codeSending } = this.data;
if (codeSending || countdown > 0) return;
if (!PHONE_RE.test(mobile)) {
wx.showToast({ title: '请输入正确手机号', icon: 'none' });
return;
}
this.setData({ codeSending: true });
wx.showLoading({ title: '发送中...' });
auth
.sendVerificationCode(mobile)
.then(() => {
wx.showToast({ title: '验证码已发送', icon: 'success' });
this._startCountdown(60);
})
.catch(() => {
wx.showToast({ title: '发送失败', icon: 'none' });
})
.finally(() => {
this.setData({ codeSending: false });
// wx.hideLoading();
});
},
_startCountdown(sec) {
this._clearTimer();
this.setData({ countdown: sec });
this._timer = setInterval(() => {
const next = this.data.countdown - 1;
if (next <= 0) {
this._clearTimer();
this.setData({ countdown: 0 });
} else {
this.setData({ countdown: next });
}
}, 1000);
},
_clearTimer() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
},
onSubmitTap() {
if (this.data.submitting) return;
const {
activeTab, idCardImageUrl, idCardBackImageUrl, passportImageUrl, householdImageUrl,
name, idCardNumber, birthDate,
genderOptions, genderIndex, mobile, verificationCode, passportNumber
} = this.data;
if (!name) {
wx.showToast({ title: '请先上传证件图片完成识别', icon: 'none' });
return;
}
if (!PHONE_RE.test(mobile)) {
wx.showToast({ title: '请输入正确手机号', icon: 'none' });
return;
}
if (!verificationCode) {
wx.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
const genderValue = genderOptions[genderIndex] ? genderOptions[genderIndex].value : 0;
let payload;
if (activeTab === 1) {
if (!idCardImageUrl) {
wx.showToast({ title: '请上传身份证人像面', icon: 'none' });
return;
}
if (!idCardNumber) {
wx.showToast({ title: '请先完成身份证识别', icon: 'none' });
return;
}
payload = {
authType: 1,
name,
mobile,
smsCode: verificationCode,
gender: genderValue,
birthDate: birthDate || undefined,
idCardNumber,
idCardImageUrl,
idCardBackImageUrl
};
} else if (activeTab === 3) {
if (!householdImageUrl) {
wx.showToast({ title: '请上传户口本图片', icon: 'none' });
return;
}
if (!idCardNumber) {
wx.showToast({ title: '请先完成户口本识别', icon: 'none' });
return;
}
payload = {
authType: 3,
name,
mobile,
smsCode: verificationCode,
gender: genderValue,
birthDate: birthDate || undefined,
idCardNumber,
householdPhotoUrl: householdImageUrl
};
} else {
if (!passportImageUrl) {
wx.showToast({ title: '请上传护照', icon: 'none' });
return;
}
if (!birthDate) {
wx.showToast({ title: '请先完成护照识别', icon: 'none' });
return;
}
payload = {
authType: 2,
name,
mobile,
smsCode: verificationCode,
gender: genderValue,
birthDate,
passportPhotoUrl: passportImageUrl,
passportNumber: passportNumber
};
}
this.setData({ submitting: true });
wx.showLoading({ title: '提交中...' });
auth
.submitMyAuth(payload)
.then(() => {
wx.showToast({ title: '提交成功', icon: 'success' });
this.loadAuth();
setTimeout(() => wx.navigateBack({ delta: 1 }), 1500);
})
.catch((err) => {
wx.showToast({ title: (err && err.message) || '提交失败', icon: 'none' });
})
.finally(() => {
this.setData({ submitting: false });
// wx.hideLoading();
});
}
});