Files
training/miniprogram/src/pages/content-detail/index.jsx
T
Pine bab70c8eee feat(content): 小程序详情富UI(阅读/动作条/留言)
- content-detail 微信文章式:标题 + 作者(头像/昵称/认证✓) + 日期 + 阅读X + 富文本正文
- 作者条+动作(点赞/转发=复制链接/收藏/写留言滚动)
- 阅读X + 留言区(输入框+😊🖼+发送+列表,未登录提示登录;空态"还没有留言,来写首评")
- utils/content.js +getComments/addComment

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 15:30:19 +08:00

124 lines
6.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { View, Text, Image, Video, ScrollView, Input, Button, RichText } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { getContent, getComments, addComment } from '@/utils/content';
import { isAuthed } from '@/utils/auth';
import './index.scss';
const rw = (s) => (s || '').replace('T', ' ').replace('Z', '').slice(0, 16);
/** 资讯详情(微信文章式):标题/作者/阅读 + 正文 + 动作条 + 留言。 */
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 [draft, setDraft] = useState('');
const [sending, setSending] = useState(false);
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));
getComments(id).then(setComments).catch(() => {});
}, []);
if (!loaded) return <View className="cd page-in"><Text className="cd-err">加载中</Text></View>;
if (err || !item) return <View className="cd page-in"><Text className="cd-err">{err || '内容不存在'}</Text></View>;
const id = item.id;
const img = item.cover || (item.video ? item.video_cover : '');
const images = Array.isArray(item.images) ? item.images : [];
const author = item.publisher_name || '云超服';
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 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' }); }
} catch (e) { Taro.showToast({ title: (e && e.message) || '留言失败', icon: 'none' }); }
finally { setSending(false); }
};
return (
<View className="cd page-in">
{/* 封面/视频 */}
{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" />
: 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 className="cd-badge"></Text></Text>
</View>
<View className="cd-meta-line">
<Text className="cd-meta-at">{rw(item.created_at)}</Text>
<Text className="cd-dot">·</Text>
<Text className="cd-meta-at">阅读 {views}</Text>
</View>
{/* 正文 */}
{item.summary ? <Text className="cd-summary">{item.summary}</Text> : null}
{item.body
? <RichText className="cd-body" nodes={item.body} />
: <Text className="cd-body">暂无正文</Text>}
{images.length > 0 && (
<ScrollView className="cd-gallery" scrollX>
{images.map((u, i) => <Image key={i} className="cd-gal-img" src={u} mode="aspectFill" />)}
</ScrollView>
)}
{item.link ? (
<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 className="cd-badge"></Text></Text>
</View>
<View className="cd-bar-actions">
<View className="cd-act" onClick={() => setLike((v) => !v)}><Text className={like ? 'cd-act-ic on' : 'cd-act-ic'}>👍</Text><Text className="cd-act-n">{like ? 1 : 0}</Text></View>
<View className="cd-act" onClick={copyLink}><Text className="cd-act-ic"></Text><Text className="cd-act-n">转发</Text></View>
<View className="cd-act" onClick={() => setFav((v) => !v)}><Text className={fav ? 'cd-act-ic on' : 'cd-act-ic'}></Text></View>
<View className="cd-act" onClick={scrollToComment}><Text className="cd-act-ic"></Text><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-input">
<Input className="cd-cmt-field" placeholder="写留言…" value={draft} onInput={(e) => setDraft(e.detail.value)} />
<View className="cd-cmt-tools"><Text>😊</Text><Text>🖼</Text></View>
<Button className="btn btn-sm 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>
<Text className="cd-cmt-text">{c.content}</Text>
</View>
))}
</View>
</View>
);
}