feat(小程序): 资讯详情点赞/分享落库 + 微信式评论区(回复嵌套/评论点赞/折叠) + 动作条图标数字弱化恒显0 + 富文本媒体限宽 + 修hooks越界(#310)
This commit is contained in:
@@ -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
|
||||
? <Video className="cd-video" src={item.video} poster={item.video_cover || item.cover || ''} controls />
|
||||
: img
|
||||
? <Image className="cd-cover" src={img} mode="widthFix" />
|
||||
? <Image className="cd-cover" src={img} mode="aspectFill" />
|
||||
: null)}
|
||||
|
||||
{/* 标题 + 元信息 */}
|
||||
<Text className="cd-title">{item.title}</Text>
|
||||
<View className="cd-meta-row">
|
||||
{item.publisher_avatar ? <Image className="cd-ava" src={item.publisher_avatar} mode="aspectFill" /> : <View className="cd-ava" />}
|
||||
<Text className="cd-author">{author}</Text><View className="ic ic-check cd-badge-ic" />
|
||||
{item.publisher_avatar ? <Image className='cd-ava' src={item.publisher_avatar} mode='aspectFill' /> : null}
|
||||
<Text className="cd-author">{author}</Text>
|
||||
<View className={`pub-badge ${pubBadge.cls}`}><View className={`ic ${pubBadge.ic} pub-badge-ic`} /><Text className="pub-badge-t">{pubBadge.label}</Text></View>
|
||||
</View>
|
||||
<View className="cd-meta-line">
|
||||
<Text className="cd-meta-at">{rw(item.created_at)}</Text>
|
||||
@@ -73,7 +152,7 @@ export default function ContentDetail() {
|
||||
{/* 正文 */}
|
||||
{item.summary ? <Text className="cd-summary">{item.summary}</Text> : null}
|
||||
{item.body
|
||||
? <RichText className="cd-body" nodes={item.body} />
|
||||
? <RichText className="cd-body" nodes={fitMedia(item.body)} />
|
||||
: <Text className="cd-body">暂无正文</Text>}
|
||||
{images.length > 0 && (
|
||||
<ScrollView className="cd-gallery" scrollX>
|
||||
@@ -84,38 +163,73 @@ export default function ContentDetail() {
|
||||
<Button className="btn btn-ghost cd-link" type="default" onClick={() => Taro.setClipboardData({ data: item.link })}>原文链接(点击复制)</Button>
|
||||
) : null}
|
||||
|
||||
{/* 作者条 + 动作 */}
|
||||
<View className="cd-bar">
|
||||
<View className="cd-bar-left">
|
||||
{item.publisher_avatar ? <Image className="cd-ava2" src={item.publisher_avatar} mode="aspectFill" /> : <View className="cd-ava2" />}
|
||||
<Text className="cd-author">{author}</Text><View className="ic ic-check cd-badge-ic" />
|
||||
</View>
|
||||
<View className="cd-bar-actions">
|
||||
<View className="cd-act" onClick={() => setLike((v) => !v)}><View className={`ic ic-thumb cd-act-ic${like ? ' on' : ''}`} /><Text className="cd-act-n">{like ? 1 : 0}</Text></View>
|
||||
<View className="cd-act" onClick={copyLink}><View className="ic ic-share cd-act-ic" /><Text className="cd-act-n">转发</Text></View>
|
||||
<View className="cd-act" onClick={() => setFav((v) => !v)}><View className={`ic ic-heart cd-act-ic${fav ? ' on' : ''}`} /></View>
|
||||
<View className="cd-act" onClick={scrollToComment}><View className="ic ic-cmt cd-act-ic" /><Text className="cd-act-n">留言</Text></View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 阅读 + 留言 */}
|
||||
<View className="cd-read">阅读 {views}</View>
|
||||
{/* 留言区(标题+阅读数一行,动作按钮右侧) */}
|
||||
<View className="cd-comments">
|
||||
<Text className="cd-cmt-title">留言</Text>
|
||||
<View className="cd-cmt-headrow">
|
||||
<View className="cd-cmt-title-wrap">
|
||||
<Text className="cd-cmt-title">留言</Text>
|
||||
<Text className="cd-cmt-read">{views} 次阅读</Text>
|
||||
</View>
|
||||
<View className="cd-bar-actions">
|
||||
<Button className="cd-share-btn" openType="share">
|
||||
<View className="ic ic-share cd-act-ic" />
|
||||
<Text className="cd-act-n">{shareCount || 0}</Text>
|
||||
</Button>
|
||||
<View className="cd-act" onClick={doLike}>
|
||||
<View className={`ic ic-heart cd-act-ic${liked ? ' on' : ''}`} />
|
||||
<Text className="cd-act-n">{likeCount || 0}</Text>
|
||||
</View>
|
||||
<View className="cd-act" onClick={scrollToComment}><View className="ic ic-cmt cd-act-ic" /></View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="cd-cmt-input">
|
||||
<Input className="cd-cmt-field" placeholder="写留言…" value={draft} onInput={(e) => setDraft(e.detail.value)} />
|
||||
<View className="cd-cmt-tools"><View className="ic ic-cmt" /></View>
|
||||
<Button className="btn btn-sm cd-cmt-send" disabled={sending || !draft.trim()} onClick={send}>发送</Button>
|
||||
<Input className="cd-cmt-field" placeholder={replyTo ? `回复 ${replyTo.name}…` : '写留言…'} value={draft} onInput={(e) => setDraft(e.detail.value)} />
|
||||
{replyTo && <Text className="cd-cmt-cancel" onClick={() => setReplyTo(null)}>取消</Text>}
|
||||
<Button className="cd-cmt-send" disabled={sending || !draft.trim()} onClick={send}>发送</Button>
|
||||
</View>
|
||||
{comments.length === 0
|
||||
? <Text className="cd-cmt-empty">还没有留言,来写首评</Text>
|
||||
: comments.map((c) => (
|
||||
<View key={c.id} className="cd-cmt">
|
||||
<View className="cd-cmt-head">
|
||||
{c.avatar ? <Image className="cd-ava" src={c.avatar} mode="aspectFill" /> : <View className="cd-ava" />}
|
||||
<View className="cd-cmt-meta"><Text className="cd-cmt-name">{c.nickname || c.username || '用户'}</Text><Text className="cd-cmt-time">{rw(c.created_at)}</Text></View>
|
||||
<View className="cd-cmt-head" onClick={() => startReply(c)}>
|
||||
{c.avatar ? <Image className='cd-ava' src={c.avatar} mode='aspectFill' /> : null}
|
||||
<Text className="cd-cmt-name">{c.nickname || c.username || '用户'}</Text>
|
||||
<Text className="cd-cmt-time">{rw(c.created_at)}</Text>
|
||||
</View>
|
||||
<Text className="cd-cmt-text">{c.content}</Text>
|
||||
<View className="cd-cmt-foot">
|
||||
<Text className="cd-cmt-reply" onClick={() => startReply(c)}>回复</Text>
|
||||
<View className={`cd-cmt-like${c.liked_by_me ? ' on' : ''}`} onClick={() => doCommentLike(c.id)}>
|
||||
<View className="ic ic-heart cd-cmt-like-ic" />
|
||||
<Text className="cd-cmt-like-n">{c.like_count || 0}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{(c.replies || []).length > 0 && (
|
||||
<View className="cd-cmt-replies">
|
||||
{(expanded[c.id] ? c.replies : c.replies.slice(0, 2)).map((rp) => (
|
||||
<View key={rp.id} className="cd-cmt cd-cmt-sub">
|
||||
<View className="cd-cmt-head" onClick={() => startReply(rp)}>
|
||||
{rp.avatar ? <Image className='cd-ava cd-ava-sm' src={rp.avatar} mode='aspectFill' /> : null}
|
||||
<Text className="cd-cmt-name">{rp.nickname || rp.username || '用户'}</Text>
|
||||
<Text className="cd-cmt-time">{rw(rp.created_at)}</Text>
|
||||
</View>
|
||||
<Text className="cd-cmt-text">{rp.content}</Text>
|
||||
<View className="cd-cmt-foot">
|
||||
<Text className="cd-cmt-reply" onClick={() => startReply(rp)}>回复</Text>
|
||||
<View className={`cd-cmt-like${rp.liked_by_me ? ' on' : ''}`} onClick={() => doCommentLike(rp.id)}>
|
||||
<View className="ic ic-heart cd-cmt-like-ic" />
|
||||
<Text className="cd-cmt-like-n">{rp.like_count || 0}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
{c.replies.length > 2 && (
|
||||
<Text className="cd-cmt-expand" onClick={() => setExpanded((e) => ({ ...e, [c.id]: !e[c.id] }))}>
|
||||
{expanded[c.id] ? '收起回复' : `展开 ${c.reply_count} 条回复`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* 资讯详情 · Soft UI 风格(浅色柔彩) */
|
||||
.cd { position: relative; z-index: 2; padding: 0 0 60rpx; min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f7f9ff 0%, #f3eefc 100%); }
|
||||
.cd::before { content: ''; position: fixed; inset: 0; z-index: -1;
|
||||
background: radial-gradient(50% 40% at 88% 6%, rgba(158,225,236,.30), transparent 60%),
|
||||
radial-gradient(46% 40% at 10% 10%, rgba(229,167,224,.22), transparent 60%); }
|
||||
.cd-cover { width: 100%; aspect-ratio: 3.35 / 1; object-fit: cover; border-radius: 20rpx; display: block; margin: 20rpx 24rpx 0; width: calc(100% - 48rpx); }
|
||||
.cd { position: relative; z-index: 2; padding: 0 0 60rpx; min-height: 100vh; }
|
||||
/* 背景统一走全局 page(app.scss),本页不再叠渐变/光斑,避免顶部出现色差条 */
|
||||
.cd-cover { display: block; width: calc(100% - 48rpx); height: calc((100vw - 48rpx) / 3.35); object-fit: cover; border-radius: 20rpx; margin: 20rpx 24rpx 0; }
|
||||
.cd-video { width: calc(100% - 48rpx); height: 400rpx; border-radius: 20rpx; margin: 20rpx 24rpx 0; }
|
||||
.cd-title { display: block; margin-top: 24rpx; padding: 0 28rpx; font-size: 38rpx; color: #23315c; font-weight: 700; line-height: 1.4; }
|
||||
.cd-meta-row { display: flex; align-items: center; gap: 10rpx; padding: 20rpx 28rpx 0; }
|
||||
@@ -12,6 +9,15 @@
|
||||
.cd-ava2 { width: 44rpx; height: 44rpx; border-radius: 50%; background: linear-gradient(135deg, #e5a7e0, #9ee1ec); flex-shrink: 0; }
|
||||
.cd-author { font-size: 25rpx; color: #5a6aa3; display: inline-flex; align-items: center; gap: 4rpx; }
|
||||
.cd-badge-ic { width: 22rpx; height: 22rpx; color: #6284ff; margin-left: 6rpx; }
|
||||
|
||||
/* 发布者徽章(官方/园区/OPC) */
|
||||
.pub-badge { display: inline-flex; align-items: center; gap: 4rpx; margin-left: 8rpx;
|
||||
padding: 2rpx 12rpx 2rpx 8rpx; border-radius: 999rpx; flex-shrink: 0; }
|
||||
.pub-badge-ic { width: 20rpx; height: 20rpx; background-color: #fff; }
|
||||
.pub-badge-t { font-size: 18rpx; color: #fff; line-height: 1.4; }
|
||||
.pub-badge--official { background: linear-gradient(135deg, #4f7dff, #2f5bf6); box-shadow: 0 4rpx 10rpx rgba(47,91,246,.28); }
|
||||
.pub-badge--park { background: linear-gradient(135deg, #2fbf9b, #14a37f); box-shadow: 0 4rpx 10rpx rgba(20,163,127,.28); }
|
||||
.pub-badge--opc { background: linear-gradient(135deg, #b06ef2, #8f4ce0); box-shadow: 0 4rpx 10rpx rgba(143,76,224,.28); }
|
||||
.cd-meta-line { display: flex; align-items: center; gap: 8rpx; padding: 8rpx 28rpx 0; }
|
||||
.cd-meta-at { font-size: 23rpx; color: #9aa4c6; }
|
||||
.cd-dot { font-size: 23rpx; color: #c0c7e2; }
|
||||
@@ -27,26 +33,56 @@
|
||||
.cd-bar { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 28rpx; margin: 26rpx 24rpx 0;
|
||||
border-radius: 20rpx; background: rgba(255,255,255,.82); box-shadow: 0 8rpx 24rpx rgba(59,79,125,.08); }
|
||||
.cd-bar-left { display: flex; align-items: center; gap: 12rpx; }
|
||||
.cd-bar-actions { display: flex; align-items: center; gap: 2rpx; }
|
||||
.cd-act { display: flex; flex-direction: column; align-items: center; gap: 2rpx; padding: 6rpx 14rpx; border-radius: 10rpx; }
|
||||
.cd-act-ic { width: 34rpx; height: 34rpx; color: #9aa4c6; }
|
||||
/* 动作组:参考公众号式——图标在上、数字在下,大小颜色与「N 次阅读」一致 */
|
||||
.cd-bar-actions { display: flex; align-items: center; gap: 30rpx; }
|
||||
.cd-act { display: flex; flex-direction: column; align-items: center; gap: 2rpx; padding: 0; }
|
||||
.cd-act:active { transform: scale(.9); opacity: .7; }
|
||||
.cd-act-ic { width: 26rpx; height: 26rpx; color: #9aa4c6; }
|
||||
.cd-act-ic.on { color: #6284ff; }
|
||||
.cd-act-n { font-size: 20rpx; color: #a3abc9; }
|
||||
.cd-act-n { font-size: 20rpx; line-height: 1.2; color: #9aa4c6; }
|
||||
|
||||
/* 阅读 + 留言 */
|
||||
.cd-read { padding: 24rpx 28rpx 0; font-size: 24rpx; color: #9aa4c6; }
|
||||
.cd-comments { padding: 20rpx 28rpx 0; margin-top: 20rpx; }
|
||||
.cd-cmt-title { display: block; font-size: 30rpx; color: #23315c; font-weight: 700; margin-bottom: 16rpx; }
|
||||
.cd-cmt-input { display: flex; align-items: center; gap: 12rpx; background: rgba(255,255,255,.85); border: 1rpx solid rgba(99,132,255,.16); border-radius: 16rpx; padding: 10rpx 16rpx; }
|
||||
.cd-comments { padding: 20rpx 28rpx 0; margin-top: 8rpx; }
|
||||
.cd-cmt-headrow { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
|
||||
.cd-cmt-headrow .cd-bar-actions { display: flex; align-items: center; gap: 10rpx; }
|
||||
.cd-cmt-title-wrap { display: flex; align-items: baseline; gap: 12rpx; }
|
||||
.cd-cmt-title { display: inline; font-size: 30rpx; color: #23315c; font-weight: 700; }
|
||||
.cd-cmt-read { font-size: 22rpx; color: #9aa4c6; }
|
||||
.cd-cmt-input { display: flex; align-items: center; gap: 12rpx; background: rgba(255,255,255,.85);
|
||||
border: 1rpx solid rgba(99,132,255,.16); border-radius: 999rpx; padding: 8rpx 8rpx 8rpx 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(59,79,125,.05); }
|
||||
.cd-cmt-field { flex: 1; color: #3a4672; font-size: 26rpx; }
|
||||
.cd-cmt-tools .ic { width: 32rpx; height: 32rpx; color: #7a86b0; }
|
||||
.cd-cmt-send { margin: 0; padding: 0 20rpx; font-size: 22rpx; line-height: 2; }
|
||||
.cd-cmt-send { margin: 0; padding: 0 30rpx; font-size: 24rpx; line-height: 2.1;
|
||||
border-radius: 999rpx; border: none; color: #fff;
|
||||
background: linear-gradient(135deg, #6284ff 0%, #7b5ce6 100%);
|
||||
box-shadow: 0 6rpx 16rpx rgba(98,132,255,.32); }
|
||||
.cd-cmt-send[disabled] { opacity: .45; }
|
||||
.cd-cmt-empty { display: block; padding: 60rpx 0; text-align: center; font-size: 24rpx; color: #a3abc9; }
|
||||
/* 评论动作行:回复 + 点赞(微信式) */
|
||||
.cd-cmt-foot { display: flex; align-items: center; gap: 28rpx; margin-top: 8rpx; }
|
||||
.cd-cmt-reply { font-size: 23rpx; color: #6284ff; }
|
||||
.cd-cmt-like { display: flex; align-items: center; gap: 6rpx; }
|
||||
.cd-cmt-like-ic { width: 26rpx; height: 26rpx; }
|
||||
.cd-cmt-like-n { font-size: 22rpx; color: #9aa4c6; }
|
||||
.cd-cmt-like.on .cd-cmt-like-ic { background-color: #e05a6d; }
|
||||
/* 回复嵌套:左缩进 + 竖线 */
|
||||
.cd-cmt-replies { margin-top: 12rpx; padding-left: 24rpx; border-left: 3rpx solid rgba(99,132,255,.18); }
|
||||
.cd-cmt-sub { margin-top: 14rpx; }
|
||||
.cd-ava-sm { width: 44rpx; height: 44rpx; }
|
||||
.cd-cmt-sub .cd-cmt-text { font-size: 25rpx; }
|
||||
.cd-cmt-expand { display: block; margin-top: 12rpx; font-size: 23rpx; color: #6284ff; }
|
||||
.cd-cmt-cancel { flex-shrink: 0; font-size: 22rpx; color: #9aa4c6; padding: 0 12rpx; }
|
||||
.cd-cmt { margin-top: 22rpx; }
|
||||
.cd-cmt-head { display: flex; align-items: center; gap: 12rpx; }
|
||||
.cd-cmt-meta { display: flex; flex-direction: column; }
|
||||
.cd-cmt-name { font-size: 24rpx; color: #7a86b0; }
|
||||
.cd-cmt-time { font-size: 20rpx; color: #7a86b0; }
|
||||
/* 时间推到行右侧(头像 · 昵称 ·——时间) */
|
||||
.cd-cmt-time { margin-left: auto; font-size: 20rpx; color: #9aa4c6; }
|
||||
.cd-cmt-text { display: block; margin-top: 10rpx; font-size: 27rpx; color: #3a4672; line-height: 1.6; }
|
||||
|
||||
.cd-err { display: block; padding: 90rpx 0; text-align: center; font-size: 26rpx; color: #9aa4c6; }
|
||||
|
||||
/* 微信分享按钮(openType=share)样式重置为图标按钮 */
|
||||
.cd-share-btn { display: flex; flex-direction: column; align-items: center; gap: 2rpx;
|
||||
padding: 0; margin: 0; background: transparent; border: none; line-height: 1; }
|
||||
.cd-share-btn::after { border: none; }
|
||||
|
||||
@@ -31,8 +31,26 @@ export async function getComments(id) {
|
||||
return Array.isArray(r?.items) ? r.items : [];
|
||||
}
|
||||
|
||||
/** 发表留言(需登录) */
|
||||
export async function addComment(id, content) {
|
||||
const r = await request('POST', `/opc/content/${id}/comments`, { content }, true);
|
||||
/** 发表留言/回复(需登录);replyTo 为父评论 id(空=顶层留言) */
|
||||
export async function addComment(id, content, replyTo = '') {
|
||||
const r = await request('POST', `/opc/content/${id}/comments`, { content, reply_to: replyTo }, true);
|
||||
return r?.comment || null;
|
||||
}
|
||||
|
||||
/** 评论点赞/取消(登录,一人一赞);返回 {liked, like_count} */
|
||||
export async function toggleCommentLike(commentId) {
|
||||
const r = await request('POST', `/opc/comments/${commentId}/like`, {}, true);
|
||||
return r && r.liked !== undefined ? r : (r && r.data) || r;
|
||||
}
|
||||
|
||||
/** 点赞/取消点赞(登录);返回 {liked, like_count} */
|
||||
export async function toggleLike(id) {
|
||||
const r = await request('POST', `/opc/content/${id}/like`, {}, true);
|
||||
return r && r.liked !== undefined ? r : (r && r.data) || r;
|
||||
}
|
||||
|
||||
/** 分享计数 +1(公开,可重复分享);返回 {share_count} */
|
||||
export async function reportShare(id) {
|
||||
const r = await request('POST', `/opc/content/${id}/share`, {}, false).catch(() => null);
|
||||
return r || null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user