- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
import Link from "next/link";
|
|
import { Pin } from "lucide-react";
|
|
import type { PostListItem } from "@/lib/api";
|
|
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 function PinnedPosts({
|
|
posts,
|
|
showBoard = true,
|
|
}: {
|
|
posts: PostListItem[];
|
|
showBoard?: boolean;
|
|
}) {
|
|
if (posts.length === 0) return null;
|
|
|
|
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={`/post/${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 !text-[10.5px] !px-1.5 !py-0 shrink-0">推荐</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>
|
|
);
|
|
}
|