125 lines
6.4 KiB
React
125 lines
6.4 KiB
React
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 isSmall = item.card_mode === 'small'; // 小图模式:详情页不显示封面
|
||
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">
|
||
{/* 封面/视频(小图模式不显示封面) */}
|
||
{!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" />
|
||
: 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" />
|
||
</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><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-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>
|
||
</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>
|
||
);
|
||
}
|