Files
jiang13-bbs/frontend/components/PinnedPosts.tsx
freefire ff2ab286fb feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

107 lines
4.0 KiB
TypeScript

import Link from "next/link";
import { Pin, Star } from "lucide-react";
import type { PostListItem } from "@/lib/api";
import { postPath } from "@/lib/urlStyle";
import { getPublicSettingsCached } from "@/lib/serverData";
import Avatar from "./Avatar";
import { IconComment, IconEye } from "./Icons";
function formatTime(dateStr: string) {
const d = new Date(dateStr);
return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, "0")}.${String(
d.getDate()
).padStart(2, "0")}`;
}
/**
* 管理员置顶区:独立于普通帖子列表。
* 陶土色语义(与 badge-pin 一致),紧凑排列,一眼识别为公告性质内容。
*/
export default async function PinnedPosts({
posts,
showBoard = true,
}: {
posts: PostListItem[];
showBoard?: boolean;
}) {
if (posts.length === 0) return null;
const urlStyle = (await getPublicSettingsCached()).url_style;
return (
<section className="mb-6" aria-label="置顶讨论">
<div className="flex items-center gap-2 mb-2.5 px-1">
<span
className="w-6 h-6 rounded-full flex items-center justify-center"
style={{ background: "var(--clay-soft)", color: "var(--clay)" }}
>
<Pin size={12} fill="currentColor" strokeWidth={2.4} />
</span>
<h2 className="text-[13.5px] font-extrabold tracking-wide" style={{ color: "var(--clay)" }}>
置顶讨论
</h2>
<span className="meta text-xs">管理员置顶 · {posts.length}</span>
</div>
<div className="panel overflow-hidden">
{posts.map((p, i) => (
<Link
key={p.id}
href={postPath(urlStyle, p.id)}
className="group relative flex items-center gap-3 sm:gap-3.5 px-4 sm:px-5 py-3.5 transition-colors hover:bg-[var(--item-hover-bg)] focus-visible:bg-[var(--item-hover-bg)] j13-fade-up"
style={{
...(i > 0 ? { borderTop: "1px solid var(--line)" } : {}),
animationDelay: `${i * 35}ms`,
}}
>
<Avatar name={p.user.nickname || p.user.username} src={p.user.avatar || undefined} size={36} square />
<div className="flex-1 min-w-0">
<h3
className="text-[14.5px] font-bold leading-snug truncate transition-colors group-hover:text-[var(--accent)]"
style={{ color: "var(--ink)" }}
>
{p.title}
</h3>
<div className="meta flex items-center gap-2 mt-1 text-xs min-w-0">
<span className="truncate">{p.user.nickname}</span>
<span aria-hidden>·</span>
<span className="shrink-0">{formatTime(p.created_at)}</span>
{showBoard && (
<>
<span aria-hidden>·</span>
<span className="truncate">{p.board.name}</span>
</>
)}
{p.recommended && (
<span
className="badge-rec shrink-0"
style={{ width: 16, height: 16 }}
title="推荐"
aria-label="推荐"
>
<Star size={10} fill="currentColor" strokeWidth={2.2} aria-hidden />
</span>
)}
</div>
</div>
<div className="hidden sm:flex items-center gap-3 shrink-0 meta text-[12.5px]">
<span className="inline-flex items-center gap-1.5" aria-label={`${p.comment_count} 条评论`}>
<IconComment size={14} /> {p.comment_count}
</span>
<span className="inline-flex items-center gap-1.5" aria-label={`${p.view_count} 次浏览`}>
<IconEye size={14} /> {p.view_count}
</span>
</div>
<Pin
size={13}
className="shrink-0 hidden sm:block"
style={{ color: "var(--clay)" }}
fill="currentColor"
strokeWidth={2.2}
/>
</Link>
))}
</div>
</section>
);
}