feat(content): C端资讯卡片/详情 封面·视频·发布人
- 小程序 content 卡片:封面(有则图,无则类别渐变)、视频播放角标、作者(昵称/头像,回落云超服/渐变) - content-detail:视频或封面 + 发布人行 + 图集横滑 + 外链复制 + 正文 - 网站 Content.jsx:大图/行式混排卡片 + 封面/播放角标/发布人 + 详情展开(图集/外链) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { View, Text, Image, Video, ScrollView, Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getContent } from '@/utils/content';
|
||||
import './index.scss';
|
||||
|
||||
/** 资讯详情:展示单条 title/summary/body。 */
|
||||
/** 资讯详情:封面/视频 + 正文 + 图集 + 外链 + 发布人。 */
|
||||
export default function ContentDetail() {
|
||||
const [item, setItem] = useState(null);
|
||||
const [err, setErr] = useState('');
|
||||
@@ -20,12 +20,45 @@ export default function ContentDetail() {
|
||||
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 img = item.cover || (item.video ? item.video_cover : '');
|
||||
const images = Array.isArray(item.images) ? item.images : [];
|
||||
|
||||
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>
|
||||
<Text className="cd-meta">发布于 {fmt(item.created_at)}</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">{item.publisher_name || '云超服'}</Text>
|
||||
<Text className="cd-dot">·</Text>
|
||||
<Text className="cd-meta-at">{fmt(item.created_at)}</Text>
|
||||
</View>
|
||||
|
||||
{item.summary ? <Text className="cd-summary">{item.summary}</Text> : null}
|
||||
<Text className="cd-body">{item.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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
/* 资讯详情 · 小程序样式(暗色令牌一致) */
|
||||
.cd { position: relative; z-index: 2; padding: 32rpx 28rpx 60rpx; }
|
||||
.cd-title { display: block; font-size: 36rpx; color: #fff; font-weight: 700; line-height: 1.4; }
|
||||
.cd-meta { display: block; margin-top: 14rpx; font-size: 23rpx; color: #7c7c80; }
|
||||
.cd-summary { display: block; margin-top: 20rpx; font-size: 27rpx; color: #c4c2c3; line-height: 1.7; }
|
||||
.cd-body { display: block; margin-top: 24rpx; font-size: 28rpx; color: #e4e4e6; line-height: 1.8; white-space: pre-wrap; }
|
||||
.cd { position: relative; z-index: 2; padding: 0 0 60rpx; }
|
||||
.cd-cover { display: block; width: 100%; border-radius: 20rpx; }
|
||||
.cd-video { width: 100%; height: 400rpx; border-radius: 20rpx; margin-bottom: 24rpx; }
|
||||
.cd-title { display: block; margin-top: 24rpx; padding: 0 28rpx; font-size: 38rpx; color: #fff; font-weight: 700; line-height: 1.4; }
|
||||
.cd-meta-row { display: flex; align-items: center; gap: 10rpx; padding: 20rpx 28rpx 0; }
|
||||
.cd-ava { width: 36rpx; height: 36rpx; border-radius: 50%; background: linear-gradient(135deg, #555, #333); }
|
||||
.cd-author { font-size: 24rpx; color: #c4c2c3; }
|
||||
.cd-dot { font-size: 24rpx; color: #6d6d72; }
|
||||
.cd-meta-at { font-size: 24rpx; color: #7c7c80; }
|
||||
.cd-summary { display: block; margin: 20rpx 28rpx 0; font-size: 27rpx; color: #c4c2c3; line-height: 1.7; }
|
||||
.cd-body { display: block; margin: 20rpx 28rpx 0; font-size: 28rpx; color: #e4e4e6; line-height: 1.8; white-space: pre-wrap; }
|
||||
.cd-gallery { display: flex; gap: 12rpx; margin: 24rpx 28rpx 0; white-space: nowrap; }
|
||||
.cd-gal-img { width: 240rpx; height: 240rpx; border-radius: 14rpx; flex-shrink: 0; margin-right: 12rpx; }
|
||||
.cd-link { margin: 28rpx 28rpx 0; }
|
||||
.cd-err { display: block; padding: 90rpx 0; text-align: center; font-size: 26rpx; color: #8e8e8e; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { listContent, CONTENT_CATS } from '@/utils/content';
|
||||
@@ -25,20 +25,30 @@ function timeAgo(s) {
|
||||
return `${Math.floor(mins / 1440)} 天前`;
|
||||
}
|
||||
|
||||
function Ava({ it, m }) {
|
||||
return it.publisher_avatar
|
||||
? <Image className="ct-ava" src={it.publisher_avatar} mode="aspectFill" />
|
||||
: <View className="ct-ava" style={{ background: `linear-gradient(135deg, ${m.from}, ${m.to})` }} />;
|
||||
}
|
||||
const authorName = (it) => it.publisher_name || '云超服';
|
||||
|
||||
/** 行式卡片(缩略图左 + 类别/标题/作者 + 时间) */
|
||||
function CardRow({ it, go }) {
|
||||
const m = catMeta(it.type);
|
||||
const img = it.cover || (it.video ? it.video_cover : '');
|
||||
return (
|
||||
<View className={`ct-row ct-row--in`} onClick={() => go(it.id)}>
|
||||
<View className="ct-thumb" style={{ background: `linear-gradient(135deg, ${m.from}, ${m.to})` }}>
|
||||
<Text className="ct-thumb-char">{m.label[0]}</Text>
|
||||
</View>
|
||||
{img
|
||||
? <Image className="ct-thumb" src={img} mode="aspectFill" />
|
||||
: <View className="ct-thumb" style={{ background: `linear-gradient(135deg, ${m.from}, ${m.to})` }}>
|
||||
<Text className="ct-thumb-char">{m.label[0]}</Text>
|
||||
</View>}
|
||||
<View className="ct-row-bd">
|
||||
<Text className="ct-tag" style={{ color: m.from }}>{m.label}</Text>
|
||||
<Text className="ct-row-title">{it.title}</Text>
|
||||
<View className="ct-row-meta">
|
||||
<View className="ct-ava" style={{ background: `linear-gradient(135deg, ${m.from}, ${m.to})` }} />
|
||||
<Text className="ct-author">云超服</Text>
|
||||
<Ava it={it} m={m} />
|
||||
<Text className="ct-author">{authorName(it)}</Text>
|
||||
<Text className="ct-dot">·</Text>
|
||||
<Text className="ct-time">{timeAgo(it.created_at)}</Text>
|
||||
</View>
|
||||
@@ -50,16 +60,18 @@ function CardRow({ it, go }) {
|
||||
/** 大图卡片(整幅图 + 播放角标 + 标题 + 作者 + 点赞) */
|
||||
function CardBig({ it, go, liked, onLike }) {
|
||||
const m = catMeta(it.type);
|
||||
const img = it.cover || (it.video ? it.video_cover : '');
|
||||
return (
|
||||
<View className="ct-card">
|
||||
<View className="ct-cover" style={{ background: `linear-gradient(135deg, ${m.from}, ${m.to})` }} onClick={() => go(it.id)}>
|
||||
<View className="ct-play">▶</View>
|
||||
<View className="ct-cover" style={img ? undefined : { background: `linear-gradient(135deg, ${m.from}, ${m.to})` }} onClick={() => go(it.id)}>
|
||||
{img ? <Image className="ct-cover-img" src={img} mode="aspectFill" /> : <Text className="ct-thumb-char">{m.label[0]}</Text>}
|
||||
{it.video && <View className="ct-play">▶</View>}
|
||||
</View>
|
||||
<View className="ct-card-bd">
|
||||
<Text className="ct-card-title" onClick={() => go(it.id)}>{it.title}</Text>
|
||||
<View className="ct-card-ad">
|
||||
<View className="ct-ava" style={{ background: `linear-gradient(135deg, ${m.from}, ${m.to})` }} />
|
||||
<Text className="ct-author">云超服</Text>
|
||||
<Ava it={it} m={m} />
|
||||
<Text className="ct-author">{authorName(it)}</Text>
|
||||
<Text className="ct-dot">·</Text>
|
||||
<Text className="ct-time">{timeAgo(it.created_at)}</Text>
|
||||
<View className={`ct-like${liked ? ' on' : ''}`} onClick={(e) => { e.stopPropagation(); onLike(it.id); }}>
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
|
||||
/* ---------- 大图卡片 ---------- */
|
||||
.ct-card { margin: 6rpx 0 18rpx; border-radius: 22rpx; overflow: hidden; background: #111116; border: 1rpx solid #222228; }
|
||||
.ct-cover { position: relative; width: 100%; aspect-ratio: 16 / 9; display: flex; align-items: center; justify-content: center; }
|
||||
.ct-cover { position: relative; width: 100%; aspect-ratio: 16 / 9; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
.ct-cover-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
||||
.ct-play { position: relative; z-index: 2; }
|
||||
.ct-play {
|
||||
width: 72rpx; height: 72rpx; border-radius: 50%; background: rgba(255,255,255,.9);
|
||||
color: #111; font-size: 30rpx; display: flex; align-items: center; justify-content: center;
|
||||
|
||||
@@ -31,21 +31,57 @@ export default function Content() {
|
||||
<section className="section" style={{ marginTop: 8 }}>
|
||||
{!loaded ? <p className="pf-muted">加载中…</p> : items.length === 0
|
||||
? <p className="pf-muted">暂无{CONTENT_CATS.find((c) => c.key === cat)?.label}内容</p>
|
||||
: items.map((it) => (
|
||||
<Reveal key={it.id} as="article" className="bk-ev-card" style={{ marginBottom: 10, padding: '16px 18px' }}>
|
||||
<div style={{ cursor: 'pointer' }} onClick={() => setOpenId(openId === it.id ? null : it.id)}>
|
||||
<h3 style={{ margin: 0, fontSize: 16 }}>{it.title}</h3>
|
||||
{it.summary && <p className="pf-muted" style={{ margin: '6px 0 0' }}>{it.summary}</p>}
|
||||
<p className="pf-muted" style={{ margin: '6px 0 0' }}>{fmt(it.created_at)}</p>
|
||||
</div>
|
||||
{openId === it.id && <p className="pf-body" style={{ whiteSpace: 'pre-wrap', marginTop: 10, lineHeight: 1.8 }}>{it.body || '暂无正文'}</p>}
|
||||
</Reveal>
|
||||
: items.map((it, i) => (
|
||||
<NewsCard key={it.id} it={it} big={i % 3 === 0} open={openId === it.id}
|
||||
onToggle={() => setOpenId(openId === it.id ? null : it.id)} />
|
||||
))}
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
const CAT_COLOR = { policy: '#3b82f6', news: '#10b981', skill: '#f59e0b', dynamic: '#8b5cf6' };
|
||||
|
||||
/** 资讯卡片:大图版(big) / 行式版,含封面/视频/发布人。 */
|
||||
function NewsCard({ it, big, open, onToggle }) {
|
||||
const m = CAT_COLOR[it.type] || '#10b981';
|
||||
const img = it.cover || (it.video ? it.video_cover : '');
|
||||
const author = it.publisher_name || '云超服';
|
||||
return (
|
||||
<Reveal as="article" className="bk-ev-card" style={{ marginBottom: 10, padding: big ? 0 : '14px 16px', overflow: 'hidden' }}>
|
||||
<div style={{ cursor: 'pointer' }} onClick={onToggle}>
|
||||
{big ? (
|
||||
<div style={{ position: 'relative', aspectRatio: '16/9', background: `linear-gradient(135deg, ${m}, ${m}cc)` }}>
|
||||
{img ? <img src={img} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} /> : null}
|
||||
{it.video ? <div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: 44, height: 44, borderRadius: '50%', background: 'rgba(255,255,255,.9)', color: '#111', display: 'grid', placeItems: 'center' }}>▶</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ padding: big ? '14px 16px 6px' : 0 }}>
|
||||
<h3 style={{ margin: 0, fontSize: big ? 17 : 15, lineHeight: 1.4 }}>{it.title}</h3>
|
||||
<p className="pf-muted" style={{ margin: '8px 0 0', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ display: 'inline-block', width: 20, height: 20, borderRadius: '50%', background: `linear-gradient(135deg, ${m}, ${m}cc)`, overflow: 'hidden' }}>
|
||||
{it.publisher_avatar ? <img src={it.publisher_avatar} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : null}
|
||||
</span>
|
||||
<span>{author}</span><span>·</span><span>{fmt(it.created_at)}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
<div style={{ padding: '0 16px 16px' }}>
|
||||
{it.summary && <p className="pf-muted">{it.summary}</p>}
|
||||
<p className="pf-body" style={{ whiteSpace: 'pre-wrap', lineHeight: 1.8 }}>{it.body || '暂无正文'}</p>
|
||||
{Array.isArray(it.images) && it.images.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 8, overflowX: 'auto', marginTop: 10 }}>
|
||||
{it.images.map((u, i) => <img key={i} src={u} alt="" style={{ width: 120, height: 120, borderRadius: 8, objectFit: 'cover', flexShrink: 0 }} />)}
|
||||
</div>
|
||||
)}
|
||||
{it.link && <a href={it.link} target="_blank" rel="noreferrer" style={{ display: 'inline-block', marginTop: 10, color: '#7fd087' }}>原文链接 ↗</a>}
|
||||
</div>
|
||||
)}
|
||||
</Reveal>
|
||||
);
|
||||
}
|
||||
|
||||
function fmt(s) {
|
||||
return (s || '').replace('T', ' ').replace('Z', '').slice(0, 16);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user