Ship signup sheet, subscribe reminders, cover crop, and theme updates.

Wire real WeChat app config, prefer activity cover over posters, and add admin cover cropping for the home banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michelle Zan
2026-10-08 13:06:43 +08:00
co-authored by Cursor
parent cd05113755
commit 0ca2e87bf2
35 changed files with 968 additions and 184 deletions
@@ -20,7 +20,7 @@ Component({
activity(activity) {
const item = activity || {}
this.setData({
poster: media.absUrl(item.poster || item.cover),
poster: media.absUrl(item.cover || item.poster),
video: media.absUrl(item.video),
ended: item.status === 'past'
})
@@ -0,0 +1,158 @@
const api = require('../../utils/api')
const authUtil = require('../../utils/auth')
const activityUtil = require('../../utils/activity')
function requestRemind(tmplIds) {
const ids = (tmplIds || []).filter(Boolean).slice(0, 3)
if (!ids.length) {
return Promise.resolve({ status: 'none', enabled: false })
}
return new Promise((resolve) => {
wx.requestSubscribeMessage({
tmplIds: ids,
success(res) {
// res[tmplId] === 'accept' | 'reject' | 'ban' | 'filter'
let status = 'reject'
for (let i = 0; i < ids.length; i += 1) {
if (res[ids[i]] === 'accept') {
status = 'accept'
break
}
if (res[ids[i]]) status = res[ids[i]]
}
resolve({ status: status, enabled: true })
},
fail() {
resolve({ status: 'reject', enabled: true })
}
})
})
}
Component({
properties: {
show: {
type: Boolean,
value: false
},
activityId: {
type: String,
value: ''
}
},
data: {
name: '',
phone: '',
company: '',
position: '',
submitting: false,
tmplIds: [],
remindReady: false
},
observers: {
show(visible) {
if (visible) {
wx.hideTabBar({ animation: false })
this.prefetch()
} else {
wx.showTabBar({ animation: false })
}
}
},
lifetimes: {
detached() {
wx.showTabBar({ animation: false })
}
},
methods: {
prefetch() {
api
.subscribeConfig()
.then((config) => {
this.setData({
tmplIds: (config && config.tmplIds) || [],
remindReady: !!(config && config.enabled)
})
})
.catch(() => this.setData({ tmplIds: [], remindReady: false }))
api
.profile()
.then((profile) => {
if (!profile) return
this.setData({
name: this.data.name || profile.name || '',
phone: this.data.phone || profile.phone || '',
company: this.data.company || profile.company || '',
position: this.data.position || profile.position || ''
})
})
.catch(() => {})
},
noop() {},
onInput(event) {
const field = event.currentTarget.dataset.field
const next = {}
next[field] = event.detail.value
this.setData(next)
},
onCancel() {
if (this.data.submitting) return
wx.showTabBar({ animation: false })
this.triggerEvent('close')
},
onSubmit() {
if (this.data.submitting) return
const name = (this.data.name || '').trim()
const phone = (this.data.phone || '').trim()
const company = (this.data.company || '').trim()
const position = (this.data.position || '').trim()
if (!name || !phone) {
wx.showToast({ title: '请填写姓名和手机号', icon: 'none' })
return
}
if (!/^1\d{10}$/.test(phone)) {
wx.showToast({ title: '请填写正确的手机号', icon: 'none' })
return
}
if (!this.data.activityId) {
wx.showToast({ title: '活动不存在', icon: 'none' })
return
}
this.setData({ submitting: true })
// Must run inside the tap gesture so WeChat can show the subscribe dialog.
requestRemind(this.data.tmplIds)
.then((remind) =>
api.signup({
activityId: this.data.activityId,
name: name,
phone: phone,
company: company,
position: position,
sourceSharer: authUtil.sourceSharer(),
remindStatus: remind.status
}).then((signup) => ({ signup: signup, remind: remind }))
)
.then((result) => {
const accepted = result.remind && result.remind.status === 'accept'
wx.showToast({
title: accepted ? '报名成功,已开启提醒' : '报名成功',
icon: 'success'
})
this.setData({ submitting: false })
wx.showTabBar({ animation: false })
this.triggerEvent('success')
this.triggerEvent('close')
})
.catch((error) => {
this.setData({ submitting: false })
activityUtil.toast(error)
})
}
}
})
@@ -0,0 +1,3 @@
{
"component": true
}
@@ -0,0 +1,77 @@
<view class="root {{show ? 'on' : ''}}" wx:if="{{show}}" catchtouchmove="noop">
<view class="mask" bindtap="onCancel"></view>
<view class="sheet" catchtap="noop">
<view class="handle"></view>
<text class="title">活动报名</text>
<text class="subtitle">填写信息提交后,将推送微信服务通知</text>
<view class="notice">
<text class="notice-icon">📣</text>
<text class="notice-text">{{remindReady ? '提交后将申请活动提醒授权,同意即可在活动开始前收到微信通知' : '提交即视为同意接收活动提醒(管理员需先在后台配置订阅消息模板)'}}</text>
</view>
<view class="field">
<view class="label-row">
<text class="label">姓名</text>
<text class="required">*</text>
</view>
<input
class="input"
placeholder="请输入姓名"
placeholder-class="placeholder"
value="{{name}}"
maxlength="20"
data-field="name"
bindinput="onInput"
/>
</view>
<view class="field">
<view class="label-row">
<text class="label">手机号码</text>
<text class="required">*</text>
</view>
<input
class="input"
type="number"
placeholder="请输入手机号"
placeholder-class="placeholder"
value="{{phone}}"
maxlength="11"
data-field="phone"
bindinput="onInput"
/>
</view>
<view class="field">
<text class="label">公司名称</text>
<input
class="input"
placeholder="选填"
placeholder-class="placeholder"
value="{{company}}"
maxlength="40"
data-field="company"
bindinput="onInput"
/>
</view>
<view class="field">
<text class="label">职位</text>
<input
class="input"
placeholder="选填"
placeholder-class="placeholder"
value="{{position}}"
maxlength="40"
data-field="position"
bindinput="onInput"
/>
</view>
<view class="btn primary {{submitting ? 'busy' : ''}}" bindtap="onSubmit">
{{submitting ? '提交中…' : '提交报名'}}
</view>
<view class="btn ghost" bindtap="onCancel">取消</view>
</view>
</view>
@@ -0,0 +1,144 @@
.root {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1000;
}
.mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
}
.sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
max-height: 92vh;
padding: 16rpx 40rpx calc(40rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
overflow-y: auto;
}
.handle {
width: 64rpx;
height: 8rpx;
margin: 0 auto 28rpx;
border-radius: 4rpx;
background: #e5e7eb;
}
.title {
display: block;
color: #111827;
font-size: 40rpx;
font-weight: 700;
line-height: 1.3;
}
.subtitle {
display: block;
margin-top: 12rpx;
color: #9ca3af;
font-size: 26rpx;
line-height: 1.5;
}
.notice {
display: flex;
align-items: flex-start;
gap: 12rpx;
margin-top: 28rpx;
padding: 20rpx 24rpx;
border-radius: 16rpx;
background: #e8f7f4;
}
.notice-icon {
flex-shrink: 0;
font-size: 28rpx;
line-height: 1.45;
}
.notice-text {
flex: 1;
color: #0d8b87;
font-size: 24rpx;
line-height: 1.55;
}
.field {
margin-top: 28rpx;
}
.label-row {
display: flex;
align-items: center;
gap: 4rpx;
}
.label {
display: block;
color: #374151;
font-size: 28rpx;
font-weight: 500;
}
.required {
color: #ef4444;
font-size: 28rpx;
line-height: 1;
}
.input {
height: 88rpx;
margin-top: 14rpx;
padding: 0 28rpx;
border: 2rpx solid #e5e7eb;
border-radius: 16rpx;
box-sizing: border-box;
font-size: 30rpx;
color: #111827;
background: #ffffff;
}
.placeholder {
color: #c4c9d1;
}
.btn {
height: 96rpx;
margin-top: 24rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
box-sizing: border-box;
}
.btn.primary {
margin-top: 40rpx;
background: #0d8b87;
color: #ffffff;
}
.btn.primary.busy {
opacity: 0.7;
}
.btn.ghost {
background: #ffffff;
color: #0d8b87;
border: 2rpx solid #0d8b87;
}