2026-09-09 22:05:00 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* ActivityCard 活动卡片(规范 §43-46)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 不变量(AGENTS.md 第 1 条):feeType=paid 仅作展示字段,前端不实现任何支付入口。
|
|
|
|
|
|
* "公益免费"作为主要视觉标识突出展示。
|
|
|
|
|
|
*/
|
2026-09-17 14:02:00 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 活动类型 → 中文名。
|
|
|
|
|
|
*
|
|
|
|
|
|
* <p><b>Phase 14</b>:键必须**逐字等于**后端 {@code activities.activity_type}
|
|
|
|
|
|
* (生产约束 {@code chk_activity_type} 白名单)。守卫会解析真实 DDL 校验。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const ACTIVITY_TYPE_LABELS = {
|
|
|
|
|
|
matchmaking: '公益联谊',
|
|
|
|
|
|
interest_social: '兴趣社交',
|
|
|
|
|
|
relationship_education: '婚恋课堂',
|
|
|
|
|
|
premarital_guidance: '婚前辅导',
|
|
|
|
|
|
marriage_custom_culture: '婚俗文化',
|
|
|
|
|
|
anti_fraud: '反诈宣传',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 按类型的**差异化资格提示**(Gate 要求:matchmaking 与课堂/反诈类不同)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* <p>相亲类接触陌生人 → 实名 + 本地资格 + 性别名额;
|
|
|
|
|
|
* 婚恋课堂 / 婚俗文化 / 反诈宣传是公益科普 → 不设实名与本地资格门槛。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const ACTIVITY_TYPE_ELIGIBILITY = {
|
|
|
|
|
|
matchmaking: '需实名认证 · 丹阳本地资格',
|
|
|
|
|
|
premarital_guidance: '需实名认证',
|
|
|
|
|
|
interest_social: '按男女名额分别报名',
|
|
|
|
|
|
relationship_education: '公益科普 · 免费参与',
|
|
|
|
|
|
marriage_custom_culture: '公益科普 · 免费参与',
|
|
|
|
|
|
anti_fraud: '公益科普 · 免费参与',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-09 22:05:00 +08:00
|
|
|
|
Component({
|
|
|
|
|
|
options: { addGlobalClass: true },
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
activity: { type: Object, value: null },
|
|
|
|
|
|
showStatusBadge: { type: Boolean, value: true },
|
|
|
|
|
|
compact: { type: Boolean, value: false }, // 紧凑模式(首页推荐位)
|
|
|
|
|
|
},
|
2026-09-17 14:02:00 +08:00
|
|
|
|
data: {
|
|
|
|
|
|
typeLabel: '',
|
|
|
|
|
|
eligibilityText: '',
|
|
|
|
|
|
registeredCount: 0,
|
|
|
|
|
|
capacity: 0,
|
|
|
|
|
|
cover: '',
|
|
|
|
|
|
startAtText: '',
|
|
|
|
|
|
},
|
|
|
|
|
|
observers: {
|
|
|
|
|
|
/** 活动对象变化时重算派生展示字段(列表复用节点时 observer 同样触发) */
|
|
|
|
|
|
'activity': function (activity) {
|
|
|
|
|
|
if (!activity) {
|
|
|
|
|
|
this.setData({
|
|
|
|
|
|
typeLabel: '', eligibilityText: '', registeredCount: 0,
|
|
|
|
|
|
capacity: 0, cover: '', startAtText: '',
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const type = activity.activityType || '';
|
|
|
|
|
|
// Phase 14 修复:原 wxml 读 registeredCount / currentMaleCount / currentFemaleCount,
|
|
|
|
|
|
// 后端三个字段都不存在 → "报名人数" 恒显示 0。改为后端真实字段 + 本地兜底。
|
|
|
|
|
|
const registered =
|
|
|
|
|
|
typeof activity.registeredCount === 'number'
|
|
|
|
|
|
? activity.registeredCount
|
|
|
|
|
|
: (activity.maleCount || 0) + (activity.femaleCount || 0);
|
|
|
|
|
|
this.setData({
|
|
|
|
|
|
typeLabel: ACTIVITY_TYPE_LABELS[type] || '',
|
|
|
|
|
|
eligibilityText: ACTIVITY_TYPE_ELIGIBILITY[type] || '',
|
|
|
|
|
|
registeredCount: registered,
|
|
|
|
|
|
// 容量只认 totalLimit —— 原实现还有 activity.capacity 回退,但 DTO 并无该字段
|
|
|
|
|
|
capacity: activity.totalLimit || 0,
|
|
|
|
|
|
cover: buildCover(activity),
|
|
|
|
|
|
startAtText: formatStartAt(activity.startAt),
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-09-09 22:05:00 +08:00
|
|
|
|
methods: {
|
|
|
|
|
|
onTap() {
|
|
|
|
|
|
if (!this.data.activity) return;
|
2026-09-17 14:02:00 +08:00
|
|
|
|
this.triggerEvent('tap', { activityId: this.data.activity.activityId });
|
2026-09-09 22:05:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-09-17 14:02:00 +08:00
|
|
|
|
|
|
|
|
|
|
/** 封面地址:优先后端给定 URL,否则按 uploadBase + /files/{coverFileId} 拼接 */
|
|
|
|
|
|
function buildCover(activity) {
|
|
|
|
|
|
if (activity.coverUrl) return activity.coverUrl;
|
|
|
|
|
|
if (!activity.coverFileId) return '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const app = getApp();
|
|
|
|
|
|
const base = (app && app.globalData && app.globalData.uploadBase) || '';
|
|
|
|
|
|
return base ? `${base}/files/${activity.coverFileId}` : '';
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 开始时间:后端返回 ISO 串,转成「M月D日 HH:mm」 */
|
|
|
|
|
|
function formatStartAt(raw) {
|
|
|
|
|
|
if (!raw) return '';
|
|
|
|
|
|
const d = new Date(typeof raw === 'string' ? raw.replace(/-/g, '/').replace(/T/, ' ').slice(0, 19) : raw);
|
|
|
|
|
|
if (isNaN(d.getTime())) return String(raw);
|
|
|
|
|
|
const pad = (n) => (n < 10 ? `0${n}` : `${n}`);
|
|
|
|
|
|
return `${d.getMonth() + 1}月${d.getDate()}日 ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
|
|
|
|
}
|