feat: 新增帖子最后回复展示、文章目录与三栏布局,优化点赞按钮与导航栏
这是一次包含多个功能升级的提交: 1. 后端新增批量查询帖子最后回复的逻辑,在前端帖子列表项中展示最后回复信息 2. 重构点赞按钮,新增文章样式变体并统一样式逻辑 3. 重写工作台导航栏,精简默认展示的菜单选项 4. 新增文章目录组件,为帖子详情页添加文章目录导航 5. 重构帖子详情页,实现三栏响应式布局,新增板块卡片、作者信息栏与同板块热议推荐 6. 重构帖子列表行组件,优化排版与信息展示逻辑 7. 重构帖子管理操作组件,使用下拉菜单整合所有管理功能 8. 新增全局通用的滚动容器与点赞按钮样式
This commit is contained in:
@@ -1,19 +1,29 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import type { PostListItem } from "@/lib/api";
|
||||
import LikeButton from "./LikeButton";
|
||||
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")}`;
|
||||
/** 发帖/回复时间统一为相对时间:刚刚 / N分钟前 / N小时前 / N天前 / N个月前 / N年前 */
|
||||
function formatRelative(dateStr: string) {
|
||||
const t = new Date(dateStr).getTime();
|
||||
const diff = Date.now() - t;
|
||||
const min = 60_000,
|
||||
hour = 60 * min,
|
||||
day = 24 * hour;
|
||||
if (diff < min) return "刚刚";
|
||||
if (diff < hour) return `${Math.floor(diff / min)}分钟前`;
|
||||
if (diff < day) return `${Math.floor(diff / hour)}小时前`;
|
||||
if (diff < 30 * day) return `${Math.floor(diff / day)}天前`;
|
||||
if (diff < 365 * day) return `${Math.floor(diff / (30 * day))}个月前`;
|
||||
return `${Math.floor(diff / (365 * day))}年前`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子列表行:头像 + 标题 + 作者元信息 + 统计 + 点赞
|
||||
* 用于首页/板块页,以细线串联,悬浮浅色反馈
|
||||
* 帖子列表行(参考社区信息流样式):
|
||||
* 大头像 + 加粗标题(徽章内联)+ 第二行「板块 · 作者 · 发布时间 ← 最后回复」+
|
||||
* 右侧评论数等统计垂直居中。用于首页/板块页/搜索结果。
|
||||
*/
|
||||
export default function PostRow({
|
||||
post,
|
||||
@@ -30,13 +40,16 @@ export default function PostRow({
|
||||
showViews?: boolean;
|
||||
showLike?: boolean;
|
||||
}) {
|
||||
const lastReply = post.last_reply;
|
||||
const replied = lastReply && new Date(lastReply.created_at).getTime() > new Date(post.created_at).getTime() + 60_000;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group relative flex items-center gap-3 sm:gap-4 px-4 sm:px-5 border-t transition-colors hover:bg-[var(--panel-2)] j13-fade-up"
|
||||
className="group relative flex items-center gap-3.5 sm:gap-4 px-4 sm:px-5 border-t transition-colors hover:bg-[var(--panel-2)] j13-fade-up"
|
||||
style={{
|
||||
borderColor: "var(--line)",
|
||||
paddingTop: compact ? 13 : 16,
|
||||
paddingBottom: compact ? 13 : 16,
|
||||
paddingTop: compact ? 15 : 18,
|
||||
paddingBottom: compact ? 15 : 18,
|
||||
animationDelay: `${Math.min(index, 9) * 35}ms`,
|
||||
}}
|
||||
>
|
||||
@@ -46,57 +59,74 @@ export default function PostRow({
|
||||
aria-label={post.user.nickname}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Avatar name={post.user.nickname || post.user.username} size={compact ? 38 : 42} />
|
||||
<Avatar name={post.user.nickname || post.user.username} size={compact ? 44 : 46} />
|
||||
</Link>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{(post.pinned > 0 || post.recommended) && (
|
||||
<div className="flex items-center gap-1.5 mb-1.5 flex-wrap">
|
||||
{post.pinned > 0 && <span className="badge-pin">置顶</span>}
|
||||
{post.recommended && <span className="badge-rec">推荐</span>}
|
||||
</div>
|
||||
)}
|
||||
<Link href={`/post/${post.id}`} className="stretch-link" aria-label={post.title}>
|
||||
<h3
|
||||
className="font-semibold text-[15px] sm:text-[16px] leading-snug truncate transition-colors group-hover:text-[var(--accent)]"
|
||||
style={{ color: "var(--ink)" }}
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{post.pinned > 0 && <span className="badge-pin shrink-0">置顶</span>}
|
||||
{post.recommended && <span className="badge-rec shrink-0">推荐</span>}
|
||||
<Link
|
||||
href={`/post/${post.id}`}
|
||||
className="stretch-link min-w-0 flex-1"
|
||||
aria-label={post.title}
|
||||
>
|
||||
{post.title}
|
||||
</h3>
|
||||
</Link>
|
||||
<div className="meta relative z-[2] flex items-center gap-2.5 mt-1.5 flex-wrap">
|
||||
<h3
|
||||
className="font-bold text-[16px] sm:text-[17px] leading-snug truncate transition-colors group-hover:text-[var(--accent)]"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
{post.title}
|
||||
</h3>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="meta relative z-[2] flex items-center gap-x-2 gap-y-1 mt-1.5 flex-wrap text-[13px]">
|
||||
{boardHref && (
|
||||
<Link
|
||||
href={`/board/${post.board.id}`}
|
||||
className={`av-${post.board.id % 8} rounded-md px-1.5 py-px text-[11px] font-bold leading-[1.5] transition-opacity hover:opacity-85`}
|
||||
>
|
||||
{post.board.name}
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href={`/u/${post.user.id}`}
|
||||
className="hover:text-[var(--accent)] transition-colors"
|
||||
className="font-semibold hover:text-[var(--accent)] transition-colors"
|
||||
style={{ color: "var(--ink-2)" }}
|
||||
>
|
||||
{post.user.nickname}
|
||||
</Link>
|
||||
{boardHref && (
|
||||
<>
|
||||
<span aria-hidden>·</span>
|
||||
<Link
|
||||
href={`/board/${post.board.id}`}
|
||||
className="hover:text-[var(--accent)] transition-colors"
|
||||
>
|
||||
{post.board.name}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<span aria-hidden>·</span>
|
||||
<span>{formatTime(post.created_at)}</span>
|
||||
<span className="tabular-nums">{formatRelative(post.created_at)}</span>
|
||||
{replied && (
|
||||
<span className="inline-flex items-center gap-1 min-w-0">
|
||||
<ArrowLeft size={13} className="shrink-0" aria-hidden />
|
||||
<Link
|
||||
href={`/u/${lastReply!.user.id}`}
|
||||
className="font-medium hover:text-[var(--accent)] transition-colors truncate max-w-[8em]"
|
||||
>
|
||||
{lastReply!.user.nickname || lastReply!.user.username}
|
||||
</Link>
|
||||
<span className="tabular-nums shrink-0">{formatRelative(lastReply!.created_at)}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="above-link flex items-center gap-2.5 sm:gap-3.5 shrink-0">
|
||||
<span
|
||||
className={`${showViews ? "hidden sm:inline-flex" : "inline-flex"} items-center gap-1.5 meta`}
|
||||
<div className="above-link flex items-center gap-3 sm:gap-4 shrink-0">
|
||||
<Link
|
||||
href={`/post/${post.id}`}
|
||||
tabIndex={-1}
|
||||
aria-label={`${post.comment_count} 条评论`}
|
||||
className={`${showViews ? "hidden sm:inline-flex" : "inline-flex"} items-center gap-1.5 meta tabular-nums text-[13.5px] hover:text-[var(--accent)] transition-colors`}
|
||||
>
|
||||
<IconComment size={15} />
|
||||
<IconComment size={17} />
|
||||
{post.comment_count}
|
||||
</span>
|
||||
</Link>
|
||||
{showViews && (
|
||||
<span className="hidden md:inline-flex items-center gap-1.5 meta" aria-label={`${post.view_count} 次浏览`}>
|
||||
<span
|
||||
className="hidden md:inline-flex items-center gap-1.5 meta tabular-nums"
|
||||
aria-label={`${post.view_count} 次浏览`}
|
||||
>
|
||||
<IconEye size={15} />
|
||||
{post.view_count}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user