feat: 微信分享图回退 + 活动时间区间 + 发布方权限 + 体验优化
- 活动/资讯分享图双通道回退(朋友圈shareImgTimeline/会话shareImgMessage,未设置回退封面) - 资讯详情页新增朋友圈分享(enableShareTimeline) - 活动分享标题去掉「· 云超服活动」后缀,只保留活动标题 - 活动时间渲染改为开始-结束区间(同一天只显示一次日期,跨天显示完整两端) - 活动列表时间过长时回退只显示开始时间 - 活动详情页副标题改为活动标题 - 报名截止行强制单行不换行(标题+内容均nowrap) - 默认问卷「最想解决的问题」admin可控显示(showDefaultQuestion!==false时渲染) - 发布方/管理员权限拆分:报名管理入口用canReview||canManage(发布方+管理员均可进入审核报名)
This commit is contained in:
@@ -1 +1,5 @@
|
||||
export default { navigationStyle: 'custom' };
|
||||
export default {
|
||||
navigationStyle: 'custom',
|
||||
enableShareAppMessage: true,
|
||||
enableShareTimeline: true,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
<View className="ed page-in">
|
||||
<PageHeader back no="EVENT" title={manual ? '报名已提交' : '报名成功'} sub="线下 · 线上 公益课与沙龙" />
|
||||
<PageHeader back no="EVENT" title={manual ? '报名已提交' : '报名成功'} sub={ev.title} />
|
||||
<View className="ok-icon" />
|
||||
<Text className="ok-title">{manual ? '报名已提交' : '报名成功'}</Text>
|
||||
{paid && ev.priceFen > 0 && <Text className="ok-desc">已支付 {priceText(ev.priceFen)}</Text>}
|
||||
<Text className="ok-desc">场次:{ev.title}</Text>
|
||||
<Text className="ok-desc">时间:{fmtEvt(ev.startAt)}</Text>
|
||||
<Text className="ok-desc">时间:{fmtEvtRange(ev.startAt, ev.endAt)}</Text>
|
||||
<Text className="ok-desc">{ev.mode === 'online' ? `线上 · ${ev.location}` : `线下 · ${ev.location}`}</Text>
|
||||
<Text className="ok-desc">预约编号:{done.id}</Text>
|
||||
<Text className="ok-desc">
|
||||
@@ -390,7 +399,7 @@ export default function EventDetail() {
|
||||
|
||||
return (
|
||||
<View className="ed page-in">
|
||||
<PageHeader back no="EVENT" title="活动详情" sub="线下 · 线上 公益课与沙龙" />
|
||||
<PageHeader back no="EVENT" title="活动详情" sub={ev.title} />
|
||||
{/* 宣传图 / 头图 */}
|
||||
<View className="ed-hero">
|
||||
<Image className="ed-img" src={ev.image || coverWide} mode="aspectFill"
|
||||
@@ -414,12 +423,12 @@ export default function EventDetail() {
|
||||
{ev.subtitle && <Text className="ed-sub">{ev.subtitle}</Text>}
|
||||
|
||||
<View className="ed-info">
|
||||
<View className="ed-line"><Text className="ed-k">时间</Text><Text className="ed-v">{fmtEvt(ev.startAt)}</Text></View>
|
||||
<View className="ed-line"><Text className="ed-k">时间</Text><Text className="ed-v">{fmtEvtRange(ev.startAt, ev.endAt)}</Text></View>
|
||||
<View className="ed-line"><Text className="ed-k">费用</Text><Text className="ed-v ed-price">{priceText(ev.priceFen)}</Text></View>
|
||||
<View className="ed-line"><Text className="ed-k">{ev.mode === 'online' ? '入口' : '地点'}</Text><Text className="ed-v">{ev.location}</Text></View>
|
||||
{ev.host && <View className="ed-line"><Text className="ed-k">主办</Text><Text className="ed-v">{ev.host}</Text></View>}
|
||||
{ev.signupDeadline && (
|
||||
<View className="ed-line"><Text className="ed-k">报名截止</Text>
|
||||
<View className="ed-line deadline"><Text className="ed-k">报名截止</Text>
|
||||
<Text className={`ed-v${signupClosed ? ' closed' : ''}`}>{fmtDeadline(ev.signupDeadline)}{signupClosed ? '(已截止)' : ''}</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -428,7 +437,7 @@ export default function EventDetail() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{ev.canManage && (
|
||||
{(ev.canReview || ev.canManage) && (
|
||||
<View className="ed-manage">
|
||||
<Button className="btn-ghost manage-btn" onClick={goManage}>报名管理(审核/编辑名单)→</Button>
|
||||
</View>
|
||||
@@ -564,16 +573,19 @@ export default function EventDetail() {
|
||||
)}
|
||||
{authed && phoneBound && (
|
||||
<View>
|
||||
<Text className="ed-form-title">报名本场</Text>
|
||||
{/* <Text className="ed-form-title">报名本场</Text> */}
|
||||
<Text className="muted bind-tip">
|
||||
将使用你的个人资料报名({user && user.name ? user.name : '已登录账号'}
|
||||
{user && user.status ? ` · ${user.status}` : ''}),可在「我的」页修改。
|
||||
</Text>
|
||||
{/* 自定义报名表单(按活动定义渲染;必填/选填/隐藏字段均需填写) */}
|
||||
{(ev.formFields || []).map((f) => renderField(f))}
|
||||
<View className="field"><Text className="label">最想解决的问题(选填)</Text>
|
||||
<Input value={question} onInput={(e) => setQuestion(e.detail.value)} placeholder="例:有设计技能但接单不稳定,想把它做成产品" className="input" />
|
||||
</View>
|
||||
{/* 默认问卷「最想解决的问题」:admin 可控制是否显示(默认显示,兼容旧数据) */}
|
||||
{ev.showDefaultQuestion !== false && (
|
||||
<View className="field"><Text className="label">最想解决的问题(选填)</Text>
|
||||
<Input value={question} onInput={(e) => setQuestion(e.detail.value)} placeholder="例:有设计技能但接单不稳定,想把它做成产品" className="input" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{debug && <Text className="ok-text">{debug}</Text>}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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() {
|
||||
<Text className="bk-want">{b.eventTitle || b.want || '报名'}</Text>
|
||||
<Text className={`bk-phase ${p.cls}`}>{p.text}</Text>
|
||||
</View>
|
||||
{b.eventStart && <Text className="bk-time">场次时间:{fmtTime(b.eventStart)}</Text>}
|
||||
{b.eventStart && <Text className="bk-time">场次时间:{fmtEvtRange(b.eventStart, b.eventEnd)}</Text>}
|
||||
<Text className="bk-time">报名时间:{fmtTime(b.createdAt)}</Text>
|
||||
<Text className="bk-extra">编号 {b.id} · {b.name || '—'}</Text>
|
||||
{renderFormData(b)}
|
||||
|
||||
@@ -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() {
|
||||
<Text className="adm-meta">{e.category || '未分类'} · {e.region || '其他'} · {e.mode === 'online' ? '线上' : e.mode === 'both' ? '线上/线下' : '线下'}</Text>
|
||||
</View>
|
||||
<View className="adm-meta-row">
|
||||
<Text className="adm-meta">{fmtEvt(e.startAt)}</Text>
|
||||
<Text className="adm-meta">{fmtEvtRange(e.startAt, e.endAt)}</Text>
|
||||
<Text className="adm-meta">{priceText(e.priceFen)}</Text>
|
||||
</View>
|
||||
{e.reviewStatus === 'rejected' && e.reviewComment ? <Text className="adm-note">驳回原因:{e.reviewComment}</Text> : null}
|
||||
|
||||
@@ -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() {
|
||||
<View className="cell-body">
|
||||
<Text className="cell-name">{e.title}</Text>
|
||||
<Text className="cell-desc">{e.desc}</Text>
|
||||
<Text className="cell-meta">{fmtEvt(e.startAt)}</Text>
|
||||
<Text className="cell-meta">{(() => { const t = fmtEvtRange(e.startAt, e.endAt); return t.length > 22 ? fmtEvt(e.startAt) : t; })()}</Text>
|
||||
<Text className="cell-loc">{e.location}</Text>
|
||||
{e.showCapacity && e.capacity > 0 && (
|
||||
<Text className="cell-cap">名额 {e.enrolled || 0} / {e.capacity}</Text>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 || '') : '',
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user