2026-04-10 16:10:33 +08:00

553 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, eventFormatText } = require('../../utils/format');
/** 接口未返回时的 UI 兜底(与产品稿一致,有真实数据后自动以接口为准) */
var DETAIL_UI_FALLBACK = {
eventTitle: '2025-2026学年上海市中小学生跳绳锦标赛',
eventFormatTag: '线下',
registrationMethodText: '只允许个人报名',
feeModeLabel: '定价模式(赛事)',
priceDisplay: 892,
registrationStartText: '2015-10-02 09:21:00',
registrationEndText: '2015-10-10 18:00:00',
matchStartText: '2015-10-02 09:21:00',
matchEndText: '2015-10-10 18:00:00',
materialRangeText: '2015-10-02 09:21:00 ~ 2015-10-10 18:00:00',
uploadMaterialRemark:
'上传素材备注备',
eventAddress: '上海市杨浦区307号',
refundRulesText: '开赛前7天扣除金额5%,3-7天扣除金额10%,3天内扣除金额20%',
participationPointsLabel: '5积分',
rankingPointsLines: ['1--8 15积分', '9--20 10积分'],
registrationLimitLines: [
'每人限报个人项目2个,团队项目3个',
'项目-组别允许同一个个人/单位最大报名数量:30秒单摇2个,2*30秒双摇接力1个,30秒单摇2个'
],
introductionText:
'这是一场比赛这是一场比赛这是一场比赛这是一场比赛这是一场比赛这是一场比赛这是一场比赛这是一场比赛这是一场比赛这是一场比赛这是一场比赛这是一场比.赛这是一场比赛这是一场比赛这是一场'
};
var DEMO_PROJECT_TABLE_ROWS = [
{ rowKey: 'fb-1', typeName: '个人', projectName: '30秒单摇', groupName: '少年组', fee: '23' },
{ rowKey: 'fb-2', typeName: '团队', projectName: '少年儿童组', groupName: '成年组', fee: '411' },
{ rowKey: 'fb-3', typeName: '团队', projectName: '少年儿童组', groupName: '32', fee: '32' },
{ rowKey: 'fb-4', typeName: '团队', projectName: '成年组', groupName: '12', fee: '12' }
];
function isEmptyDisplayTime(s) {
return s == null || String(s).trim() === '' || String(s).trim() === '-';
}
function pick(obj, keys) {
if (!obj) return null;
for (let i = 0; i < keys.length; i++) {
const v = obj[keys[i]];
if (v != null && v !== '') return v;
}
return null;
}
/**
* 赛事状态(与后端枚举一致)
* 0 NotStarted 未开始 | 1 Enrolling 报名中 | 2 RegistrationEnded 报名结束
* 3 Ongoing 比赛中 | 4 Ended 已结束
* 未返回字段时视为旧接口,不拦截报名
*/
function parseEventStatus(rawDetail) {
const v = pick(rawDetail, ['eventStatus', 'EventStatus']);
if (v == null || v === '') return null;
const n = Number(v);
return Number.isNaN(n) ? null : n;
}
function applyUiFromEventStatus(es) {
if (es == null) {
return { canApply: true, applyButtonText: '报名' };
}
if (es === 0) {
return { canApply: false, applyButtonText: '未开始' };
}
if (es === 1) {
return { canApply: true, applyButtonText: '报名' };
}
if (es === 2) {
return { canApply: false, applyButtonText: '报名结束' };
}
if (es === 3) {
return { canApply: false, applyButtonText: '比赛中' };
}
if (es === 4) {
return { canApply: false, applyButtonText: '已结束' };
}
return { canApply: false, applyButtonText: '暂不可报名' };
}
function toastForEventStatus(es) {
if (es === 0) return '赛事未开始';
if (es === 2) return '报名已结束';
if (es === 3) return '比赛进行中,暂不可报名';
if (es === 4) return '赛事已结束';
return '当前不可报名';
}
function registrationModeText(mode) {
const m = mode === undefined || mode === null ? null : Number(mode);
if (m === 1) return '只允许个人报名';
if (m === 2) return '只允许团队报名';
if (m === 0 || m === 3) return '个人与团队均可报名';
return null;
}
function formatRange(start, end) {
const a = formatDateTime(start);
const b = formatDateTime(end);
if (a === '-' && b === '-') return '';
if (a === '-') return b;
if (b === '-') return a;
return `${a} ~ ${b}`;
}
function splitLines(text) {
if (!text || typeof text !== 'string') return [];
return text
.split(/\r?\n/)
.map((s) => s.trim())
.filter(Boolean);
}
function normalizePointsRuleDetail(raw) {
const d = raw && typeof raw === 'object' ? raw : null;
if (!d) return null;
const detailsRaw = Array.isArray(d.details) ? d.details : Array.isArray(d.Details) ? d.Details : [];
const details = detailsRaw
.map((x) => {
const startRank = toNonNegInt(pick(x, ['startRank', 'StartRank']));
const endRank = toNonNegInt(pick(x, ['endRank', 'EndRank']));
const points = toNonNegInt(pick(x, ['points', 'Points']));
if (startRank == null && endRank == null && points == null) return null;
return {
startRank: startRank != null ? startRank : endRank,
endRank: endRank != null ? endRank : startRank,
points
};
})
.filter(Boolean);
return {
ruleName: pick(d, ['ruleName', 'RuleName']) || '',
participationPoints: pick(d, ['participationPoints', 'ParticipationPoints']),
details
};
}
function toNonNegInt(v) {
const n = Number(v);
if (Number.isNaN(n)) return null;
const i = Math.floor(n);
return i >= 0 ? i : null;
}
function buildRegistrationLimitLines(detail, projects) {
const d = detail && typeof detail === 'object' ? detail : {};
const lines = [];
const individualLimit = toNonNegInt(pick(d, ['individualProjectLimit', 'IndividualProjectLimit']));
const teamLimit = toNonNegInt(pick(d, ['teamProjectLimit', 'TeamProjectLimit']));
if (individualLimit != null || teamLimit != null) {
const a = individualLimit != null ? `个人项目${individualLimit}个` : null;
const b = teamLimit != null ? `团队项目${teamLimit}个` : null;
const joined = [a, b].filter(Boolean).join(',');
if (joined) lines.push(`每人/单位限报:${joined}`);
}
if (Array.isArray(projects) && projects.length) {
const segs = [];
projects.forEach((p) => {
const name = p && (p.name || p.Name) ? (p.name || p.Name) : '';
const limit = toNonNegInt(pick(p, ['projectLimitCount', 'ProjectLimitCount']));
if (!name || limit == null) return;
segs.push(`${name}${limit}个`);
});
if (segs.length) {
lines.push(`项目组别单位报名上限(每组别):${segs.join(',')}`);
}
}
return lines;
}
function flattenProjectTableRows(projects) {
if (!Array.isArray(projects)) return [];
const rows = [];
projects.forEach((p) => {
const pid = p.id != null ? p.id : p.Id;
const rawGroups = Array.isArray(p.groups) ? p.groups : Array.isArray(p.Groups) ? p.Groups : [];
const groups = rawGroups.length ? rawGroups : [null];
groups.forEach((g, gi) => {
const gid = g && (g.groupId != null ? g.groupId : g.GroupId != null ? g.GroupId : null);
const gname = g && (g.groupName || g.GroupName);
rows.push({
rowKey: `p-${pid}-${gi}-${gid != null ? gid : gname || 'g'}`,
typeName: p.typeName || p.TypeName || '-',
projectName: p.name || p.Name || '-',
groupName: g ? gname || gid || '-' : '-',
fee: p.fee != null ? p.fee : p.Fee != null ? p.Fee : '-'
});
});
});
return rows;
}
function normalizeEventDetail(raw) {
const d = raw && typeof raw === 'object' ? raw : {};
const projects = Array.isArray(d.projects) ? d.projects : Array.isArray(d.Projects) ? d.Projects : [];
const registrationMethodText =
pick(d, [
'registrationMethodText',
'RegistrationMethodText',
'registrationMethodDescription',
'RegistrationMethodDescription'
]) || registrationModeText(pick(d, ['registrationMode', 'RegistrationMode']));
const feeModeLabel =d.chargeMode == 0 ? '免费' : d.chargeMode == 1 ? '定价模式(赛事)' : d.chargeMode == 2 ? '单价(赛事)' : '单价(项目)';
let priceDisplay = d && d.feeConfig ? d.feeConfig.price : null;
if (priceDisplay == null && projects.length) {
const fees = projects.map((p) => p.fee).filter((f) => f != null && f !== '');
if (fees.length) {
const nums = fees.map(Number).filter((n) => !Number.isNaN(n));
if (nums.length) priceDisplay = Math.min(...nums);
}
}
const materialRangeText =
pick(d, ['materialUploadDeadlineText', 'MaterialUploadDeadlineText']) ||
formatRange(
pick(d, ['materialUploadStartTime', 'MaterialUploadStartTime']),
pick(d, ['materialUploadEndTime', 'MaterialUploadEndTime'])
) ||
formatRange(
pick(d, ['uploadMaterialStartTime', 'UploadMaterialStartTime']),
pick(d, ['uploadMaterialEndTime', 'UploadMaterialEndTime'])
);
const refundRulesText = pick(d, ['refundRules', 'RefundRules', 'refundDescription', 'RefundDescription']);
const pointsRuleDetailRaw = pick(d, ['pointsRuleDetail', 'PointsRuleDetail']);
const pointsRuleDetail = normalizePointsRuleDetail(pointsRuleDetailRaw);
const participationPoints = pointsRuleDetail
? pick(pointsRuleDetail, ['participationPoints', 'ParticipationPoints'])
: pick(d, ['participationPoints', 'ParticipationPoints']);
const rankingPointsRaw = pointsRuleDetail
? pointsRuleDetail.details
: pick(d, [
'rankingPointsText',
'RankingPointsText',
'rankingPointsDescription',
'RankingPointsDescription',
'rankingPoints',
'RankingPoints'
]);
let rankingPointsLines = [];
if (Array.isArray(rankingPointsRaw) && rankingPointsRaw.length && typeof rankingPointsRaw[0] === 'object') {
rankingPointsLines = rankingPointsRaw
.map((it) => {
const sr = toNonNegInt(pick(it, ['startRank', 'StartRank']));
const er = toNonNegInt(pick(it, ['endRank', 'EndRank']));
const pts = toNonNegInt(pick(it, ['points', 'Points']));
if (pts == null) return null;
const a = sr != null ? sr : er;
const b = er != null ? er : sr;
if (a == null && b == null) return `${pts}积分`;
if (a != null && b != null && a === b) return `${a} ${pts}积分`;
if (a != null && b != null) return `${a}--${b} ${pts}积分`;
return `${a != null ? a : b} ${pts}积分`;
})
.filter(Boolean);
} else if (typeof rankingPointsRaw === 'string') {
rankingPointsLines = splitLines(rankingPointsRaw);
} else if (Array.isArray(rankingPointsRaw)) {
rankingPointsLines = rankingPointsRaw.map((x) => String(x)).filter(Boolean);
} else if (rankingPointsRaw != null && rankingPointsRaw !== '') {
rankingPointsLines = [String(rankingPointsRaw)];
}
const registrationLimitRaw = pick(d, [
'registrationLimitText',
'RegistrationLimitText',
'registrationRules',
'RegistrationRules',
'limitDescription',
'LimitDescription'
]);
let registrationLimitLines = [];
if (registrationLimitRaw != null && registrationLimitRaw !== '') {
if (typeof registrationLimitRaw === 'string') {
registrationLimitLines = splitLines(registrationLimitRaw);
} else if (Array.isArray(registrationLimitRaw)) {
registrationLimitLines = registrationLimitRaw.filter((x) => x != null && String(x).trim() !== '');
} else {
registrationLimitLines = [String(registrationLimitRaw)];
}
}
// 若接口未提供文本规则,则由结构化字段拼接
if (!registrationLimitLines.length) {
registrationLimitLines = buildRegistrationLimitLines(d, projects);
}
const participationPointsLabel =
pick(d, ['participationPointsText', 'ParticipationPointsText']) ||
(participationPoints != null && participationPoints !== ''
? `${participationPoints}积分`
: '');
const introImage = pick(d, ['introductionImage', 'IntroductionImage', 'detailImage', 'DetailImage']);
return {
detail: d,
projects,
eventFormatTag: eventFormatText(d.eventType || d.eventFormat || d.EventType || d.EventFormat),
registrationMethodText,
feeModeLabel: feeModeLabel || '定价模式(赛事)',
priceDisplay,
materialRangeText,
refundRulesText,
participationPointsLabel,
rankingPointsLines,
registrationLimitLines,
introImage,
hasNoticeBlock: Boolean(refundRulesText || participationPointsLabel || rankingPointsLines.length),
hasRegistrationLimit: registrationLimitLines.length > 0,
projectTableRows: flattenProjectTableRows(projects)
};
}
Page({
data: {
id: null,
eventDetail: {},
projects: [],
registrationStartText: '',
registrationEndText: '',
matchStartText: '',
matchEndText: '',
eventFormatTag: '',
registrationMethodText: '',
feeModeLabel: '',
priceDisplay: null,
materialRangeText: '',
refundRulesText: '',
participationPointsLabel: '',
rankingPointsLines: [],
registrationLimitLines: [],
introImage: '',
introductionText: '',
uploadMaterialRemark: '',
eventTitle: '',
eventAddress: '',
heroImage: '',
hasNoticeBlock: false,
hasRegistrationLimit: false,
projectTableRows: [],
unitGroups: [],
/** 0未开始 1报名中 2报名结束 3比赛中 4已结束;null 表示接口未返回 */
eventStatus: null,
canApply: true,
applyButtonText: '报名'
},
onLoad(options) {
console.log('Detail onLoad options:', options);
let rawId = options.id;
// 处理扫描普通二维码进入:options.q 为 URL 编码后的链接
// 格式:https://jitsi-web.yuedongsports.com/mini-app/event?id=xxx
if (!rawId && options.q) {
try {
const decoded = decodeURIComponent(options.q);
const match = decoded.match(/[?&]id=([^&]+)/);
console.log('Detail onLoad options:', decoded);
console.log('Detail onLoad options:', match);
if (match) rawId = match[1];
} catch (e) {
console.warn('Detail: 解析 options.q 失败', e);
}
}
const id = Number(rawId);
this.setData({ id });
if (!id) return;
this.fetchDetail(id);
},
fetchDetail(id) {
request({
path: API.eventDetail(id),
method: 'GET',
needAuth: true
})
.then((res) => {
const detail = res || {};
const projects = Array.isArray(detail.projects) ? detail.projects : [];
const normalizedProjects = projects.map((p) => {
const groups = Array.isArray(p.groups)
? p.groups
: Array.isArray(p.Groups)
? p.Groups
: [];
const groupSummary = groups.length
? groups
.map((g) => g.groupName || g.groupId)
.filter(Boolean)
.join(',')
: '';
return { ...p, groupSummary };
});
const n = normalizeEventDetail({ ...detail, projects: normalizedProjects });
const fb = DETAIL_UI_FALLBACK;
var introductionText =
pick(detail, ['eventIntroduction', 'EventIntroduction', 'introduction', 'Introduction']) || '';
if (!introductionText.trim()) introductionText = fb.introductionText;
var eventTitle = pick(detail, ['eventName', 'EventName']) || '';
if (!eventTitle.trim()) eventTitle = fb.eventTitle;
var eventAddress = pick(detail, ['address', 'Address']) || '';
if (!eventAddress.trim()) eventAddress = fb.eventAddress;
var heroImage = pick(detail, ['coverImage', 'CoverImage']) || '';
var registrationStartText = formatDateTime(
pick(detail, ['registrationStartTime', 'RegistrationStartTime'])
);
var registrationEndText = formatDateTime(
pick(detail, ['registrationEndTime', 'RegistrationEndTime'])
);
if (isEmptyDisplayTime(registrationStartText)) registrationStartText = fb.registrationStartText;
if (isEmptyDisplayTime(registrationEndText)) registrationEndText = fb.registrationEndText;
var matchStartText = formatDateTime(pick(detail, ['matchStartTime', 'MatchStartTime']));
var matchEndText = formatDateTime(pick(detail, ['matchEndTime', 'MatchEndTime']));
if (isEmptyDisplayTime(matchStartText)) matchStartText = fb.matchStartText;
if (isEmptyDisplayTime(matchEndText)) matchEndText = fb.matchEndText;
var eventFormatTag = n.eventFormatTag || fb.eventFormatTag;
var registrationMethodText = n.registrationMethodText || fb.registrationMethodText;
var feeModeLabel = n.feeModeLabel || fb.feeModeLabel;
var priceDisplay = n.priceDisplay != null ? n.priceDisplay : fb.priceDisplay;
var materialRangeText = n.materialRangeText || fb.materialRangeText;
var uploadMaterialRemark =
pick(detail, [
'uploadMaterialRemark',
'UploadMaterialRemark',
'materialRemark',
'MaterialRemark'
]) || '';
if (!uploadMaterialRemark.trim()) uploadMaterialRemark = fb.uploadMaterialRemark;
var refundRulesText = n.refundRulesText || fb.refundRulesText;
var participationPointsLabel = n.participationPointsLabel || fb.participationPointsLabel;
var rankingPointsLines =
n.rankingPointsLines && n.rankingPointsLines.length ? n.rankingPointsLines : fb.rankingPointsLines;
var registrationLimitLines =
n.registrationLimitLines && n.registrationLimitLines.length
? n.registrationLimitLines
: fb.registrationLimitLines;
var projectTableRows =
n.projectTableRows && n.projectTableRows.length ? n.projectTableRows : DEMO_PROJECT_TABLE_ROWS;
var hasNoticeBlock = Boolean(
refundRulesText || participationPointsLabel || rankingPointsLines.length
);
var hasRegistrationLimit = registrationLimitLines.length > 0;
// 解析 units 数组,按 unitType 分组
// unitType: 0=主办单位 1=承办单位 2=协办单位 3=赞助单位
const UNIT_TYPE_LABELS = ['主办单位', '承办单位', '协办单位', '赞助单位'];
const rawUnits = Array.isArray(detail.units)
? detail.units
: Array.isArray(detail.Units)
? detail.Units
: [];
const unitMap = {};
rawUnits.forEach((u) => {
const type = u.unitType != null ? u.unitType : u.UnitType != null ? u.UnitType : null;
const name = u.unitName || u.UnitName || u.name || u.Name || '';
if (type == null || !name.trim()) return;
const t = Number(type);
if (!unitMap[t]) unitMap[t] = [];
unitMap[t].push(name.trim());
});
const unitGroups = [0, 1, 2, 3]
.filter((t) => unitMap[t] && unitMap[t].length)
.map((t) => ({ label: UNIT_TYPE_LABELS[t], names: unitMap[t] }));
const eventStatus = parseEventStatus(detail);
const { canApply, applyButtonText } = applyUiFromEventStatus(eventStatus);
this.setData({
eventDetail: detail,
projects: normalizedProjects,
introductionText,
eventTitle,
eventAddress,
heroImage,
registrationStartText,
registrationEndText,
matchStartText,
matchEndText,
eventFormatTag,
registrationMethodText,
feeModeLabel,
priceDisplay,
materialRangeText,
uploadMaterialRemark,
refundRulesText,
participationPointsLabel,
rankingPointsLines,
registrationLimitLines,
introImage: n.introImage || '',
hasNoticeBlock,
hasRegistrationLimit,
projectTableRows,
unitGroups,
eventStatus,
canApply,
applyButtonText
});
})
.catch((err) => {
// wx.showToast({ title: err.Error, icon: 'none' });
// console.log('Detail fetchDetail error:', err);
this.setData({
canApply: false,
applyButtonText: '暂不可报名'
});
});
},
onApplyTap() {
const id = this.data.id;
if (!id) return;
if (!this.data.canApply) {
wx.showToast({ title: toastForEventStatus(this.data.eventStatus), icon: 'none' });
return;
}
wx.navigateTo({
url: `/pages/apply/apply?eventId=${id}`
});
}
});