update:首页&取消报名&上传

This commit is contained in:
ltx 2026-04-08 17:32:03 +08:00
parent 253c27db4b
commit b408c873eb
11 changed files with 525 additions and 9 deletions

View File

@ -45,7 +45,8 @@
"team/team",
"team-detail/team-detail",
"order/order",
"order-detail/order-detail"
"order-detail/order-detail",
"cancel-registration/cancel-registration"
]
}
],

View File

@ -15,7 +15,7 @@ var DETAIL_UI_FALLBACK = {
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积分',

View File

@ -40,6 +40,15 @@ function normalizeBannerItem(raw) {
};
}
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',
@ -201,6 +210,62 @@ Page({
wx.navigateTo({ url: `/pages/detail/detail?id=${id}` });
},
onCancelRegistrationTap(e) {
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;
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' ? '上传付款凭证' : '上传素材';
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: 接入后端上传接口(素材/付款凭证)
// kind: material | voucher
// registrationId: 报名记录 id
// tempFilePath: 本地图片路径
wx.showToast({ title: '已选择', icon: 'success' });
}
});
}
});
}
});
},
loadEvents(reset) {
if (this.data.loading) return Promise.resolve();
@ -318,17 +383,29 @@ Page({
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);
return {
id: eid,
rowKey: `m-${rid}`,
registrationId: rid,
eventName: item.eventName,
coverImage: item.coverImage,
address: '',
eventStatusPill: 1,
eventStatusText: '已报名',
eventStatusPill,
eventStatusText: eventStatusText(rawEventStatus) || '已报名',
registrationTimeText: formatDateTime(item.registrationTime),
projectCount: item.projectCount,
eventFormatText: ''
eventFormatText: '',
paymentStatus: item.paymentStatus != null ? Number(item.paymentStatus) : null,
canCancel: canCancelByEventStatus(rawEventStatus)
};
});

View File

@ -129,6 +129,33 @@
<view class="meta-line" wx:if="{{item.projectCount != null}}">已报项目:{{item.projectCount}} 项</view>
</block>
</view>
<view class="card-actions" wx:if="{{sectionTab === 'registered'}}" catchtap>
<view
class="action-btn action-btn--ghost"
catchtap="onUploadMaterialTap"
data-registrationid="{{item.registrationId}}"
>上传素材</view>
<block wx:if="{{item.paymentStatus === 1}}">
<view class="action-btn action-btn--paid" catchtap>已支付</view>
</block>
<block wx:else>
<view
class="action-btn action-btn--ghost"
catchtap="onUploadVoucherTap"
data-registrationid="{{item.registrationId}}"
>上传付款凭证</view>
</block>
<view class="action-btn action-btn--ghost" catchtap>更改参赛人</view>
<view
class="action-btn action-btn--danger"
wx:if="{{item.canCancel}}"
catchtap="onCancelRegistrationTap"
data-registrationid="{{item.registrationId}}"
>取消报名</view>
</view>
</view>
</block>

View File

@ -295,6 +295,44 @@ page {
padding: 22rpx 20rpx 26rpx;
}
.card-actions {
display: flex;
gap: 16rpx;
padding: 0 20rpx 22rpx;
flex-wrap: nowrap;
}
.action-btn {
flex: 1;
height: 64rpx;
line-height: 64rpx;
text-align: center;
font-size: 24rpx;
border-radius: 10rpx;
border: 2rpx solid #e1e3e6;
background: #fff;
color: #323233;
box-sizing: border-box;
}
.action-btn--ghost {
background: #fff;
color: #323233;
}
.action-btn--danger {
border-color: #e6060b;
color: #e6060b;
background: #fff;
}
.action-btn--paid {
border-color: #00b56d;
background: rgba(0, 181, 109, 0.12);
color: #00b56d;
font-weight: 700;
}
.title-row {
display: flex;
flex-wrap: wrap;

View File

@ -73,8 +73,4 @@
</view>
</view>
</view>
<view class="openid-row">
<button class="openid-mini-btn" size="mini" bindtap="onFetchOpenId">获取openid</button>
</view>
</view>

View File

@ -0,0 +1,155 @@
const { API } = require('../../../utils/constants');
const { request } = require('../../../utils/http');
function toNumber(v, fallback = 0) {
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
function formatMoney(v) {
const n = toNumber(v, 0);
return (Math.round(n * 100) / 100).toFixed(2).replace(/\.00$/, '');
}
function genderText(v) {
if (v == null) return '';
const s = String(v).toLowerCase();
if (s === 'male' || s === 'm' || s === '1') return '男';
if (s === 'female' || s === 'f' || s === '2') return '女';
return '';
}
Page({
data: {
registrationId: null,
loading: true,
refundRulesText: '退款规则以赛事说明为准',
// items: [{ itemKey, projectName, pills: [{ pillKey, label, price, selected }] }]
items: [],
selectAll: false,
selectedCount: 0,
refundAmount: '0'
},
onLoad(options) {
const registrationId = Number(options.registrationId);
this.setData({ registrationId });
if (!registrationId) {
this.setData({ loading: false });
wx.showToast({ title: '参数错误', icon: 'none' });
return;
}
this.fetchRegistrationDetail(registrationId);
},
fetchRegistrationDetail(registrationId) {
this.setData({ loading: true });
request({
path: API.myRegistrationDetail(registrationId),
method: 'GET',
needAuth: true
})
.then((detail) => {
const d = detail && typeof detail === 'object' ? detail : {};
const rawItems = Array.isArray(d.items) ? d.items : [];
const items = rawItems.map((it, idx) => {
const projectName = it.projectName || it.ProjectName || `项目${idx + 1}`;
const groupName = it.groupName || it.GroupName || '';
const price = it.price != null ? toNumber(it.price, 0) : 0;
const persons = Array.isArray(it.persons) ? it.persons : [];
const pills =
persons.length > 0
? persons.map((p, pi) => {
const gender = genderText(p.gender);
const label = [groupName, gender].filter(Boolean).join(' | ') || groupName || '参赛人';
return {
pillKey: `i-${idx}-p-${pi}`,
label,
price,
selected: true
};
})
: [
{
pillKey: `i-${idx}-g`,
label: groupName || '组别',
price,
selected: true
}
];
return {
itemKey: `it-${idx}`,
projectName,
pills
};
});
// 默认全选
this.setData({
items,
selectAll: items.length > 0,
loading: false
});
this.recalc();
})
.catch((err) => {
this.setData({ loading: false, items: [] });
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' });
});
},
onTogglePill(e) {
const pillKey = e.currentTarget.dataset.pillkey;
if (!pillKey) return;
const items = this.data.items.map((it) => ({
...it,
pills: it.pills.map((p) => (p.pillKey === pillKey ? { ...p, selected: !p.selected } : p))
}));
this.setData({ items });
this.recalc();
},
onToggleAll() {
const next = !this.data.selectAll;
const items = this.data.items.map((it) => ({
...it,
pills: it.pills.map((p) => ({ ...p, selected: next }))
}));
this.setData({ items, selectAll: next });
this.recalc();
},
recalc() {
const all = [];
this.data.items.forEach((it) => it.pills.forEach((p) => all.push(p)));
const selected = all.filter((p) => p.selected);
const sum = selected.reduce((acc, p) => acc + toNumber(p.price, 0), 0);
const selectAll = all.length > 0 && selected.length === all.length;
this.setData({
selectedCount: selected.length,
selectAll,
refundAmount: formatMoney(sum)
});
},
onConfirmCancel() {
if (this.data.selectedCount === 0) return;
wx.showModal({
title: '确认取消报名?',
content: `本次预计退款金额 ¥${this.data.refundAmount}`,
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (!res || !res.confirm) return;
// TODO: 接入后端“取消报名/退款”接口后提交所选项目
wx.showToast({ title: '已提交(待接接口)', icon: 'none' });
}
});
}
});

View File

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "取消报名",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}

View File

@ -0,0 +1,53 @@
<view class="page">
<view class="notice">
<view class="notice-head">
<text class="notice-title">注意</text>
</view>
<view class="notice-row">
<text class="notice-label">退款规则:</text>
<text class="notice-text">{{refundRulesText}}</text>
</view>
<view class="notice-row">
<text class="notice-label">退款路径:</text>
<text class="notice-text">原路退款,请注意查收</text>
</view>
</view>
<view class="items" wx:if="{{items.length}}">
<block wx:for="{{items}}" wx:key="itemKey" wx:for-item="regItem">
<view class="item-card">
<view class="item-title">
<text class="item-title-text">{{regItem.projectName}}</text>
</view>
<view class="pill-row">
<block wx:for="{{regItem.pills}}" wx:key="pillKey" wx:for-item="pill">
<view
class="pill {{pill.selected ? 'is-on' : ''}}"
catchtap="onTogglePill"
data-pillkey="{{pill.pillKey}}"
>{{pill.label}}</view>
</block>
</view>
</view>
</block>
</view>
<view class="empty" wx:else>
<text class="empty-text">{{loading ? '加载中...' : '暂无可取消项目'}}</text>
</view>
<view class="footer">
<view class="footer-left" catchtap="onToggleAll">
<view class="check {{selectAll ? 'is-on' : ''}}"></view>
<text class="footer-left-text">取消全部</text>
</view>
<view
class="footer-btn {{selectedCount === 0 ? 'is-disabled' : ''}}"
catchtap="onConfirmCancel"
>
<text class="footer-btn-sub">退款金额 ¥{{refundAmount}}</text>
<text class="footer-btn-main">确认取消</text>
</view>
</view>
</view>

View File

@ -0,0 +1,161 @@
.page {
min-height: 100%;
background: #f5f6f8;
padding: 20rpx 20rpx 140rpx;
box-sizing: border-box;
}
.notice {
background: #fff;
border-radius: 16rpx;
padding: 22rpx 22rpx 18rpx;
box-sizing: border-box;
border: 2rpx solid rgba(26, 87, 178, 0.28);
}
.notice-head {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.notice-title {
font-size: 28rpx;
font-weight: 800;
color: #1a57b2;
}
.notice-row {
display: flex;
font-size: 24rpx;
color: #646566;
line-height: 1.6;
margin-top: 6rpx;
}
.notice-label {
flex-shrink: 0;
color: #323233;
}
.notice-text {
flex: 1;
}
.items {
margin-top: 18rpx;
}
.item-card {
background: #fff;
border-radius: 16rpx;
padding: 18rpx 18rpx 12rpx;
margin-top: 18rpx;
}
.item-title-text {
font-size: 26rpx;
font-weight: 800;
color: #111;
}
.pill-row {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 14rpx;
}
.pill {
padding: 10rpx 16rpx;
border-radius: 999rpx;
border: 2rpx solid #e1e3e6;
font-size: 22rpx;
color: #323233;
background: #fff;
}
.pill.is-on {
background: #1a57b2;
border-color: #1a57b2;
color: #fff;
font-weight: 700;
}
.empty {
padding: 140rpx 0;
text-align: center;
}
.empty-text {
font-size: 26rpx;
color: #969799;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 18rpx 20rpx calc(env(safe-area-inset-bottom) + 18rpx);
background: #fff;
display: flex;
align-items: center;
gap: 18rpx;
box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.06);
}
.footer-left {
display: flex;
align-items: center;
gap: 12rpx;
padding: 8rpx 10rpx;
}
.check {
width: 34rpx;
height: 34rpx;
border-radius: 999rpx;
border: 2rpx solid #c8c9cc;
box-sizing: border-box;
background: #fff;
}
.check.is-on {
border-color: #1a57b2;
background: #1a57b2;
}
.footer-left-text {
font-size: 24rpx;
color: #646566;
}
.footer-btn {
flex: 1;
height: 90rpx;
border-radius: 14rpx;
background: #1a57b2;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.footer-btn.is-disabled {
opacity: 0.45;
}
.footer-btn-sub {
font-size: 22rpx;
line-height: 1.2;
}
.footer-btn-main {
font-size: 30rpx;
font-weight: 800;
line-height: 1.2;
margin-top: 4rpx;
}

View File

@ -8,6 +8,8 @@ const API = {
eventList: '/api/eventclient/list',
eventMyList: '/api/eventclient/my-events',
eventDetail: (id) => `/api/eventclient/${id}`,
eventRegistrationStatus: (registrationId) => `/api/eventclient/status/${registrationId}`,
myRegistrationDetail: (registrationId) => `/api/eventclient/my-registration/${registrationId}`,
apply: '/api/eventclient/apply',
calculateAmount: '/api/eventclient/calculate-amount',