From 0643cd1a0be644c1dcae7527d746c5de8589fc6f Mon Sep 17 00:00:00 2001 From: Pine Date: Wed, 9 Sep 2026 21:53:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BE=AE=E4=BF=A1=E5=88=86=E4=BA=AB?= =?UTF-8?q?=E5=9B=BE=E5=9B=9E=E9=80=80=20+=20=E6=B4=BB=E5=8A=A8=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E5=8C=BA=E9=97=B4=20+=20=E5=8F=91=E5=B8=83=E6=96=B9?= =?UTF-8?q?=E6=9D=83=E9=99=90=20+=20=E4=BD=93=E9=AA=8C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 活动/资讯分享图双通道回退(朋友圈shareImgTimeline/会话shareImgMessage,未设置回退封面) - 资讯详情页新增朋友圈分享(enableShareTimeline) - 活动分享标题去掉「· 云超服活动」后缀,只保留活动标题 - 活动时间渲染改为开始-结束区间(同一天只显示一次日期,跨天显示完整两端) - 活动列表时间过长时回退只显示开始时间 - 活动详情页副标题改为活动标题 - 报名截止行强制单行不换行(标题+内容均nowrap) - 默认问卷「最想解决的问题」admin可控显示(showDefaultQuestion!==false时渲染) - 发布方/管理员权限拆分:报名管理入口用canReview||canManage(发布方+管理员均可进入审核报名) --- .../content-detail/index.config.js | 6 +++- .../src/pages-extra/content-detail/index.jsx | 12 +++++-- .../src/pages-extra/event-detail/index.jsx | 34 +++++++++++++------ .../src/pages-extra/event-detail/index.scss | 3 ++ .../src/pages-extra/my-bookings/index.jsx | 4 +-- .../src/pages-extra/my-events/index.jsx | 4 +-- miniprogram/src/pages/booking/index.jsx | 4 +-- miniprogram/src/utils/booking.js | 17 ++++++++++ miniprogram/src/utils/share.js | 15 ++++---- 9 files changed, 72 insertions(+), 27 deletions(-) diff --git a/miniprogram/src/pages-extra/content-detail/index.config.js b/miniprogram/src/pages-extra/content-detail/index.config.js index a4049ce..6d44e84 100644 --- a/miniprogram/src/pages-extra/content-detail/index.config.js +++ b/miniprogram/src/pages-extra/content-detail/index.config.js @@ -1 +1,5 @@ -export default { navigationStyle: 'custom' }; +export default { + navigationStyle: 'custom', + enableShareAppMessage: true, + enableShareTimeline: true, +}; diff --git a/miniprogram/src/pages-extra/content-detail/index.jsx b/miniprogram/src/pages-extra/content-detail/index.jsx index 9cdd186..0293492 100644 --- a/miniprogram/src/pages-extra/content-detail/index.jsx +++ b/miniprogram/src/pages-extra/content-detail/index.jsx @@ -1,5 +1,5 @@ import { View, Text, Image, Video, ScrollView, Input, Button, RichText } from '@tarojs/components'; -import Taro, { useShareAppMessage } from '@tarojs/taro'; +import Taro, { useShareAppMessage, useShareTimeline } from '@tarojs/taro'; import { useState, useEffect } from 'react'; import { getContent, getComments, addComment, toggleLike, reportShare } from '@/utils/content'; import { isAuthed } from '@/utils/auth'; @@ -40,16 +40,22 @@ export default function ContentDetail() { const [replyTo, setReplyTo] = useState(null); // 被回复的评论 {id, name}(hooks 必须在提前 return 之前) const [expanded, setExpanded] = useState({}); // 展开全部回复的评论 id - // 微信分享:转发标题/封面/路径(配合 Button openType="share");转发落库 share_count +1 + // 微信分享:会话(好友/群聊 5:4)+ 朋友圈(1:1),分享图 admin 端可单独配置,未配置回退封面; + // 转发落库 share_count +1(仅会话转发计数) useShareAppMessage(() => { const sid = (item && item.id) || ''; if (sid) reportShare(sid).then((r) => { if (r && r.share_count) setShareCount(r.share_count); }).catch(() => {}); return { title: (item && item.title) || '云超服 · 资讯', path: `/pages-extra/content-detail/index?id=${sid}`, - imageUrl: (item && (item.cover || item.video_cover)) || '', + imageUrl: (item && (item.share_img_message || item.cover || item.video_cover)) || '', }; }); + useShareTimeline(() => ({ + title: (item && item.title) || '云超服 · 资讯', + query: (item && item.id) ? `id=${item.id}` : '', + imageUrl: (item && (item.share_img_timeline || item.cover || item.video_cover)) || '', + })); useEffect(() => { const inst = Taro.getCurrentInstance(); diff --git a/miniprogram/src/pages-extra/event-detail/index.jsx b/miniprogram/src/pages-extra/event-detail/index.jsx index 0071ccf..62c376a 100644 --- a/miniprogram/src/pages-extra/event-detail/index.jsx +++ b/miniprogram/src/pages-extra/event-detail/index.jsx @@ -1,7 +1,7 @@ import { View, Text, Input, Textarea, Picker, Button, Image, RichText } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { useState, useEffect } from 'react'; -import { getEventDetail, submitBooking, getMyBookingForEvent, fmtEvt, payBooking, bookingPayStatus, requestWxPay, priceText } from '@/utils/booking'; +import { getEventDetail, submitBooking, getMyBookingForEvent, fmtEvt, fmtEvtRange, payBooking, bookingPayStatus, requestWxPay, priceText } from '@/utils/booking'; /* rich-text 子树不吃外部 CSS:传入前对 HTML 内联约束,杜绝图片/表格横向溢出 */ const sanitizeRichHtml = (html) => String(html || '') @@ -141,6 +141,12 @@ export default function EventDetail() { finally { setBusy(false); } }; + const refreshMyBook = () => { + if (isAuthed() && id) { + getMyBookingForEvent(id).then((d) => setMyBook(d || null)).catch(() => {}); + } + }; + /* 微信一键登录:wx.login + 昵称头像 → 后端建/查 wxid 账号 */ const onWxLogin = async () => { if (!agree) { Taro.showToast({ title: '请先阅读并同意《用户服务协议》和《隐私政策》', icon: 'none' }); return; } @@ -152,6 +158,7 @@ export default function EventDetail() { const d = await wxLogin({ code: loginCode, nickName, avatarUrl }); setAuthed(true); setPhoneBound(!!d.phoneBound); + refreshMyBook(); // 登录成功立即刷新「我的报名」状态,已报名用户直接展示报名卡 } catch (e) { setErr((e && e.message) || '微信登录失败'); } finally { setBusy(false); } }; @@ -166,6 +173,7 @@ export default function EventDetail() { const d = await wxPhoneBind(pcode); setAuthed(true); setPhoneBound(!!d.phoneBound); + refreshMyBook(); // 绑号成功刷新报名状态(已报名用户直接进入「我的报名」卡) } catch (err) { setErr((err && err.message) || '绑定失败'); } finally { setBusy(false); } }; @@ -186,6 +194,7 @@ export default function EventDetail() { } setAuthed(true); setPhoneBound(true); + refreshMyBook(); // 登录/绑号成功后同步报名状态 } catch (e) { setErr((e && e.message) || '登录失败'); } finally { setBusy(false); } }; @@ -366,12 +375,12 @@ export default function EventDetail() { const manual = ev.auditMode === 'manual'; return ( - + {manual ? '报名已提交' : '报名成功'} {paid && ev.priceFen > 0 && 已支付 {priceText(ev.priceFen)}} 场次:{ev.title} - 时间:{fmtEvt(ev.startAt)} + 时间:{fmtEvtRange(ev.startAt, ev.endAt)} {ev.mode === 'online' ? `线上 · ${ev.location}` : `线下 · ${ev.location}`} 预约编号:{done.id} @@ -390,7 +399,7 @@ export default function EventDetail() { return ( - + {/* 宣传图 / 头图 */} {ev.subtitle}} - 时间{fmtEvt(ev.startAt)} + 时间{fmtEvtRange(ev.startAt, ev.endAt)} 费用{priceText(ev.priceFen)} {ev.mode === 'online' ? '入口' : '地点'}{ev.location} {ev.host && 主办{ev.host}} {ev.signupDeadline && ( - 报名截止 + 报名截止 {fmtDeadline(ev.signupDeadline)}{signupClosed ? '(已截止)' : ''} )} @@ -428,7 +437,7 @@ export default function EventDetail() { )} - {ev.canManage && ( + {(ev.canReview || ev.canManage) && ( @@ -564,16 +573,19 @@ export default function EventDetail() { )} {authed && phoneBound && ( - 报名本场 + {/* 报名本场 */} 将使用你的个人资料报名({user && user.name ? user.name : '已登录账号'} {user && user.status ? ` · ${user.status}` : ''}),可在「我的」页修改。 {/* 自定义报名表单(按活动定义渲染;必填/选填/隐藏字段均需填写) */} {(ev.formFields || []).map((f) => renderField(f))} - 最想解决的问题(选填) - setQuestion(e.detail.value)} placeholder="例:有设计技能但接单不稳定,想把它做成产品" className="input" /> - + {/* 默认问卷「最想解决的问题」:admin 可控制是否显示(默认显示,兼容旧数据) */} + {ev.showDefaultQuestion !== false && ( + 最想解决的问题(选填) + setQuestion(e.detail.value)} placeholder="例:有设计技能但接单不稳定,想把它做成产品" className="input" /> + + )} )} {debug && {debug}} diff --git a/miniprogram/src/pages-extra/event-detail/index.scss b/miniprogram/src/pages-extra/event-detail/index.scss index fd8073e..d2217ac 100644 --- a/miniprogram/src/pages-extra/event-detail/index.scss +++ b/miniprogram/src/pages-extra/event-detail/index.scss @@ -154,6 +154,9 @@ /* ── 报名体系升级:自定义表单 / 截止 / 隐藏信息 / 报名管理入口 ── */ .ed-v.closed { color: #f07575; } +/* 报名截止行:标题与内容均强制单行,不换行 */ +.ed-line.deadline .ed-k { width: auto; flex-shrink: 0; white-space: nowrap; } +.ed-line.deadline .ed-v { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ed-manage { margin-top: 20rpx; } .manage-btn { width: 100%; font-size: 26rpx; } diff --git a/miniprogram/src/pages-extra/my-bookings/index.jsx b/miniprogram/src/pages-extra/my-bookings/index.jsx index 9d1da32..44246f1 100644 --- a/miniprogram/src/pages-extra/my-bookings/index.jsx +++ b/miniprogram/src/pages-extra/my-bookings/index.jsx @@ -2,7 +2,7 @@ import { View, Text, Button, Image } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { useState } from 'react'; import { isAuthed } from '@/utils/auth'; -import { getMyBookings, payBooking, requestWxPay, bookingPayStatus, priceText, cancelBooking, deleteBooking } from '@/utils/booking'; +import { getMyBookings, payBooking, requestWxPay, bookingPayStatus, priceText, cancelBooking, deleteBooking, fmtEvtRange } from '@/utils/booking'; import { PageHeader } from '@/components/TopInset'; import Skeleton from '@/components/skeleton'; import './index.scss'; @@ -199,7 +199,7 @@ export default function MyBookings() { {b.eventTitle || b.want || '报名'} {p.text} - {b.eventStart && 场次时间:{fmtTime(b.eventStart)}} + {b.eventStart && 场次时间:{fmtEvtRange(b.eventStart, b.eventEnd)}} 报名时间:{fmtTime(b.createdAt)} 编号 {b.id} · {b.name || '—'} {renderFormData(b)} diff --git a/miniprogram/src/pages-extra/my-events/index.jsx b/miniprogram/src/pages-extra/my-events/index.jsx index d2a5a54..7d9b08c 100644 --- a/miniprogram/src/pages-extra/my-events/index.jsx +++ b/miniprogram/src/pages-extra/my-events/index.jsx @@ -1,7 +1,7 @@ import { View, Text, Button } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { useState } from 'react'; -import { myEvents, managedEvents, priceText, fmtEvt } from '@/utils/booking'; +import { myEvents, managedEvents, priceText, fmtEvt, fmtEvtRange } from '@/utils/booking'; import { PageHeader } from '@/components/TopInset'; import './index.scss'; @@ -74,7 +74,7 @@ export default function MyEvents() { {e.category || '未分类'} · {e.region || '其他'} · {e.mode === 'online' ? '线上' : e.mode === 'both' ? '线上/线下' : '线下'} - {fmtEvt(e.startAt)} + {fmtEvtRange(e.startAt, e.endAt)} {priceText(e.priceFen)} {e.reviewStatus === 'rejected' && e.reviewComment ? 驳回原因:{e.reviewComment} : null} diff --git a/miniprogram/src/pages/booking/index.jsx b/miniprogram/src/pages/booking/index.jsx index 27d2d34..73d76c5 100644 --- a/miniprogram/src/pages/booking/index.jsx +++ b/miniprogram/src/pages/booking/index.jsx @@ -1,7 +1,7 @@ import { View, Text, Button, Image, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { useState, useEffect } from 'react'; -import { getBookableEvents, fmtEvt, priceText } from '@/utils/booking'; +import { getBookableEvents, fmtEvt, fmtEvtRange, priceText } from '@/utils/booking'; import { cachedRequest } from '@/utils/api'; import { useTabIndex } from '@/utils/tab'; import Skeleton from '@/components/skeleton'; @@ -78,7 +78,7 @@ export default function Booking() { {e.title} {e.desc} - {fmtEvt(e.startAt)} + {(() => { const t = fmtEvtRange(e.startAt, e.endAt); return t.length > 22 ? fmtEvt(e.startAt) : t; })()} {e.location} {e.showCapacity && e.capacity > 0 && ( 名额 {e.enrolled || 0} / {e.capacity} diff --git a/miniprogram/src/utils/booking.js b/miniprogram/src/utils/booking.js index d6e4b07..301c3b4 100644 --- a/miniprogram/src/utils/booking.js +++ b/miniprogram/src/utils/booking.js @@ -86,6 +86,23 @@ export function fmtEvt(iso) { return `${d.getMonth() + 1}月${d.getDate()}日 周${week} ${p(d.getHours())}:${p(d.getMinutes())}`; } +/** 活动时间区间:开始-结束(同一天只显示一次日期;无结束时间则只显示开始)。 */ +export function fmtEvtRange(start, end) { + const s = parseIso(start); + if (!s) return ''; + const e = parseIso(end); + const p = (n) => String(n).padStart(2, '0'); + const week = ['日', '一', '二', '三', '四', '五', '六'][s.getDay()]; + const dateStr = `${s.getMonth() + 1}月${s.getDate()}日 周${week}`; + const startStr = `${p(s.getHours())}:${p(s.getMinutes())}`; + if (!e) return `${dateStr} ${startStr}`; + if (s.getFullYear() === e.getFullYear() && s.getMonth() === e.getMonth() && s.getDate() === e.getDate()) { + return `${dateStr} ${startStr}-${p(e.getHours())}:${p(e.getMinutes())}`; + } + const eweek = ['日', '一', '二', '三', '四', '五', '六'][e.getDay()]; + return `${dateStr} ${startStr} - ${e.getMonth() + 1}月${e.getDate()}日 周${eweek} ${p(e.getHours())}:${p(e.getMinutes())}`; +} + /** 查当前登录用户是否已报名某活动及其审核状态(详情页判断用) */ export async function getMyBookingForEvent(eventId) { return request('GET', `/api/bookings/by-event/${eventId}`, {}, true); diff --git a/miniprogram/src/utils/share.js b/miniprogram/src/utils/share.js index 4a76016..6e01366 100644 --- a/miniprogram/src/utils/share.js +++ b/miniprogram/src/utils/share.js @@ -3,22 +3,25 @@ import Taro, { useShareAppMessage, useShareTimeline } from '@tarojs/taro'; /** * 活动双通道分享:对话(onShareAppMessage)+ 朋友圈(onShareTimeline)。 * - * - 对话分享:标题 + 活动封面 + 落地页(带 id),群聊/单聊均可直达活动详情。 - * - 朋友圈分享:标题 + 活动封面 + query 带 id(需页面 config 开启 enableShareTimeline)。 + * - 对话分享:标题 + 分享图 + 落地页(带 id),群聊/单聊均可直达活动详情。 + * - 朋友圈分享:标题 + 分享图 + query 带 id(需页面 config 开启 enableShareTimeline)。 + * - 分享图规范(admin 端可单独配置,未配置自动回退活动封面): + * 朋友圈 1:1(ev.shareImgTimeline)| 微信好友/群聊 5:4(ev.shareImgMessage)。 + * 活动封面为 3.35:1 宽幅,直接使用会被微信按 1:1/5:4 裁剪导致展示不完整。 * * 用法:在组件内先持有 ev 状态,再调用 useEventShare(() => ev)。 */ export function useEventShare(getEv) { const ev = (typeof getEv === 'function' && getEv()) || null; useShareAppMessage(() => ({ - title: ev ? `${ev.title} · 云超服活动` : '云超服活动', + title: ev ? ev.title : '', path: `/pages-extra/event-detail/index?id=${ev ? ev.id : ''}`, - imageUrl: ev && ev.image ? ev.image : '', + imageUrl: ev ? (ev.shareImgMessage || ev.image || '') : '', })); useShareTimeline(() => ({ - title: ev ? ev.title : '云超服活动', + title: ev ? ev.title : '', query: `id=${ev ? ev.id : ''}`, - imageUrl: ev && ev.image ? ev.image : '', + imageUrl: ev ? (ev.shareImgTimeline || ev.image || '') : '', })); }