From 377aeeefb35de56f1cf0d47a8a4ee90ad80bdd54 Mon Sep 17 00:00:00 2001 From: Pine Date: Fri, 28 Aug 2026 17:19:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=B0=8F=E7=A8=8B=E5=BA=8F):=20=E8=B5=84?= =?UTF-8?q?=E8=AE=AF=E8=AF=A6=E6=83=85=E7=82=B9=E8=B5=9E/=E5=88=86?= =?UTF-8?q?=E4=BA=AB=E8=90=BD=E5=BA=93=20+=20=E5=BE=AE=E4=BF=A1=E5=BC=8F?= =?UTF-8?q?=E8=AF=84=E8=AE=BA=E5=8C=BA(=E5=9B=9E=E5=A4=8D=E5=B5=8C?= =?UTF-8?q?=E5=A5=97/=E8=AF=84=E8=AE=BA=E7=82=B9=E8=B5=9E/=E6=8A=98?= =?UTF-8?q?=E5=8F=A0)=20+=20=E5=8A=A8=E4=BD=9C=E6=9D=A1=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E6=95=B0=E5=AD=97=E5=BC=B1=E5=8C=96=E6=81=92=E6=98=BE0=20+=20?= =?UTF-8?q?=E5=AF=8C=E6=96=87=E6=9C=AC=E5=AA=92=E4=BD=93=E9=99=90=E5=AE=BD?= =?UTF-8?q?=20+=20=E4=BF=AEhooks=E8=B6=8A=E7=95=8C(#310)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/content-detail/index.jsx | 180 ++++++++++++++---- .../src/pages/content-detail/index.scss | 70 +++++-- miniprogram/src/utils/content.js | 24 ++- 3 files changed, 221 insertions(+), 53 deletions(-) diff --git a/miniprogram/src/pages/content-detail/index.jsx b/miniprogram/src/pages/content-detail/index.jsx index 1f32873..4d341dc 100644 --- a/miniprogram/src/pages/content-detail/index.jsx +++ b/miniprogram/src/pages/content-detail/index.jsx @@ -1,28 +1,59 @@ import { View, Text, Image, Video, ScrollView, Input, Button, RichText } from '@tarojs/components'; -import Taro from '@tarojs/taro'; +import Taro, { useShareAppMessage } from '@tarojs/taro'; import { useState, useEffect } from 'react'; -import { getContent, getComments, addComment } from '@/utils/content'; +import { getContent, getComments, addComment, toggleLike, reportShare } from '@/utils/content'; import { isAuthed } from '@/utils/auth'; import './index.scss'; const rw = (s) => (s || '').replace('T', ' ').replace('Z', '').slice(0, 16); +/* rich-text 内部节点不吃页面 CSS:渲染前把图片/视频宽度约束进正文(不超出页面) */ +const fitMedia = (html) => (html || '').replace(/<(img|video)\b([^>]*)>/gi, (m, tag, attrs) => { + const strip = attrs.replace(/\s(?:width|height)\s*=\s*("[^"]*"|'[^']*'|\S+)/gi, ' '); + const style = /style\s*=\s*("[^"]*"|'[^']*')/i.test(strip) + ? strip.replace(/style\s*=\s*("([^"]*)"|'([^']*)')/i, (s, _q, dq, sq) => `style="${dq || sq || ''};max-width:100%;height:auto;display:block;"`) + : `${strip} style="max-width:100%;height:auto;display:block;"`; + return `<${tag}${style}>`; +}); + /** 资讯详情(微信文章式):标题/作者/阅读 + 正文 + 动作条 + 留言。 */ export default function ContentDetail() { const [item, setItem] = useState(null); const [comments, setComments] = useState([]); const [err, setErr] = useState(''); const [loaded, setLoaded] = useState(false); - const [like, setLike] = useState(false); const [fav, setFav] = useState(false); + const [likeCount, setLikeCount] = useState(0); + const [liked, setLiked] = useState(false); + const [shareCount, setShareCount] = useState(0); const [draft, setDraft] = useState(''); + const doLike = async () => { + if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; } + try { + const r = await toggleLike(item.id); + setLiked(!!r.liked); setLikeCount(r.like_count || 0); + } catch { Taro.showToast({ title: '点赞失败', icon: 'none' }); } + }; const [sending, setSending] = useState(false); + const [replyTo, setReplyTo] = useState(null); // 被回复的评论 {id, name}(hooks 必须在提前 return 之前) + const [expanded, setExpanded] = useState({}); // 展开全部回复的评论 id + + // 微信分享:转发标题/封面/路径(配合 Button openType="share");转发落库 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/content-detail/index?id=${sid}`, + imageUrl: (item && (item.cover || item.video_cover)) || '', + }; + }); useEffect(() => { const inst = Taro.getCurrentInstance(); const id = (inst && inst.router && inst.router.params && inst.router.params.id) || ''; if (!id) { setErr('无效内容'); setLoaded(true); return; } - getContent(id).then(setItem).catch(() => setErr('加载失败')).finally(() => setLoaded(true)); + getContent(id).then((it) => { setItem(it); setLikeCount(it.like_count || 0); setLiked(!!it.liked_by_me); setShareCount(it.share_count || 0); }).catch(() => setErr('加载失败')).finally(() => setLoaded(true)); getComments(id).then(setComments).catch(() => {}); }, []); @@ -34,17 +65,64 @@ export default function ContentDetail() { const isSmall = item.card_mode === 'small'; // 小图模式:详情页不显示封面 const images = Array.isArray(item.images) ? item.images : []; const author = item.publisher_name || '云超服'; +// 发布者徽章:运营端显式身份 official/carrier/opc 优先;旧数据按原规则回退(无作者/官方→官方,否则 OPC) +const badgeOf = (it) => { + const bySource = { + official: { cls: 'pub-badge--official', ic: 'ic-shield', label: '官方' }, + carrier: { cls: 'pub-badge--park', ic: 'ic-landmark', label: '园区' }, + opc: { cls: 'pub-badge--opc', ic: 'ic-users', label: 'OPC' }, + }[it.source]; + if (bySource) return bySource; + return (!(it.publisher_name || '') || it.publisher_name === '官方') + ? { cls: 'pub-badge--official', ic: 'ic-shield', label: '官方' } + : { cls: 'pub-badge--opc', ic: 'ic-users', label: 'OPC' }; +}; +const pubBadge = badgeOf(item); const views = item.read_count || 0; const copyLink = () => Taro.setClipboardData({ data: `https://opc.pinesound.cn/#/content-detail?id=${id}` }).then(() => Taro.showToast({ title: '链接已复制', icon: 'none' })); const scrollToComment = () => { Taro.pageScrollTo({ selector: '.cd-comments', duration: 300 }); }; + + /* 留言(微信式):回复某条 → 底部输入框进入回复态;评论点赞一人一赞 */ + const startReply = (c) => { + if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; } + setReplyTo({ id: c.id, name: c.nickname || c.username || '用户' }); + Taro.pageScrollTo({ selector: '.cd-cmt-input', duration: 300 }); + }; + const doCommentLike = async (cid) => { + if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; } + try { + const r = await toggleCommentLike(cid); + const patch = (list) => list.map((it) => { + if (it.id === cid) return { ...it, liked_by_me: r.liked, like_count: r.like_count }; + return it.replies ? { ...it, replies: patch(it.replies) } : it; + }); + setComments((m) => patch(m)); + } catch { Taro.showToast({ title: '点赞失败', icon: 'none' }); } + }; const send = async () => { if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; } if (!draft.trim()) return; setSending(true); try { - const c = await addComment(id, draft.trim()); - if (c) { setComments((m) => [c, ...m]); setDraft(''); Taro.showToast({ title: '已留言', icon: 'success' }); } + const c = await addComment(id, draft.trim(), replyTo ? replyTo.id : ''); + if (c) { + setDraft(''); + setComments((m) => { + if (!replyTo) return [{ ...c, replies: [], reply_count: 0 }, ...m]; + // 回复:挂到对应顶层评论(顶层或其回复均挂根) + const rootId = replyTo.id; + const findRoot = (rid, list) => list.some((it) => it.id === rid) + ? rid + : (list.find((it) => (it.replies || []).some((rp) => rp.id === rid)) || {}).id; + const rid = findRoot(rootId, m) || rootId; + return m.map((it) => it.id === rid + ? { ...it, replies: [...(it.replies || []), c], reply_count: (it.reply_count || 0) + 1 } + : it); + }); + setReplyTo(null); + Taro.showToast({ title: '已回复', icon: 'success' }); + } } catch (e) { Taro.showToast({ title: (e && e.message) || '留言失败', icon: 'none' }); } finally { setSending(false); } }; @@ -55,14 +133,15 @@ export default function ContentDetail() { {!isSmall && (item.video ?