- 完成 T-011: TanStack Query 数据获取 hooks - 完成 T-012: 内容卡片组件 ContentCard - 完成 T-013: 响应式网格布局 + 骨架屏 - 完成 T-014: 平台 Tab 切换 - 完成 T-015: 排序工具栏 - 完成 T-016: 内容详情页 - 完成 T-017: 自动刷新机制 - 完成 T-018: 手动刷新 + 刷新时间 - 完成 T-019: 首页组装 - 完成 T-020: 收藏 store - 完成 T-021: 收藏按钮组件 - 完成 T-022: 收藏页面 - 完成 T-023: 设置页面 - API Key - 完成 T-024: 设置页面 - 刷新间隔 - 完成 T-025: Toast 通知 (sonner) - 完成 T-026: 错误/空状态组件 Co-Authored-By: Claude <noreply@anthropic.com>
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { Play, Heart, MessageCircle } from "lucide-react";
|
|
import { getPlatformConfig } from "@/lib/platforms";
|
|
import { formatCount } from "@/lib/format";
|
|
import { FavoriteButton } from "@/components/common/FavoriteButton";
|
|
import type { ContentItem } from "@/types/content";
|
|
import { useState } from "react";
|
|
|
|
interface ContentCardProps {
|
|
item: ContentItem;
|
|
}
|
|
|
|
export function ContentCard({ item }: ContentCardProps) {
|
|
const platform = getPlatformConfig(item.platform);
|
|
const [imgError, setImgError] = useState(false);
|
|
|
|
const playCount = formatCount(item.play_count);
|
|
const likeCount = formatCount(item.like_count);
|
|
const commentCount = formatCount(item.comment_count);
|
|
|
|
return (
|
|
<Link
|
|
href={`/detail/${item.platform}/${encodeURIComponent(item.id)}`}
|
|
className="group block rounded-lg border border-slate-200 bg-white shadow-sm overflow-hidden transition-all hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
{/* Cover image */}
|
|
<div className="relative aspect-[4/3] bg-slate-100 overflow-hidden">
|
|
{item.cover_url && !imgError ? (
|
|
<Image
|
|
src={item.cover_url}
|
|
alt={item.title}
|
|
fill
|
|
className="object-cover"
|
|
loading="lazy"
|
|
sizes="(max-width: 640px) 100vw, (max-width: 960px) 50vw, (max-width: 1240px) 33vw, 25vw"
|
|
onError={() => setImgError(true)}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-full text-3xl text-slate-300">
|
|
{platform?.icon || "📄"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-3">
|
|
{/* Platform tag */}
|
|
{platform && (
|
|
<span
|
|
className="inline-block text-xs px-1.5 py-0.5 rounded mb-1.5"
|
|
style={{
|
|
backgroundColor: `${platform.color}15`,
|
|
color: platform.color,
|
|
}}
|
|
>
|
|
{platform.icon} {platform.name}
|
|
</span>
|
|
)}
|
|
|
|
{/* Title */}
|
|
<h3 className="text-sm font-medium text-slate-800 line-clamp-2 mb-2 leading-snug">
|
|
{item.title}
|
|
</h3>
|
|
|
|
{/* Author */}
|
|
<div className="flex items-center gap-1.5 mb-2">
|
|
{item.author_avatar ? (
|
|
<Image
|
|
src={item.author_avatar}
|
|
alt={item.author_name}
|
|
width={20}
|
|
height={20}
|
|
className="rounded-full object-cover"
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = "none";
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="w-5 h-5 rounded-full bg-slate-200 flex items-center justify-center text-[10px] text-slate-500">
|
|
👤
|
|
</div>
|
|
)}
|
|
<span className="text-xs text-slate-500 truncate">
|
|
{item.author_name}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Stats + Favorite */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 text-xs text-slate-400">
|
|
{playCount && (
|
|
<span className="flex items-center gap-0.5">
|
|
<Play className="w-3 h-3" />
|
|
{playCount}
|
|
</span>
|
|
)}
|
|
{likeCount && (
|
|
<span className="flex items-center gap-0.5">
|
|
<Heart className="w-3 h-3" />
|
|
{likeCount}
|
|
</span>
|
|
)}
|
|
{commentCount && (
|
|
<span className="flex items-center gap-0.5">
|
|
<MessageCircle className="w-3 h-3" />
|
|
{commentCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<FavoriteButton item={item} />
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
}
|