style(content): 小程序资讯中心两种卡片自由混排

- 行式卡片(缩略图左+类别标签+标题+作者+时间)
- 大图卡片(整幅图+播放角标+标题+作者+点赞),每 3 篇一篇大图,其余行式混排
- 无 image 字段时用类别渐变占位;作者统一「云超服」+ 相对时间
- 暗色令牌风格,分类高亮色(政策蓝/资讯青/技能橙/动态紫)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-27 14:26:04 +08:00
parent 05d19aed0f
commit a0c835d9d5
2 changed files with 114 additions and 18 deletions
+73 -12
View File
@@ -6,13 +6,79 @@ import { useTabIndex } from '@/utils/tab';
import Skeleton from '@/components/skeleton';
import './index.scss';
/** 资讯中心(C 端 tab):按类别(政策/资讯/技能/动态)展示 admin 发布的资讯。 */
/** 类别视觉(无 image 字段时用类别渐变占位) */
const CAT_META = {
policy: { label: '政策', from: '#3b82f6', to: '#2563eb' },
news: { label: '资讯', from: '#10b981', to: '#0ea5e9' },
skill: { label: '技能', from: '#f59e0b', to: '#ef4444' },
dynamic: { label: '动态', from: '#8b5cf6', to: '#ec4899' },
};
const catMeta = (t) => CAT_META[t] || CAT_META.news;
function timeAgo(s) {
if (!s) return '';
const d = new Date(s).getTime();
if (Number.isNaN(d)) return '';
const mins = Math.max(0, Math.floor((Date.now() - d) / 60000));
if (mins < 60) return `${mins} 分钟前`;
if (mins < 1440) return `${Math.floor(mins / 60)} 小时前`;
return `${Math.floor(mins / 1440)} 天前`;
}
/** 行式卡片(缩略图左 + 类别/标题/作者 + 时间) */
function CardRow({ it, go }) {
const m = catMeta(it.type);
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>
<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>
<Text className="ct-dot">·</Text>
<Text className="ct-time">{timeAgo(it.created_at)}</Text>
</View>
</View>
</View>
);
}
/** 大图卡片(整幅图 + 播放角标 + 标题 + 作者 + 点赞) */
function CardBig({ it, go, liked, onLike }) {
const m = catMeta(it.type);
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>
<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>
<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); }}>
<Text className="ct-like-ic">{liked ? '♥' : '♡'}</Text>
</View>
</View>
</View>
</View>
);
}
/** 资讯中心(C 端 tab):两种卡片样式自由混排(3 篇一图)。 */
export default function ContentIndex() {
useTabIndex(3);
const [cat, setCat] = useState('policy');
const [items, setItems] = useState([]);
const [err, setErr] = useState('');
const [loaded, setLoaded] = useState(false);
const [liked, setLiked] = useState({});
useEffect(() => {
setLoaded(false); setErr('');
@@ -24,6 +90,9 @@ export default function ContentIndex() {
const go = (id) => Taro.navigateTo({ url: `/pages/content-detail/index?id=${id}` });
const goQuiz = () => Taro.navigateTo({ url: '/pages/policy/index' });
const toggleLike = (id) => setLiked((m) => ({ ...m, [id]: !m[id] }));
const featured = (i) => i % 3 === 0;
return (
<View className="ct page-in">
@@ -46,20 +115,12 @@ export default function ContentIndex() {
{loaded ? (
items.length === 0
? <View className="ct-empty"><Text>暂无{CONTENT_CATS.find((c) => c.key === cat)?.label || ''}内容</Text></View>
: items.map((it) => (
<View key={it.id} className="ct-item" onClick={() => go(it.id)}>
<Text className="ct-item-title">{it.title}</Text>
{it.summary ? <Text className="ct-item-summary">{it.summary}</Text> : null}
<Text className="ct-item-time">{fmt(it.created_at)}</Text>
</View>
))
: items.map((it, i) => featured(i)
? <CardBig key={it.id} it={it} go={go} liked={!!liked[it.id]} onLike={toggleLike} />
: <CardRow key={it.id} it={it} go={go} />)
) : (
<Skeleton />
)}
</View>
);
}
function fmt(s) {
return (s || '').replace('T', ' ').replace('Z', '').slice(0, 16);
}
+41 -6
View File
@@ -1,6 +1,7 @@
/* 资讯中心 · 小程序样式(暗色令牌一致) */
/* 资讯中心 · 小程序样式(暗色令牌一致,两种卡片自由混排 */
.ct { position: relative; z-index: 2; padding: 16rpx 24rpx 60rpx; }
/* 类别分段 */
.ct-cats { display: flex; gap: 10rpx; padding: 8rpx 0 16rpx; }
.ct-cat {
flex: 1; text-align: center; padding: 14rpx 0; border-radius: 12rpx;
@@ -8,18 +9,52 @@
}
.ct-cat.on { background: #fff; color: #111; border-color: #fff; font-weight: 600; }
/* 政策测评入口 */
.ct-quiz {
display: flex; align-items: center; justify-content: space-between;
padding: 18rpx 22rpx; margin-bottom: 8rpx; border-radius: 14rpx;
padding: 18rpx 22rpx; margin-bottom: 12rpx; border-radius: 14rpx;
background: linear-gradient(180deg, #14141a, #0e0e12); border: 1rpx solid rgba(255,255,255,.12);
}
.ct-quiz-l { font-size: 27rpx; color: #fff; font-weight: 600; }
.ct-quiz-d { font-size: 24rpx; color: #ffd28a; }
.ct-item { padding: 22rpx 4rpx; border-bottom: 1rpx solid rgba(255,255,255,.08); }
.ct-item-title { display: block; font-size: 29rpx; color: #fff; font-weight: 600; line-height: 1.5; }
.ct-item-summary { display: block; margin-top: 8rpx; font-size: 25rpx; color: #a9a9ac; line-height: 1.6; }
.ct-item-time { display: block; margin-top: 10rpx; font-size: 22rpx; color: #7c7c80; }
/* ---------- 行式卡片(缩略图左) ---------- */
.ct-row { display: flex; gap: 18rpx; padding: 20rpx 2rpx; border-bottom: 1rpx solid rgba(255,255,255,.06); }
.ct-thumb {
width: 112rpx; height: 112rpx; border-radius: 16rpx; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.ct-thumb-char { font-size: 44rpx; color: #fff; font-weight: 700; }
.ct-row-bd { flex: 1; min-width: 0; }
.ct-tag { display: block; font-size: 22rpx; font-weight: 600; margin-bottom: 6rpx; }
.ct-row-title {
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
font-size: 28rpx; color: #fff; line-height: 1.45; font-weight: 600;
}
.ct-row-meta { display: flex; align-items: center; gap: 8rpx; margin-top: 12rpx; }
.ct-ava { width: 28rpx; height: 28rpx; border-radius: 50%; flex-shrink: 0; }
.ct-author { font-size: 22rpx; color: #a9a9ac; }
.ct-dot { font-size: 22rpx; color: #6d6d72; }
.ct-time { font-size: 22rpx; color: #7c7c80; }
/* ---------- 大图卡片 ---------- */
.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-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;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,.4);
}
.ct-card-bd { padding: 20rpx 22rpx 8rpx; }
.ct-card-title {
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
font-size: 31rpx; color: #fff; line-height: 1.4; font-weight: 700;
}
.ct-card-ad { display: flex; align-items: center; gap: 8rpx; margin-top: 16rpx; padding-bottom: 22rpx; }
.ct-like { margin-left: auto; width: 56rpx; height: 56rpx; border-radius: 50%; background: #1d1d22; display: flex; align-items: center; justify-content: center; }
.ct-like-ic { font-size: 30rpx; color: #9b9b9b; }
.ct-like.on .ct-like-ic { color: #ff3b5c; }
.ct-like.on { background: #2a1520; }
.ct-empty { padding: 80rpx 0; text-align: center; font-size: 26rpx; color: #8e8e8e; }
.ct-err { display: block; padding: 60rpx 0; text-align: center; font-size: 26rpx; color: #f0b8b8; }