polish(frontend): 侧栏与徽章可读性,智能悬停提示

降权全站字重;优化右栏公告/数据块;类型状态徽章用固定浅色区分;TipHost 仅在文案不一致、非文字或省略时显示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 03:54:14 +08:00
parent b9ba8cb8c9
commit 5507552ca5
28 changed files with 482 additions and 268 deletions

View File

@@ -107,7 +107,7 @@ function PendingBadge({ count }: { count: number }) {
if (count <= 0) return null; if (count <= 0) return null;
return ( return (
<span <span
className="ml-auto shrink-0 min-w-[20px] h-5 px-1.5 rounded-full inline-flex items-center justify-center text-[10.5px] font-extrabold tabular-nums" className="ml-auto shrink-0 min-w-[20px] h-5 px-1.5 rounded-full inline-flex items-center justify-center text-[10.5px] font-medium tabular-nums"
style={{ background: "var(--gold-soft)", color: "var(--gold)" }} style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
aria-label={`${count} 条待审核`} aria-label={`${count} 条待审核`}
> >
@@ -140,7 +140,7 @@ export function AdminSidebar({ user }: { user: User }) {
<span className="block text-[14.5px] font-extrabold tracking-tight truncate" style={{ color: "var(--ink)" }}> <span className="block text-[14.5px] font-extrabold tracking-tight truncate" style={{ color: "var(--ink)" }}>
管理面板 管理面板
</span> </span>
<span className="block text-[10.5px] font-semibold mt-0.5 tracking-wide" style={{ color: "var(--ink-3)" }}> <span className="block text-[10.5px] mt-0.5 tracking-wide" style={{ color: "var(--ink-3)" }}>
J13 ADMIN J13 ADMIN
</span> </span>
</span> </span>
@@ -159,7 +159,7 @@ export function AdminSidebar({ user }: { user: User }) {
key={item.href} key={item.href}
href={item.href} href={item.href}
aria-current={active ? "page" : undefined} aria-current={active ? "page" : undefined}
className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-[13.5px] font-semibold transition-colors" className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-[13.5px] transition-colors"
style={ style={
active active
? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" } ? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }
@@ -178,7 +178,7 @@ export function AdminSidebar({ user }: { user: User }) {
<div className="pt-2" style={{ borderTop: "1px solid var(--line)" }}> <div className="pt-2" style={{ borderTop: "1px solid var(--line)" }}>
<Link <Link
href="/" href="/"
className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-[13.5px] font-semibold transition-colors hover:bg-[var(--panel-2)]" className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-[13.5px] transition-colors hover:bg-[var(--panel-2)]"
style={{ color: "var(--ink-2)" }} style={{ color: "var(--ink-2)" }}
> >
<ArrowLeft size={16} className="shrink-0" style={{ color: "var(--ink-3)" }} /> <ArrowLeft size={16} className="shrink-0" style={{ color: "var(--ink-3)" }} />
@@ -190,10 +190,10 @@ export function AdminSidebar({ user }: { user: User }) {
<div className="flex items-center gap-2.5 mt-2 px-2.5 py-2.5 rounded-xl" style={{ background: "var(--panel-2)" }}> <div className="flex items-center gap-2.5 mt-2 px-2.5 py-2.5 rounded-xl" style={{ background: "var(--panel-2)" }}>
<Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={32} /> <Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={32} />
<div className="min-w-0 leading-tight"> <div className="min-w-0 leading-tight">
<p className="text-[13px] font-bold truncate" style={{ color: "var(--ink)" }}> <p className="text-[13px] truncate" style={{ color: "var(--ink)" }}>
{user.nickname} {user.nickname}
</p> </p>
<p className="text-[11px] font-semibold truncate" style={{ color: "var(--ink-3)" }}> <p className="text-[11px] truncate" style={{ color: "var(--ink-3)" }}>
@{user.username} @{user.username}
</p> </p>
</div> </div>
@@ -234,7 +234,7 @@ export function AdminNavMobile({ user }: { user: User }) {
key={item.href} key={item.href}
href={item.href} href={item.href}
aria-current={active ? "page" : undefined} aria-current={active ? "page" : undefined}
className="shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] font-semibold transition-colors" className="shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] transition-colors"
style={ style={
active active
? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" } ? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }
@@ -245,7 +245,7 @@ export function AdminNavMobile({ user }: { user: User }) {
{item.label} {item.label}
{item.pendingBadge && pendingTotal > 0 && ( {item.pendingBadge && pendingTotal > 0 && (
<span <span
className="min-w-[18px] h-[18px] px-1 rounded-full inline-flex items-center justify-center text-[10px] font-extrabold tabular-nums" className="min-w-[18px] h-[18px] px-1 rounded-full inline-flex items-center justify-center text-[10px] font-medium tabular-nums"
style={{ background: "var(--gold-soft)", color: "var(--gold)" }} style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
> >
{pendingTotal > 99 ? "99+" : pendingTotal} {pendingTotal > 99 ? "99+" : pendingTotal}
@@ -256,7 +256,7 @@ export function AdminNavMobile({ user }: { user: User }) {
})} })}
<Link <Link
href="/" href="/"
className="shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] font-semibold transition-colors" className="shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] transition-colors"
style={{ color: "var(--ink-2)", background: "var(--panel)", border: "1px solid var(--line)" }} style={{ color: "var(--ink-2)", background: "var(--panel)", border: "1px solid var(--line)" }}
> >
<ArrowLeft size={14} /> <ArrowLeft size={14} />

View File

@@ -154,7 +154,7 @@ export default function AnnouncementAdmin() {
{/* 编辑/新建表单 */} {/* 编辑/新建表单 */}
{form ? ( {form ? (
<div className="panel p-5 lg:sticky lg:top-24"> <div className="panel p-5 lg:sticky lg:top-24">
<p className="font-extrabold text-[15px] mb-4" style={{ color: "var(--ink)" }}> <p className="font-semibold text-[15px] mb-4" style={{ color: "var(--ink)" }}>
{form.id == null ? "新建公告" : "编辑公告"} {form.id == null ? "新建公告" : "编辑公告"}
</p> </p>
@@ -265,7 +265,7 @@ export default function AnnouncementAdmin() {
> >
<FileText size={22} /> <FileText size={22} />
</span> </span>
<p className="font-bold text-[14.5px]" style={{ color: "var(--ink)" }}> <p className="text-[14.5px]" style={{ color: "var(--ink)" }}>
公告编辑器 公告编辑器
</p> </p>
<p className="meta mt-1.5 text-[12.5px] leading-relaxed"> <p className="meta mt-1.5 text-[12.5px] leading-relaxed">
@@ -285,7 +285,7 @@ export default function AnnouncementAdmin() {
className="px-4 sm:px-5 py-3.5 flex items-center justify-between" className="px-4 sm:px-5 py-3.5 flex items-center justify-between"
style={{ borderBottom: "1px solid var(--line)" }} style={{ borderBottom: "1px solid var(--line)" }}
> >
<span className="font-bold text-[15px]" style={{ color: "var(--ink)" }}> <span className="font-medium text-[15px]" style={{ color: "var(--ink)" }}>
全部公告 全部公告
</span> </span>
<span className="meta text-[12px] tabular-nums">共 {list.length} 条</span> <span className="meta text-[12px] tabular-nums">共 {list.length} 条</span>
@@ -311,7 +311,7 @@ export default function AnnouncementAdmin() {
<span className={`j13-anno-tag is-${a.tag_color}`}>{a.tag}</span> <span className={`j13-anno-tag is-${a.tag_color}`}>{a.tag}</span>
{!a.published && ( {!a.published && (
<span <span
className="inline-flex items-center rounded-full px-2 py-px text-[10.5px] font-bold leading-[1.55]" className="inline-flex items-center rounded-full px-2 py-px text-[10.5px] leading-[1.55]"
style={{ background: "var(--gold-soft)", color: "var(--gold)" }} style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
> >
草稿 草稿
@@ -320,7 +320,7 @@ export default function AnnouncementAdmin() {
<span className="meta text-[11px] tabular-nums">{formatDate(a.created_at)}</span> <span className="meta text-[11px] tabular-nums">{formatDate(a.created_at)}</span>
</div> </div>
<p <p
className="mt-1 truncate text-[14px] font-semibold" className="mt-1 truncate text-[14px]"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
data-tip={a.title} data-tip={a.title}
> >

View File

@@ -101,7 +101,7 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
{/* 预设色板 */} {/* 预设色板 */}
<section className="j13-module p-5 sm:p-6 mb-5"> <section className="j13-module p-5 sm:p-6 mb-5">
<h2 className="text-[15px] font-extrabold mb-1" style={{ color: "var(--ink)" }}> <h2 className="text-[15px] font-semibold mb-1" style={{ color: "var(--ink)" }}>
预设主题色 预设主题色
</h2> </h2>
<p className="meta text-[12.5px] mb-4">浅/暗两套色板均已调校,保证文字对比度达标</p> <p className="meta text-[12.5px] mb-4">浅/暗两套色板均已调校,保证文字对比度达标</p>
@@ -139,7 +139,7 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
)} )}
</span> </span>
<span <span
className="text-[12px] font-bold leading-none" className="text-[12px] leading-none"
style={{ color: active ? "var(--accent)" : "var(--ink-2)" }} style={{ color: active ? "var(--accent)" : "var(--ink-2)" }}
> >
{p.name} {p.name}
@@ -152,7 +152,7 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
{/* 自定义颜色 */} {/* 自定义颜色 */}
<section className="j13-module p-5 sm:p-6 mb-5"> <section className="j13-module p-5 sm:p-6 mb-5">
<h2 className="text-[15px] font-extrabold mb-1 flex items-center gap-1.5" style={{ color: "var(--ink)" }}> <h2 className="text-[15px] font-semibold mb-1 flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
<Pipette size={15} /> 自定义颜色 <Pipette size={15} /> 自定义颜色
</h2> </h2>
<p className="meta text-[12.5px] mb-4"> <p className="meta text-[12.5px] mb-4">
@@ -220,14 +220,14 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
{/* 实时预览(整页已实时换肤,此处展示典型组件) */} {/* 实时预览(整页已实时换肤,此处展示典型组件) */}
<section className="j13-module p-5 sm:p-6 mb-6"> <section className="j13-module p-5 sm:p-6 mb-6">
<h2 className="text-[15px] font-extrabold mb-4" style={{ color: "var(--ink)" }}>实时预览</h2> <h2 className="text-[15px] font-semibold mb-4" style={{ color: "var(--ink)" }}>实时预览</h2>
<div className="rounded-[var(--r-sm)] border p-4 space-y-4" style={{ borderColor: "var(--line)", background: "var(--bg)" }}> <div className="rounded-[var(--r-sm)] border p-4 space-y-4" style={{ borderColor: "var(--line)", background: "var(--bg)" }}>
<div className="flex flex-wrap items-center gap-2.5"> <div className="flex flex-wrap items-center gap-2.5">
<button type="button" className="btn btn-primary btn-sm"> <button type="button" className="btn btn-primary btn-sm">
<Save size={14} /> 主按钮 <Save size={14} /> 主按钮
</button> </button>
<button type="button" className="btn btn-line btn-sm">次要按钮</button> <button type="button" className="btn btn-line btn-sm">次要按钮</button>
<a className="text-[13px] font-bold" style={{ color: "var(--accent)" }} href="#preview" onClick={(e) => e.preventDefault()}> <a className="text-[13px]" style={{ color: "var(--accent)" }} href="#preview" onClick={(e) => e.preventDefault()}>
文字链接示例 文字链接示例
</a> </a>
</div> </div>
@@ -235,7 +235,7 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
<span className="j13-tab" aria-selected="false">未选中</span> <span className="j13-tab" aria-selected="false">未选中</span>
<span className="j13-tab" aria-selected="true">选中标签</span> <span className="j13-tab" aria-selected="true">选中标签</span>
<span <span
className="inline-flex items-center rounded-full px-3.5 py-2 text-[13.5px] font-semibold" className="inline-flex items-center rounded-full px-3.5 py-2 text-[13.5px]"
style={{ background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }} style={{ background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }}
> >
导航选中态 导航选中态
@@ -246,7 +246,7 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
<span className="j13-bcard-ico" style={{ background: "var(--accent-soft)", color: "var(--accent)" }}> <span className="j13-bcard-ico" style={{ background: "var(--accent-soft)", color: "var(--accent)" }}>
<Palette size={16} /> <Palette size={16} />
</span> </span>
<span className="j13-bcard-name text-[13px] font-bold flex-1">选中的板块卡片</span> <span className="j13-bcard-name text-[13px] flex-1">选中的板块卡片</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -102,7 +102,7 @@ export default async function AdminEconomyPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
<section className="panel overflow-hidden"> <section className="panel overflow-hidden">
<div <div
className="px-5 py-3.5 font-bold text-[15px]" className="px-5 py-3.5 font-medium text-[15px]"
style={{ borderBottom: "1px solid var(--line)", color: "var(--ink)" }} style={{ borderBottom: "1px solid var(--line)", color: "var(--ink)" }}
> >
帖子类型分布 帖子类型分布
@@ -117,7 +117,7 @@ export default async function AdminEconomyPage() {
className="px-5 py-3 flex items-center justify-between" className="px-5 py-3 flex items-center justify-between"
style={{ borderTop: "1px solid var(--line)" }} style={{ borderTop: "1px solid var(--line)" }}
> >
<span className="font-semibold text-[14px]" style={{ color: "var(--ink)" }}> <span className="text-[14px]" style={{ color: "var(--ink)" }}>
{postTypeLabel(t)} {postTypeLabel(t)}
</span> </span>
<span className="tabular-nums meta">{fmt(n)}</span> <span className="tabular-nums meta">{fmt(n)}</span>
@@ -129,7 +129,7 @@ export default async function AdminEconomyPage() {
<section className="panel overflow-hidden"> <section className="panel overflow-hidden">
<div <div
className="px-5 py-3.5 font-bold text-[15px]" className="px-5 py-3.5 font-medium text-[15px]"
style={{ borderBottom: "1px solid var(--line)", color: "var(--ink)" }} style={{ borderBottom: "1px solid var(--line)", color: "var(--ink)" }}
> >
最近流水 最近流水
@@ -145,14 +145,14 @@ export default async function AdminEconomyPage() {
style={{ borderTop: "1px solid var(--line)" }} style={{ borderTop: "1px solid var(--line)" }}
> >
<div className="min-w-0"> <div className="min-w-0">
<p className="font-semibold text-[13.5px] truncate" style={{ color: "var(--ink)" }}> <p className="text-[13.5px] truncate" style={{ color: "var(--ink)" }}>
{pointReasonLabel(it.reason)} {pointReasonLabel(it.reason)}
{it.note ? ` · ${it.note}` : ""} {it.note ? ` · ${it.note}` : ""}
</p> </p>
<p className="meta text-[12px] mt-0.5">{formatRelative(it.created_at)}</p> <p className="meta text-[12px] mt-0.5">{formatRelative(it.created_at)}</p>
</div> </div>
<span <span
className="tabular-nums font-bold shrink-0" className="tabular-nums font-medium shrink-0"
style={{ color: it.delta >= 0 ? "var(--accent)" : "var(--danger)" }} style={{ color: it.delta >= 0 ? "var(--accent)" : "var(--danger)" }}
> >
{it.delta >= 0 ? `+${it.delta}` : it.delta} {it.delta >= 0 ? `+${it.delta}` : it.delta}

View File

@@ -231,7 +231,7 @@ export default function ModerationAdmin({
role="tab" role="tab"
aria-selected={active} aria-selected={active}
onClick={() => switchTab(t.key)} onClick={() => switchTab(t.key)}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] font-semibold transition-colors" className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] transition-colors"
style={ style={
active active
? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" } ? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }
@@ -281,7 +281,7 @@ export default function ModerationAdmin({
<Link <Link
href={`/post/${p.id}`} href={`/post/${p.id}`}
target="_blank" target="_blank"
className="block text-[15.5px] font-extrabold tracking-tight leading-snug hover:text-[var(--accent)] transition-colors" className="block text-[15.5px] font-semibold tracking-tight leading-snug hover:text-[var(--accent)] transition-colors"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{p.title} {p.title}
@@ -298,7 +298,7 @@ export default function ModerationAdmin({
> >
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<Avatar name={p.user.nickname || p.user.username} src={p.user.avatar || undefined} size={26} /> <Avatar name={p.user.nickname || p.user.username} src={p.user.avatar || undefined} size={26} />
<span className="text-[12.5px] font-semibold truncate" style={{ color: "var(--ink-2)" }}> <span className="text-[12.5px] truncate" style={{ color: "var(--ink-2)" }}>
{p.user.nickname} {p.user.nickname}
</span> </span>
<span className="meta text-[11.5px] truncate">@{p.user.username}</span> <span className="meta text-[11.5px] truncate">@{p.user.username}</span>
@@ -350,7 +350,7 @@ export default function ModerationAdmin({
<Avatar name={c.user.nickname || c.user.username} src={c.user.avatar || undefined} size={34} /> <Avatar name={c.user.nickname || c.user.username} src={c.user.avatar || undefined} size={34} />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-[13.5px] font-bold truncate" style={{ color: "var(--ink)" }}> <span className="text-[13.5px] truncate" style={{ color: "var(--ink)" }}>
{c.user.nickname} {c.user.nickname}
</span> </span>
<Link <Link
@@ -448,7 +448,7 @@ function EmptyQueue({ icon, text }: { icon: React.ReactNode; text: string }) {
> >
{icon} {icon}
</span> </span>
<p className="font-bold text-[14.5px]" style={{ color: "var(--ink)" }}> <p className="text-[14.5px]" style={{ color: "var(--ink)" }}>
{text} {text}
</p> </p>
<p className="meta mt-1.5 text-[12.5px]">新提交的内容会按时间顺序出现在这里</p> <p className="meta mt-1.5 text-[12.5px]">新提交的内容会按时间顺序出现在这里</p>

View File

@@ -41,7 +41,7 @@ function PanelHead({ title, extra }: { title: string; extra?: string }) {
className="px-4 sm:px-5 py-3.5 flex items-center justify-between gap-2" className="px-4 sm:px-5 py-3.5 flex items-center justify-between gap-2"
style={{ borderBottom: "1px solid var(--line)" }} style={{ borderBottom: "1px solid var(--line)" }}
> >
<span className="font-bold text-[15px]" style={{ color: "var(--ink)" }}> <span className="font-medium text-[15px]" style={{ color: "var(--ink)" }}>
{title} {title}
</span> </span>
{extra && ( {extra && (
@@ -76,19 +76,19 @@ function PendingWorkPanel({ counts }: { counts: PendingCounts }) {
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<p className="text-[15px] font-extrabold" style={{ color: "var(--ink)" }}> <p className="text-[15px] font-semibold" style={{ color: "var(--ink)" }}>
内容审核 内容审核
</p> </p>
{idle ? ( {idle ? (
<span <span
className="text-[11.5px] font-semibold px-2 py-0.5 rounded-full" className="text-[11.5px] px-2 py-0.5 rounded-full"
style={{ background: "var(--tint)", color: "var(--ink-3)" }} style={{ background: "var(--tint)", color: "var(--ink-3)" }}
> >
暂无待审 暂无待审
</span> </span>
) : ( ) : (
<span <span
className="text-[11.5px] font-semibold px-2 py-0.5 rounded-full tabular-nums" className="text-[11.5px] px-2 py-0.5 rounded-full tabular-nums"
style={{ background: "var(--ok-soft)", color: "var(--ok)" }} style={{ background: "var(--ok-soft)", color: "var(--ok)" }}
> >
{fmt(total)} 项待处理 {fmt(total)} 项待处理
@@ -170,7 +170,7 @@ export default async function AdminDashboardPage() {
className="panel mb-4 px-4 sm:px-5 py-4 flex items-center justify-between gap-3 transition-colors hover:bg-[var(--panel-2)]" className="panel mb-4 px-4 sm:px-5 py-4 flex items-center justify-between gap-3 transition-colors hover:bg-[var(--panel-2)]"
> >
<div> <div>
<p className="font-bold text-[15px]" style={{ color: "var(--ink)" }}> <p className="font-medium text-[15px]" style={{ color: "var(--ink)" }}>
积分与类型帖看板 积分与类型帖看板
</p> </p>
<p className="meta text-[12.5px] mt-0.5">余额合计、悬赏托管、类型分布与最近流水</p> <p className="meta text-[12.5px] mt-0.5">余额合计、悬赏托管、类型分布与最近流水</p>
@@ -202,7 +202,7 @@ export default async function AdminDashboardPage() {
/> />
<span className="min-w-0 flex-1 leading-tight"> <span className="min-w-0 flex-1 leading-tight">
<span <span
className="block text-[13.5px] font-bold truncate" className="block text-[13.5px] truncate"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{u.nickname} {u.nickname}
@@ -215,7 +215,7 @@ export default async function AdminDashboardPage() {
</span> </span>
</span> </span>
<span <span
className="shrink-0 inline-flex items-center gap-1 text-[11.5px] font-semibold" className="shrink-0 inline-flex items-center gap-1 text-[11.5px]"
style={{ color: "var(--ink-3)" }} style={{ color: "var(--ink-3)" }}
> >
<UserPlus size={12} aria-hidden="true" /> <UserPlus size={12} aria-hidden="true" />
@@ -227,7 +227,7 @@ export default async function AdminDashboardPage() {
{canUsers && ( {canUsers && (
<Link <Link
href="/admin/users" href="/admin/users"
className="flex items-center justify-center gap-1.5 px-4 py-3 text-[13px] font-semibold transition-colors hover:bg-[var(--panel-2)]" className="flex items-center justify-center gap-1.5 px-4 py-3 text-[13px] transition-colors hover:bg-[var(--panel-2)]"
style={{ style={{
borderTop: "1px solid var(--line)", borderTop: "1px solid var(--line)",
color: "var(--accent)", color: "var(--accent)",
@@ -262,13 +262,13 @@ export default async function AdminDashboardPage() {
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="flex items-center justify-between gap-2"> <span className="flex items-center justify-between gap-2">
<span <span
className="text-[13.5px] font-bold truncate" className="text-[13.5px] truncate"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{b.name} {b.name}
</span> </span>
<span <span
className="text-[12px] font-bold tabular-nums shrink-0" className="text-[12px] tabular-nums shrink-0"
style={{ color: "var(--ink-2)" }} style={{ color: "var(--ink-2)" }}
> >
{fmt(b.post_count)} {fmt(b.post_count)}

View File

@@ -90,13 +90,13 @@ function MiniStat({
{icon} {icon}
</span> </span>
<div className="min-w-0 leading-tight"> <div className="min-w-0 leading-tight">
<p className="text-[11.5px] font-semibold truncate" style={{ color: "var(--ink-3)" }}> <p className="text-[11.5px] truncate" style={{ color: "var(--ink-3)" }}>
{label} {label}
</p> </p>
<p className="text-[19px] font-extrabold tabular-nums" style={{ color: "var(--ink)" }}> <p className="text-[19px] font-extrabold tabular-nums" style={{ color: "var(--ink)" }}>
{value} {value}
{sub && ( {sub && (
<span className="ml-1.5 text-[11px] font-semibold" style={{ color: "var(--ink-3)" }}> <span className="ml-1.5 text-[11px]" style={{ color: "var(--ink-3)" }}>
{sub} {sub}
</span> </span>
)} )}
@@ -279,7 +279,7 @@ export default function UsersAdmin({
if (u.banned) { if (u.banned) {
return ( return (
<span <span
className="inline-flex items-center gap-1.5 text-[12.5px] font-bold" className="inline-flex items-center gap-1.5 text-[12.5px]"
style={{ color: "var(--danger)" }} style={{ color: "var(--danger)" }}
data-tip="账号已封禁" data-tip="账号已封禁"
> >
@@ -291,7 +291,7 @@ export default function UsersAdmin({
if (u.online) { if (u.online) {
return ( return (
<span <span
className="inline-flex items-center gap-1.5 text-[12.5px] font-bold" className="inline-flex items-center gap-1.5 text-[12.5px]"
style={{ color: "var(--ok)" }} style={{ color: "var(--ok)" }}
data-tip={u.last_seen_at ? `最近活跃 ${formatRelative(u.last_seen_at)}` : "在线"} data-tip={u.last_seen_at ? `最近活跃 ${formatRelative(u.last_seen_at)}` : "在线"}
> >
@@ -302,7 +302,7 @@ export default function UsersAdmin({
} }
return ( return (
<span <span
className="inline-flex items-center gap-1.5 text-[12.5px] font-semibold" className="inline-flex items-center gap-1.5 text-[12.5px]"
style={{ color: "var(--ink-3)" }} style={{ color: "var(--ink-3)" }}
data-tip={u.last_seen_at ? `最近活跃 ${formatRelative(u.last_seen_at)}` : "从未活跃"} data-tip={u.last_seen_at ? `最近活跃 ${formatRelative(u.last_seen_at)}` : "从未活跃"}
> >
@@ -325,7 +325,7 @@ export default function UsersAdmin({
aria-label={`查看 ${u.username} 的登录历史`} aria-label={`查看 ${u.username} 的登录历史`}
> >
<span <span
className="block text-[12px] font-semibold tabular-nums truncate group-hover:text-[var(--accent)]" className="block text-[12px] tabular-nums truncate group-hover:text-[var(--accent)]"
style={{ color: u.last_login_ip ? "var(--ink-2)" : "var(--ink-3)" }} style={{ color: u.last_login_ip ? "var(--ink-2)" : "var(--ink-3)" }}
> >
{u.last_login_ip || "暂无记录"} {u.last_login_ip || "暂无记录"}
@@ -371,7 +371,7 @@ export default function UsersAdmin({
<div className="flex items-center gap-1 justify-end"> <div className="flex items-center gap-1 justify-end">
{auditBtn} {auditBtn}
<span <span
className="inline-flex items-center rounded-full px-2.5 py-1 text-[11px] font-bold whitespace-nowrap" className="inline-flex items-center rounded-full px-2.5 py-1 text-[11px] whitespace-nowrap"
style={{ background: "var(--panel-2)", color: "var(--ink-3)" }} style={{ background: "var(--panel-2)", color: "var(--ink-3)" }}
> >
当前账号 当前账号
@@ -540,7 +540,7 @@ export default function UsersAdmin({
type="button" type="button"
aria-pressed={active} aria-pressed={active}
onClick={() => changeFilter(t.key)} onClick={() => changeFilter(t.key)}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] font-semibold transition-colors" className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] transition-colors"
style={ style={
active active
? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" } ? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }
@@ -570,7 +570,7 @@ export default function UsersAdmin({
className="px-4 sm:px-5 py-3.5 flex items-center justify-between gap-2 flex-wrap" className="px-4 sm:px-5 py-3.5 flex items-center justify-between gap-2 flex-wrap"
style={{ borderBottom: "1px solid var(--line)" }} style={{ borderBottom: "1px solid var(--line)" }}
> >
<span className="font-bold text-[15px]" style={{ color: "var(--ink)" }}> <span className="font-medium text-[15px]" style={{ color: "var(--ink)" }}>
{appliedQ ? ( {appliedQ ? (
<> <>
搜索“<span style={{ color: "var(--accent)" }}>{appliedQ}</span>” 搜索“<span style={{ color: "var(--accent)" }}>{appliedQ}</span>”
@@ -591,7 +591,7 @@ export default function UsersAdmin({
{/* 桌面表头 */} {/* 桌面表头 */}
{users.length > 0 && ( {users.length > 0 && (
<div <div
className={`${rowGrid} text-[11.5px] font-semibold uppercase tracking-wide`} className={`${rowGrid} text-[11.5px] uppercase tracking-wide`}
style={{ color: "var(--ink-3)", borderBottom: "1px solid var(--line)", background: "var(--panel-2)" }} style={{ color: "var(--ink-3)", borderBottom: "1px solid var(--line)", background: "var(--panel-2)" }}
> >
<span>成员</span> <span>成员</span>
@@ -618,7 +618,7 @@ export default function UsersAdmin({
> >
<UsersIcon size={22} /> <UsersIcon size={22} />
</span> </span>
<p className="font-bold text-[14.5px]" style={{ color: "var(--ink)" }}> <p className="text-[14.5px]" style={{ color: "var(--ink)" }}>
{hasFilter ? "没有匹配的成员" : "暂无成员"} {hasFilter ? "没有匹配的成员" : "暂无成员"}
</p> </p>
<p className="meta mt-1.5 text-[12.5px]"> <p className="meta mt-1.5 text-[12.5px]">
@@ -635,7 +635,7 @@ export default function UsersAdmin({
<div className="flex items-center gap-2.5 min-w-0"> <div className="flex items-center gap-2.5 min-w-0">
<Avatar name={u.nickname || u.username} src={u.avatar || undefined} size={34} /> <Avatar name={u.nickname || u.username} src={u.avatar || undefined} size={34} />
<div className="min-w-0 leading-tight"> <div className="min-w-0 leading-tight">
<p className="text-[13.5px] font-bold truncate" style={{ color: "var(--ink)" }}> <p className="text-[13.5px] truncate" style={{ color: "var(--ink)" }}>
{u.nickname} {u.nickname}
</p> </p>
<p className="text-[11.5px] truncate" style={{ color: "var(--ink-3)" }}> <p className="text-[11.5px] truncate" style={{ color: "var(--ink-3)" }}>
@@ -651,7 +651,7 @@ export default function UsersAdmin({
<div className="min-w-0">{renderLastLogin(u)}</div> <div className="min-w-0">{renderLastLogin(u)}</div>
<div className="hidden xl:block"> <div className="hidden xl:block">
<p <p
className="text-[12.5px] font-semibold tabular-nums" className="text-[12.5px] tabular-nums"
style={{ color: "var(--ink-2)" }} style={{ color: "var(--ink-2)" }}
data-tip="仅统计已发布内容;驳回/删除见内容档案" data-tip="仅统计已发布内容;驳回/删除见内容档案"
> >
@@ -670,7 +670,7 @@ export default function UsersAdmin({
<Avatar name={u.nickname || u.username} src={u.avatar || undefined} size={38} /> <Avatar name={u.nickname || u.username} src={u.avatar || undefined} size={38} />
<div className="min-w-0 flex-1 leading-tight"> <div className="min-w-0 flex-1 leading-tight">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-[14px] font-bold truncate" style={{ color: "var(--ink)" }}> <span className="text-[14px] truncate" style={{ color: "var(--ink)" }}>
{u.nickname} {u.nickname}
</span> </span>
{u.role !== ROLES.USER && ( {u.role !== ROLES.USER && (
@@ -678,7 +678,7 @@ export default function UsersAdmin({
)} )}
{isSelf && ( {isSelf && (
<span <span
className="inline-flex items-center rounded-full px-2 py-px text-[10.5px] font-bold leading-[1.55]" className="inline-flex items-center rounded-full px-2 py-px text-[10.5px] leading-[1.55]"
style={{ background: "var(--panel-2)", color: "var(--ink-3)" }} style={{ background: "var(--panel-2)", color: "var(--ink-3)" }}
> >
当前账号 当前账号
@@ -931,7 +931,7 @@ function RoleAssignDialog({
</> </>
} }
> >
<p className="mb-2 text-[12.5px] font-bold" style={{ color: "var(--ink)" }}> <p className="mb-2 text-[12.5px]" style={{ color: "var(--ink)" }}>
管理角色 管理角色
</p> </p>
<div className="flex flex-col gap-2" role="radiogroup" aria-label="管理角色"> <div className="flex flex-col gap-2" role="radiogroup" aria-label="管理角色">
@@ -964,7 +964,7 @@ function RoleAssignDialog({
{o.value === ROLES.USER ? <UsersIcon size={18} /> : <ShieldCheck size={18} />} {o.value === ROLES.USER ? <UsersIcon size={18} /> : <ShieldCheck size={18} />}
</span> </span>
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="block text-[13.5px] font-bold" style={{ color: "var(--ink)" }}> <span className="block text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
{o.title} {o.title}
</span> </span>
<span className="block mt-1 text-[12px] leading-relaxed" style={{ color: "var(--ink-3)" }}> <span className="block mt-1 text-[12px] leading-relaxed" style={{ color: "var(--ink-3)" }}>
@@ -985,7 +985,7 @@ function RoleAssignDialog({
{role === ROLES.BOARD_ADMIN && ( {role === ROLES.BOARD_ADMIN && (
<div className="mt-4"> <div className="mt-4">
<p className="text-[12.5px] font-bold mb-2" style={{ color: "var(--ink)" }}> <p className="text-[12.5px] mb-2" style={{ color: "var(--ink)" }}>
授权板块 <span style={{ color: "var(--danger)" }}>*</span> 授权板块 <span style={{ color: "var(--danger)" }}>*</span>
</p> </p>
{boards.length === 0 ? ( {boards.length === 0 ? (
@@ -1001,7 +1001,7 @@ function RoleAssignDialog({
role="checkbox" role="checkbox"
aria-checked={checked} aria-checked={checked}
onClick={() => toggleBoard(b.id)} onClick={() => toggleBoard(b.id)}
className="flex items-center gap-2 rounded-lg px-3 py-2 text-[12.5px] font-semibold transition-colors" className="flex items-center gap-2 rounded-lg px-3 py-2 text-[12.5px] transition-colors"
style={{ style={{
border: `1.5px solid ${checked ? "var(--accent)" : "var(--line)"}`, border: `1.5px solid ${checked ? "var(--accent)" : "var(--line)"}`,
background: checked ? "var(--accent-soft)" : "var(--panel-2)", background: checked ? "var(--accent-soft)" : "var(--panel-2)",
@@ -1026,7 +1026,7 @@ function RoleAssignDialog({
</div> </div>
)} )}
{err && ( {err && (
<p className="mt-2 text-[12px] font-semibold" style={{ color: "var(--danger)" }}> <p className="mt-2 text-[12px]" style={{ color: "var(--danger)" }}>
{err} {err}
</p> </p>
)} )}
@@ -1034,7 +1034,7 @@ function RoleAssignDialog({
)} )}
<div className="mt-5 pt-4" style={{ borderTop: "1px solid var(--line)" }}> <div className="mt-5 pt-4" style={{ borderTop: "1px solid var(--line)" }}>
<p className="text-[12.5px] font-bold mb-2" style={{ color: "var(--ink)" }}> <p className="text-[12.5px] mb-2" style={{ color: "var(--ink)" }}>
账号状态 账号状态
</p> </p>
<div className="grid grid-cols-2 gap-2" role="radiogroup" aria-label="账号状态"> <div className="grid grid-cols-2 gap-2" role="radiogroup" aria-label="账号状态">
@@ -1050,7 +1050,7 @@ function RoleAssignDialog({
}} }}
> >
<span <span
className="flex items-center gap-1.5 text-[13px] font-bold" className="flex items-center gap-1.5 text-[13px] font-medium"
style={{ color: !banned ? "var(--ok)" : "var(--ink)" }} style={{ color: !banned ? "var(--ok)" : "var(--ink)" }}
> >
<RotateCcw size={14} /> 正常 <RotateCcw size={14} /> 正常
@@ -1071,7 +1071,7 @@ function RoleAssignDialog({
}} }}
> >
<span <span
className="flex items-center gap-1.5 text-[13px] font-bold" className="flex items-center gap-1.5 text-[13px] font-medium"
style={{ color: banned ? "var(--danger)" : "var(--ink)" }} style={{ color: banned ? "var(--danger)" : "var(--ink)" }}
> >
<Ban size={14} /> 封禁 <Ban size={14} /> 封禁
@@ -1199,10 +1199,10 @@ function LoginLogsDialog({ target, onClose }: { target: AdminUser | null; onClos
aria-hidden aria-hidden
/> />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-[12.5px] font-semibold tabular-nums truncate" style={{ color: "var(--ink)" }}> <p className="text-[12.5px] tabular-nums truncate" style={{ color: "var(--ink)" }}>
{log.ip || "未知 IP"} {log.ip || "未知 IP"}
<span <span
className="ml-2 inline-flex items-center rounded-full px-1.5 py-px text-[10.5px] font-bold" className="ml-2 inline-flex items-center rounded-full px-1.5 py-px text-[10.5px]"
style={ style={
log.success log.success
? { background: "var(--ok-soft)", color: "var(--ok)" } ? { background: "var(--ok-soft)", color: "var(--ok)" }

View File

@@ -60,7 +60,7 @@ function StatusChip({
{chips.map((c) => ( {chips.map((c) => (
<span <span
key={c.label} key={c.label}
className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-bold" className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px]"
style={{ background: c.soft, color: c.color }} style={{ background: c.soft, color: c.color }}
> >
{c.label} {c.label}
@@ -175,7 +175,7 @@ export default function UserAuditClient({
<div> <div>
<Link <Link
href="/admin/users" href="/admin/users"
className="inline-flex items-center gap-1.5 text-[13px] font-semibold mb-4 hover:opacity-80" className="inline-flex items-center gap-1.5 text-[13px] mb-4 hover:opacity-80"
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
> >
<ArrowLeft size={14} /> <ArrowLeft size={14} />
@@ -196,7 +196,7 @@ export default function UserAuditClient({
<RoleBadge role={u.role} /> <RoleBadge role={u.role} />
{u.banned && ( {u.banned && (
<span <span
className="text-[11px] font-bold px-2 py-0.5 rounded-full" className="text-[11px] px-2 py-0.5 rounded-full"
style={{ background: "var(--danger-soft)", color: "var(--danger)" }} style={{ background: "var(--danger-soft)", color: "var(--danger)" }}
> >
已封禁 已封禁
@@ -251,7 +251,7 @@ export default function UserAuditClient({
key={t.key} key={t.key}
type="button" type="button"
onClick={() => switchTab(t.key)} onClick={() => switchTab(t.key)}
className="inline-flex items-center gap-1.5 px-3 py-2.5 text-[13px] font-bold whitespace-nowrap -mb-px" className="inline-flex items-center gap-1.5 px-3 py-2.5 text-[13px] whitespace-nowrap -mb-px"
style={{ style={{
color: active ? "var(--accent)" : "var(--ink-3)", color: active ? "var(--accent)" : "var(--ink-3)",
borderBottom: active ? "2px solid var(--accent)" : "2px solid transparent", borderBottom: active ? "2px solid var(--accent)" : "2px solid transparent",
@@ -259,7 +259,7 @@ export default function UserAuditClient({
> >
{t.icon} {t.icon}
{t.label} {t.label}
<span className="tabular-nums font-semibold opacity-70"> <span className="tabular-nums opacity-70">
{tabCount(t.key).toLocaleString("zh-CN")} {tabCount(t.key).toLocaleString("zh-CN")}
</span> </span>
</button> </button>
@@ -298,13 +298,13 @@ export default function UserAuditClient({
)} )}
</div> </div>
{p.deleted ? ( {p.deleted ? (
<p className="text-[14px] font-bold" style={{ color: "var(--ink-3)" }}> <p className="text-[14px] font-medium" style={{ color: "var(--ink-3)" }}>
{p.title || "(无标题)"} {p.title || "(无标题)"}
</p> </p>
) : ( ) : (
<Link <Link
href={`/post/${p.id}`} href={`/post/${p.id}`}
className="text-[14px] font-bold hover:underline" className="text-[14px] font-medium hover:underline"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{p.title || "(无标题)"} {p.title || "(无标题)"}
@@ -339,7 +339,7 @@ export default function UserAuditClient({
) : ( ) : (
<Link <Link
href={`/post/${c.post_id}`} href={`/post/${c.post_id}`}
className="text-[11.5px] font-semibold hover:underline" className="text-[11.5px] hover:underline"
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
> >
帖子 #{c.post_id} 帖子 #{c.post_id}

View File

@@ -381,7 +381,7 @@ export default function ChatShell({
{query.trim() && ( {query.trim() && (
<div className="px-2 pb-2"> <div className="px-2 pb-2">
<p <p
className="px-2 py-1.5 text-[11px] font-bold uppercase tracking-wider" className="px-2 py-1.5 text-[11px] uppercase tracking-wider"
style={{ color: "var(--ink-3)" }} style={{ color: "var(--ink-3)" }}
> >
用户 {searching ? "…" : ""} 用户 {searching ? "…" : ""}
@@ -397,7 +397,7 @@ export default function ChatShell({
> >
<Avatar name={u.nickname || u.username} src={u.avatar || undefined} size={36} /> <Avatar name={u.nickname || u.username} src={u.avatar || undefined} size={36} />
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[13px] font-semibold truncate" style={{ color: "var(--ink)" }}> <p className="text-[13px] truncate" style={{ color: "var(--ink)" }}>
{u.nickname || u.username} {u.nickname || u.username}
</p> </p>
<p className="text-[11px] meta truncate">@{u.username}</p> <p className="text-[11px] meta truncate">@{u.username}</p>
@@ -406,7 +406,7 @@ export default function ChatShell({
)) ))
)} )}
<p <p
className="px-2 py-1.5 mt-1 text-[11px] font-bold uppercase tracking-wider" className="px-2 py-1.5 mt-1 text-[11px] uppercase tracking-wider"
style={{ color: "var(--ink-3)" }} style={{ color: "var(--ink-3)" }}
> >
会话 会话
@@ -468,14 +468,14 @@ export default function ChatShell({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span <span
className="font-semibold text-[13.5px] truncate" className="text-[13.5px] truncate"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{roomTitle(room)} {roomTitle(room)}
</span> </span>
{pinned && ( {pinned && (
<span <span
className="inline-flex items-center gap-0.5 text-[10px] font-bold shrink-0 px-1.5 py-0.5 rounded" className="inline-flex items-center gap-0.5 text-[10px] shrink-0 px-1.5 py-0.5 rounded"
style={{ style={{
background: "var(--accent-soft)", background: "var(--accent-soft)",
color: "var(--accent)", color: "var(--accent)",
@@ -486,12 +486,12 @@ export default function ChatShell({
</span> </span>
)} )}
{room.is_default && ( {room.is_default && (
<span className="text-[10px] font-bold shrink-0" style={{ color: "var(--gold)" }}> <span className="text-[10px] shrink-0" style={{ color: "var(--gold)" }}>
大厅 大厅
</span> </span>
)} )}
{room.is_private && room.room_type === "group" && ( {room.is_private && room.room_type === "group" && (
<span className="text-[10px] font-bold shrink-0 meta">私密</span> <span className="text-[10px] shrink-0 meta">私密</span>
)} )}
<span className="ml-auto meta text-[10.5px] shrink-0"> <span className="ml-auto meta text-[10.5px] shrink-0">
{formatTime(room.last_message?.created_at || room.updated_at)} {formatTime(room.last_message?.created_at || room.updated_at)}
@@ -501,7 +501,7 @@ export default function ChatShell({
<p className="text-[12px] truncate meta flex-1">{roomPreview(room)}</p> <p className="text-[12px] truncate meta flex-1">{roomPreview(room)}</p>
{room.unread_count > 0 && ( {room.unread_count > 0 && (
<span <span
className="shrink-0 min-w-[18px] h-[18px] px-1 rounded-full text-[10px] font-bold flex items-center justify-center" className="shrink-0 min-w-[18px] h-[18px] px-1 rounded-full text-[10px] font-medium flex items-center justify-center"
style={{ background: "var(--clay)", color: "#fff" }} style={{ background: "var(--clay)", color: "#fff" }}
> >
{room.unread_count > 99 ? "99+" : room.unread_count} {room.unread_count > 99 ? "99+" : room.unread_count}
@@ -530,7 +530,7 @@ export default function ChatShell({
> >
<MessageSquare size={28} /> <MessageSquare size={28} />
</span> </span>
<p className="font-bold text-[16px]" style={{ color: "var(--ink)" }}> <p className="text-[16px]" style={{ color: "var(--ink)" }}>
选择一个会话开始聊天 选择一个会话开始聊天
</p> </p>
<p className="meta mt-1.5 text-[13px] max-w-xs"> <p className="meta mt-1.5 text-[13px] max-w-xs">
@@ -637,7 +637,7 @@ function CreateGroupModal({
> >
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-[12px] font-bold mb-1.5" style={{ color: "var(--ink-2)" }}> <label className="block text-[12px] mb-1.5" style={{ color: "var(--ink-2)" }}>
群名称 <span style={{ color: "var(--danger)" }}>*</span> 群名称 <span style={{ color: "var(--danger)" }}>*</span>
</label> </label>
<input <input
@@ -652,7 +652,7 @@ function CreateGroupModal({
/> />
</div> </div>
<div> <div>
<label className="block text-[12px] font-bold mb-1.5" style={{ color: "var(--ink-2)" }}> <label className="block text-[12px] mb-1.5" style={{ color: "var(--ink-2)" }}>
群简介(选填) 群简介(选填)
</label> </label>
<textarea <textarea
@@ -665,14 +665,14 @@ function CreateGroupModal({
/> />
</div> </div>
<div> <div>
<label className="block text-[12px] font-bold mb-2" style={{ color: "var(--ink-2)" }}> <label className="block text-[12px] mb-2" style={{ color: "var(--ink-2)" }}>
可见性 可见性
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
type="button" type="button"
onClick={() => setIsPrivate(false)} onClick={() => setIsPrivate(false)}
className="flex-1 h-9 rounded-lg text-[13px] font-semibold" className="flex-1 h-9 rounded-lg text-[13px]"
style={{ style={{
background: !isPrivate ? "var(--accent-soft)" : "var(--bg)", background: !isPrivate ? "var(--accent-soft)" : "var(--bg)",
color: !isPrivate ? "var(--accent)" : "var(--ink-2)", color: !isPrivate ? "var(--accent)" : "var(--ink-2)",
@@ -684,7 +684,7 @@ function CreateGroupModal({
<button <button
type="button" type="button"
onClick={() => setIsPrivate(true)} onClick={() => setIsPrivate(true)}
className="flex-1 h-9 rounded-lg text-[13px] font-semibold" className="flex-1 h-9 rounded-lg text-[13px]"
style={{ style={{
background: isPrivate ? "var(--accent-soft)" : "var(--bg)", background: isPrivate ? "var(--accent-soft)" : "var(--bg)",
color: isPrivate ? "var(--accent)" : "var(--ink-2)", color: isPrivate ? "var(--accent)" : "var(--ink-2)",
@@ -697,7 +697,7 @@ function CreateGroupModal({
</div> </div>
{isPrivate && ( {isPrivate && (
<div> <div>
<label className="block text-[12px] font-bold mb-1.5" style={{ color: "var(--ink-2)" }}> <label className="block text-[12px] mb-1.5" style={{ color: "var(--ink-2)" }}>
初始成员(选填) 初始成员(选填)
</label> </label>
<input <input

View File

@@ -130,14 +130,14 @@ function RecallNotice({
uid > 0 ? ( uid > 0 ? (
<Link <Link
href={`/u/${uid}`} href={`/u/${uid}`}
className="font-semibold transition-opacity hover:opacity-80" className="transition-opacity hover:opacity-80"
style={nameStyle} style={nameStyle}
data-tip={`查看 ${name} 的主页`} data-tip={`查看 ${name} 的主页`}
> >
{name} {name}
</Link> </Link>
) : ( ) : (
<span className="font-semibold" style={nameStyle}> <span style={nameStyle}>
{name} {name}
</span> </span>
); );
@@ -966,7 +966,7 @@ export default function ChatRoomClient({
</h1> </h1>
{isDefault && ( {isDefault && (
<span <span
className="text-[10.5px] font-bold px-2 py-0.5 rounded-full" className="text-[10.5px] px-2 py-0.5 rounded-full"
style={{ background: "var(--gold-soft)", color: "var(--gold)" }} style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
> >
大厅 大厅
@@ -974,7 +974,7 @@ export default function ChatRoomClient({
)} )}
{isPrivate && !isDirect && ( {isPrivate && !isDirect && (
<span <span
className="text-[10.5px] font-bold px-2 py-0.5 rounded-full" className="text-[10.5px] px-2 py-0.5 rounded-full"
style={{ background: "var(--panel-2)", color: "var(--ink-2)" }} style={{ background: "var(--panel-2)", color: "var(--ink-2)" }}
> >
私密 私密
@@ -1056,7 +1056,7 @@ export default function ChatRoomClient({
> >
{!canView ? ( {!canView ? (
<div className="text-center py-12 px-4"> <div className="text-center py-12 px-4">
<p className="font-semibold" style={{ color: "var(--ink)" }}> <p style={{ color: "var(--ink)" }}>
{isPrivate ? "此群为私密群" : "预览群信息"} {isPrivate ? "此群为私密群" : "预览群信息"}
</p> </p>
<p className="meta mt-1.5"> <p className="meta mt-1.5">
@@ -1123,7 +1123,7 @@ export default function ChatRoomClient({
<button <button
type="button" type="button"
onClick={() => handleReEdit(m.id)} onClick={() => handleReEdit(m.id)}
className="font-semibold transition-opacity hover:opacity-80" className="transition-opacity hover:opacity-80"
style={{ style={{
color: color:
"color-mix(in srgb, var(--accent) 72%, var(--ink))", "color-mix(in srgb, var(--accent) 72%, var(--ink))",
@@ -1166,7 +1166,7 @@ export default function ChatRoomClient({
<Crown size={11} style={{ color: "var(--gold)" }} /> <Crown size={11} style={{ color: "var(--gold)" }} />
)} )}
<span <span
className="text-[12px] font-semibold truncate" className="text-[12px] truncate"
style={{ color: "var(--ink-2)" }} style={{ color: "var(--ink-2)" }}
> >
{m.sender.nickname} {m.sender.nickname}
@@ -1188,7 +1188,7 @@ export default function ChatRoomClient({
}} }}
> >
<div <div
className="inline-flex items-center gap-1 mb-1.5 text-[11px] font-semibold" className="inline-flex items-center gap-1 mb-1.5 text-[11px]"
style={{ color: "var(--ink-3)" }} style={{ color: "var(--ink-3)" }}
> >
<Undo2 size={11} strokeWidth={2.4} /> <Undo2 size={11} strokeWidth={2.4} />
@@ -1232,7 +1232,7 @@ export default function ChatRoomClient({
<Crown size={11} style={{ color: "var(--gold)" }} /> <Crown size={11} style={{ color: "var(--gold)" }} />
)} )}
<span <span
className="text-[12px] font-semibold truncate" className="text-[12px] truncate"
style={{ color: "var(--ink-2)" }} style={{ color: "var(--ink-2)" }}
> >
{m.sender.nickname} {m.sender.nickname}
@@ -1319,7 +1319,7 @@ export default function ChatRoomClient({
style={{ background: "var(--accent)" }} style={{ background: "var(--accent)" }}
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-[11px] font-bold" style={{ color: "var(--accent)" }}> <p className="text-[11px]" style={{ color: "var(--accent)" }}>
回复 {replyTo.sender.nickname || replyTo.sender.username} 回复 {replyTo.sender.nickname || replyTo.sender.username}
</p> </p>
<p className="text-[12px] truncate meta mt-0.5"> <p className="text-[12px] truncate meta mt-0.5">
@@ -1365,7 +1365,7 @@ export default function ChatRoomClient({
size={28} size={28}
/> />
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[13px] font-semibold truncate" style={{ color: "var(--ink)" }}> <p className="text-[13px] truncate" style={{ color: "var(--ink)" }}>
{m.user.nickname || m.user.username} {m.user.nickname || m.user.username}
</p> </p>
<p className="text-[11px] meta truncate">@{m.user.username}</p> <p className="text-[11px] meta truncate">@{m.user.username}</p>
@@ -1486,7 +1486,7 @@ export default function ChatRoomClient({
className="flex items-center justify-between px-4 py-3 shrink-0" className="flex items-center justify-between px-4 py-3 shrink-0"
style={{ borderBottom: "1px solid var(--line)" }} style={{ borderBottom: "1px solid var(--line)" }}
> >
<p className="text-[13px] font-bold" style={{ color: "var(--ink)" }}> <p className="text-[13px] font-medium" style={{ color: "var(--ink)" }}>
{rightPanel === "members" {rightPanel === "members"
? `成员 · ${memberCount}` ? `成员 · ${memberCount}`
: rightPanel === "invite" : rightPanel === "invite"
@@ -1630,7 +1630,7 @@ function MembersPanel({
{showTitle && ( {showTitle && (
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<p <p
className="text-[12px] font-bold tracking-[0.08em] uppercase" className="text-[12px] tracking-[0.08em] uppercase"
style={{ color: "var(--ink-3)" }} style={{ color: "var(--ink-3)" }}
> >
成员 · {members.length} 成员 · {members.length}
@@ -1666,7 +1666,7 @@ function MembersPanel({
/> />
<Link <Link
href={`/u/${m.user.id}`} href={`/u/${m.user.id}`}
className="flex-1 min-w-0 truncate text-[13px] font-semibold hover:text-[var(--accent)] transition-colors" className="flex-1 min-w-0 truncate text-[13px] hover:text-[var(--accent)] transition-colors"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{m.user.nickname} {m.user.nickname}
@@ -1795,7 +1795,7 @@ function SettingsPanel({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label <label
className="block text-[12px] font-bold mb-1.5" className="block text-[12px] mb-1.5"
style={{ color: "var(--ink-2)" }} style={{ color: "var(--ink-2)" }}
> >
群名称 群名称
@@ -1815,7 +1815,7 @@ function SettingsPanel({
</div> </div>
<div> <div>
<label <label
className="block text-[12px] font-bold mb-1.5" className="block text-[12px] mb-1.5"
style={{ color: "var(--ink-2)" }} style={{ color: "var(--ink-2)" }}
> >
群简介 群简介

View File

@@ -121,23 +121,22 @@ body {
padding: 5px 10px; padding: 5px 10px;
border-radius: 8px; border-radius: 8px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 400;
line-height: 1.35; line-height: 1.4;
letter-spacing: 0.01em; letter-spacing: 0.01em;
pointer-events: none; pointer-events: none;
white-space: nowrap; white-space: normal;
overflow: hidden; overflow-wrap: anywhere;
text-overflow: ellipsis; color: var(--accent);
color: var(--panel); background: color-mix(in srgb, var(--accent) 10%, var(--panel));
background: color-mix(in srgb, var(--ink) 92%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
border: 1px solid color-mix(in srgb, var(--ink) 88%, transparent); box-shadow: var(--shadow-sm);
box-shadow: var(--shadow);
animation: j13-tip-in 80ms ease-out; animation: j13-tip-in 80ms ease-out;
} }
.dark .j13-tip-bubble { .dark .j13-tip-bubble {
color: var(--ink); color: var(--accent);
background: color-mix(in srgb, var(--panel-2) 94%, var(--ink)); background: color-mix(in srgb, var(--accent) 16%, var(--panel));
border-color: var(--line-2); border-color: color-mix(in srgb, var(--accent) 38%, var(--line));
} }
@keyframes j13-tip-in { @keyframes j13-tip-in {
from { from {
@@ -248,7 +247,7 @@ body {
background: var(--panel); background: var(--panel);
color: var(--ink-2); color: var(--ink-2);
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 500;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease; transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
} }
.j13-compose-typechip:hover:not(:disabled) { .j13-compose-typechip:hover:not(:disabled) {
@@ -326,7 +325,7 @@ body {
} }
.j13-compose-title::placeholder { .j13-compose-title::placeholder {
color: var(--ink-3); color: var(--ink-3);
font-weight: 700; font-weight: 500;
} }
.j13-compose-title:focus { .j13-compose-title:focus {
outline: none; outline: none;
@@ -427,7 +426,7 @@ body {
border-radius: 10px; border-radius: 10px;
color: var(--ink); color: var(--ink);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 500;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
transition: background 0.12s ease; transition: background 0.12s ease;
@@ -450,7 +449,7 @@ body {
border-radius: 12px; border-radius: 12px;
color: var(--ink-2); color: var(--ink-2);
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 600;
} }
/* 正文画布占满剩余高度 */ /* 正文画布占满剩余高度 */
@@ -522,7 +521,7 @@ body {
padding: 0 12px; padding: 0 12px;
border-radius: 999px; border-radius: 999px;
font-size: 12.5px; font-size: 12.5px;
font-weight: 700; font-weight: 500;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
@@ -717,7 +716,7 @@ h1, h2, h3, h4 {
/* 主题强调色加深 + 对比色字,胶囊形 */ /* 主题强调色加深 + 对比色字,胶囊形 */
background: color-mix(in srgb, var(--accent) 82%, #000); background: color-mix(in srgb, var(--accent) 82%, #000);
color: #fff !important; color: #fff !important;
font-weight: 600; font-weight: 500;
text-decoration: none !important; text-decoration: none !important;
cursor: pointer; cursor: pointer;
transition: filter 0.12s ease, opacity 0.12s ease; transition: filter 0.12s ease, opacity 0.12s ease;
@@ -754,7 +753,7 @@ a.j13-mention-token:hover {
.kicker { .kicker {
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 12.5px; font-size: 12.5px;
font-weight: 600; font-weight: 500;
letter-spacing: 0.02em; letter-spacing: 0.02em;
color: var(--ink-3); color: var(--ink-3);
} }
@@ -797,7 +796,7 @@ a.j13-mention-token:hover {
padding: 10px 20px; padding: 10px 20px;
border-radius: 999px; border-radius: 999px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 500;
line-height: 1; line-height: 1;
border: 1px solid transparent; border: 1px solid transparent;
cursor: pointer; cursor: pointer;
@@ -918,7 +917,7 @@ a.j13-mention-token:hover {
.field-label { .field-label {
display: block; display: block;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 500;
color: var(--ink-2); color: var(--ink-2);
margin-bottom: 8px; margin-bottom: 8px;
} }
@@ -971,7 +970,7 @@ a.j13-mention-token:hover {
align-items: center; align-items: center;
gap: 4px; gap: 4px;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 500;
padding: 4px 11px; padding: 4px 11px;
background: var(--clay-soft); background: var(--clay-soft);
color: var(--clay); color: var(--clay);
@@ -983,7 +982,7 @@ a.j13-mention-token:hover {
align-items: center; align-items: center;
gap: 4px; gap: 4px;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 500;
padding: 4px 11px; padding: 4px 11px;
background: var(--gold-soft); background: var(--gold-soft);
color: var(--gold); color: var(--gold);
@@ -995,7 +994,7 @@ a.j13-mention-token:hover {
align-items: center; align-items: center;
gap: 4px; gap: 4px;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 500;
padding: 4px 11px; padding: 4px 11px;
background: var(--accent-soft); background: var(--accent-soft);
color: var(--accent); color: var(--accent);
@@ -1006,18 +1005,23 @@ a.j13-mention-token:hover {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 500;
padding: 4px 11px; padding: 4px 11px;
background: color-mix(in srgb, var(--accent) 12%, var(--tint)); /* 固定靛紫,不跟主题 accent,避免与状态徽章撞色 */
color: var(--accent); background: color-mix(in srgb, #5b5bd6 12%, var(--panel));
color: #4f46b8;
border-radius: 999px; border-radius: 999px;
white-space: nowrap; white-space: nowrap;
} }
.dark .badge-type {
background: rgba(165, 180, 252, 0.16);
color: #a5b4fc;
}
.badge-access { .badge-access {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 500;
padding: 4px 11px; padding: 4px 11px;
background: var(--tint); background: var(--tint);
color: var(--ink-2); color: var(--ink-2);
@@ -1025,14 +1029,132 @@ a.j13-mention-token:hover {
white-space: nowrap; white-space: nowrap;
} }
/* 帖子列表中的板块小标签:全站统一浅灰配色,避免每行多色眩目 */ /* 类型帖状态徽章:浅色区分,避免同一灰;状态色固定,不跟主题 */
.badge-status {
display: inline-flex;
align-items: center;
font-size: 12px;
font-weight: 500;
padding: 4px 11px;
border-radius: 999px;
white-space: nowrap;
}
.badge-status-unsolved {
/* 淡红:待解决,与推荐金 / 已解决绿区分 */
background: color-mix(in srgb, #c8281e 11%, var(--panel));
color: #b91c1c;
}
.badge-status-solved {
background: color-mix(in srgb, var(--ok) 16%, var(--panel));
color: var(--ok);
}
.badge-status-open {
/* 进行中:天空蓝,不跟主题 accent,亦区别于类型徽章 */
background: color-mix(in srgb, #0284c7 12%, var(--panel));
color: #0369a1;
}
.badge-status-closed {
background: color-mix(in srgb, #5b6b8c 12%, var(--panel));
color: #5b6b8c;
}
.badge-status-expired {
background: color-mix(in srgb, var(--danger) 12%, var(--panel));
color: var(--danger);
}
.badge-status-drawn {
background: color-mix(in srgb, #7c5cbf 14%, var(--panel));
color: #6a4aad;
}
.dark .badge-status-unsolved {
background: rgba(255, 107, 96, 0.14);
color: #fca5a5;
}
.dark .badge-status-solved {
background: color-mix(in srgb, var(--ok) 22%, transparent);
color: var(--ok);
}
.dark .badge-status-open {
background: rgba(56, 189, 248, 0.14);
color: #7dd3fc;
}
.dark .badge-status-closed {
background: rgba(170, 182, 207, 0.14);
color: #aab6cf;
}
.dark .badge-status-expired {
background: color-mix(in srgb, var(--danger) 20%, transparent);
color: var(--danger);
}
.dark .badge-status-drawn {
background: rgba(192, 168, 245, 0.16);
color: #c0a8f5;
}
/* 站点公告列表 */
.j13-anno-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
}
.j13-anno-item {
display: flex;
align-items: flex-start;
gap: 0;
padding: 11px 12px 11px 0;
border-radius: 12px;
border: 1px solid var(--line);
background: transparent;
transition: background 0.15s ease, border-color 0.15s ease;
overflow: hidden;
}
.j13-anno-item::before {
content: "";
flex-shrink: 0;
width: 3px;
align-self: stretch;
margin-right: 11px;
border-radius: 0 2px 2px 0;
background: color-mix(in srgb, var(--accent) 55%, transparent);
}
.j13-anno-item:hover,
.j13-anno-item:focus-visible {
background: var(--accent-soft);
border-color: color-mix(in srgb, var(--accent) 28%, var(--line));
}
.j13-anno-item:hover::before,
.j13-anno-item:focus-visible::before {
background: var(--accent);
}
.j13-anno-item-body {
min-width: 0;
flex: 1;
padding-right: 2px;
}
.j13-anno-item-title {
display: block;
font-size: 13px;
font-weight: 400;
line-height: 1.45;
color: var(--ink);
}
.j13-anno-item:hover .j13-anno-item-title,
.j13-anno-item:focus-visible .j13-anno-item-title {
color: var(--accent);
}
.j13-anno-item-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
}
.j13-board-tag { .j13-board-tag {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 0 7px; padding: 0 7px;
border-radius: 6px; border-radius: 6px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 500;
line-height: 1.65; line-height: 1.65;
background: var(--tint); background: var(--tint);
color: var(--ink-2); color: var(--ink-2);
@@ -1052,7 +1174,7 @@ a.j13-mention-token:hover {
padding: 2px 10px; padding: 2px 10px;
border-radius: 999px; border-radius: 999px;
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 500;
line-height: 1.55; line-height: 1.55;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -1116,7 +1238,7 @@ a.j13-mention-token:hover {
background: var(--nav-active-bg); background: var(--nav-active-bg);
border-color: var(--nav-active-line); border-color: var(--nav-active-line);
color: var(--nav-active-ink); color: var(--nav-active-ink);
font-weight: 700; font-weight: 600;
} }
/* ----- 辅助 ----- */ /* ----- 辅助 ----- */
@@ -1161,7 +1283,7 @@ a.j13-mention-token:hover {
padding: 4.5px 12px; padding: 4.5px 12px;
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 12.5px; font-size: 12.5px;
font-weight: 600; font-weight: 500;
line-height: 1.35; line-height: 1.35;
color: var(--ink-3); color: var(--ink-3);
cursor: pointer; cursor: pointer;
@@ -1177,7 +1299,7 @@ a.j13-mention-token:hover {
background: var(--accent-soft); background: var(--accent-soft);
border-color: color-mix(in srgb, var(--accent) 38%, var(--line)); border-color: color-mix(in srgb, var(--accent) 38%, var(--line));
color: var(--accent); color: var(--accent);
font-weight: 700; font-weight: 600;
} }
.j13-tab:active { .j13-tab:active {
transform: scale(0.97); transform: scale(0.97);
@@ -1218,7 +1340,7 @@ a.j13-mention-token:hover {
background: var(--nav-active-bg); background: var(--nav-active-bg);
border-color: var(--nav-active-line); border-color: var(--nav-active-line);
color: var(--nav-active-ink); color: var(--nav-active-ink);
font-weight: 700; font-weight: 600;
} }
/* 首页左栏「全部讨论 / 板块目录」卡片:与面包屑的纯文字位置指引区分, /* 首页左栏「全部讨论 / 板块目录」卡片:与面包屑的纯文字位置指引区分,
@@ -1251,6 +1373,7 @@ a.j13-mention-token:hover {
} }
.j13-bcard-name { .j13-bcard-name {
color: var(--ink); color: var(--ink);
font-size: 14px;
} }
.j13-bcard[aria-current="page"] .j13-bcard-name { .j13-bcard[aria-current="page"] .j13-bcard-name {
color: var(--nav-active-ink); color: var(--nav-active-ink);
@@ -1285,7 +1408,7 @@ a.j13-mention-token:hover {
align-items: center; align-items: center;
gap: 7px; gap: 7px;
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 500;
letter-spacing: 0.02em; letter-spacing: 0.02em;
color: var(--ink); color: var(--ink);
} }
@@ -1318,31 +1441,42 @@ a.j13-mention-token:hover {
white-space: nowrap; white-space: nowrap;
} }
/* 站点数据 bento 统计瓦片 */ /* 站点数据:无灰底瓦片,细线分隔 */
.j13-pulse { .j13-pulse {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 8px; gap: 0;
margin-top: 10px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
overflow: hidden;
} }
.j13-pulse-cell { .j13-pulse-cell {
min-width: 0; min-width: 0;
padding: 11px 12px 10px; padding: 12px 14px 11px;
background: var(--panel-2); background: transparent;
border: 1px solid var(--line); border: 0;
border-radius: var(--r-sm); border-radius: 0;
}
.j13-pulse-cell:nth-child(odd) {
border-right: 1px solid var(--line);
}
.j13-pulse-cell:nth-child(-n + 2) {
border-bottom: 1px solid var(--line);
} }
.j13-pulse-lab { .j13-pulse-lab {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
font-size: 11.5px; font-size: 11.5px;
font-weight: 400;
color: var(--ink-3); color: var(--ink-3);
white-space: nowrap; white-space: nowrap;
} }
.j13-pulse-num { .j13-pulse-num {
margin-top: 6px; margin-top: 6px;
font-size: 21px; font-size: 22px;
font-weight: 800; font-weight: 500;
line-height: 1.05; line-height: 1.05;
letter-spacing: -0.015em; letter-spacing: -0.015em;
color: var(--ink); color: var(--ink);
@@ -1354,6 +1488,7 @@ a.j13-mention-token:hover {
gap: 3px; gap: 3px;
margin-top: 6px; margin-top: 6px;
font-size: 10.5px; font-size: 10.5px;
font-weight: 400;
line-height: 1; line-height: 1;
color: var(--ink-3); color: var(--ink-3);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@@ -1361,7 +1496,7 @@ a.j13-mention-token:hover {
} }
.j13-pulse-foot.is-up { .j13-pulse-foot.is-up {
color: var(--ok); color: var(--ok);
font-weight: 600; font-weight: 400;
} }
.j13-pulse-dot { .j13-pulse-dot {
position: relative; position: relative;
@@ -1409,7 +1544,7 @@ a.j13-mention-token:hover {
padding: 6px 11px; padding: 6px 11px;
border-radius: 9999px; border-radius: 9999px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 500;
white-space: nowrap; white-space: nowrap;
} }
@@ -1433,7 +1568,7 @@ a.j13-mention-token:hover {
padding-top: 2px; padding-top: 2px;
text-align: center; text-align: center;
font-size: 13px; font-size: 13px;
font-weight: 800; font-weight: 500;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: var(--ink-3); color: var(--ink-3);
flex-shrink: 0; flex-shrink: 0;
@@ -1481,7 +1616,7 @@ a.j13-mention-token:hover {
background: var(--panel); background: var(--panel);
border-radius: 999px; border-radius: 999px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 500;
color: var(--ink-2); color: var(--ink-2);
white-space: nowrap; white-space: nowrap;
transition: background 0.16s ease, color 0.16s ease, border-color 0.16s ease; transition: background 0.16s ease, color 0.16s ease, border-color 0.16s ease;
@@ -1608,14 +1743,14 @@ a.j13-mention-token:hover {
/* 子回复昵称 */ /* 子回复昵称 */
.cmt-reply-name { .cmt-reply-name {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 500;
color: var(--ink-2); color: var(--ink-2);
} }
/* 楼层号标签(主评论昵称旁) */ /* 楼层号标签(主评论昵称旁) */
.cmt-floor-label { .cmt-floor-label {
font-size: 11.5px; font-size: 11.5px;
font-weight: 600; font-weight: 500;
color: var(--ink-3); color: var(--ink-3);
text-decoration: none; text-decoration: none;
padding: 1px 6px; padding: 1px 6px;
@@ -1650,7 +1785,7 @@ a.j13-mention-token:hover {
/* 被回复者名称(可点击跳转) */ /* 被回复者名称(可点击跳转) */
.cmt-reply-target { .cmt-reply-target {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 500;
color: var(--ink-2); color: var(--ink-2);
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
@@ -1680,7 +1815,7 @@ a.j13-mention-token:hover {
align-items: center; align-items: center;
gap: 4px; gap: 4px;
font-size: 12.5px; font-size: 12.5px;
font-weight: 600; font-weight: 500;
color: var(--accent); color: var(--accent);
padding: 4px 10px; padding: 4px 10px;
border-radius: 99px; border-radius: 99px;
@@ -1765,7 +1900,7 @@ a.j13-mention-token:hover {
z-index: 10001; z-index: 10001;
padding: 10px 18px; padding: 10px 18px;
font-size: 13.5px; font-size: 13.5px;
font-weight: 600; font-weight: 500;
background: var(--accent); background: var(--accent);
color: var(--accent-on); color: var(--accent-on);
border-radius: 999px; border-radius: 999px;

View File

@@ -87,7 +87,7 @@ export default function PointsClient() {
style={{ borderTop: "1px solid var(--line)" }} style={{ borderTop: "1px solid var(--line)" }}
> >
<div className="min-w-0"> <div className="min-w-0">
<p className="font-bold text-[14px]" style={{ color: "var(--ink)" }}> <p className="text-[14px]" style={{ color: "var(--ink)" }}>
{pointReasonLabel(it.reason)} {pointReasonLabel(it.reason)}
</p> </p>
{it.note && ( {it.note && (

View File

@@ -38,7 +38,7 @@ import PostAttachments from "@/components/PostAttachments";
import PostInteractPanels from "@/components/PostInteractPanels"; import PostInteractPanels from "@/components/PostInteractPanels";
import { canModerateBoard } from "@/lib/roles"; import { canModerateBoard } from "@/lib/roles";
import { IconComment, IconEye } from "@/components/Icons"; import { IconComment, IconEye } from "@/components/Icons";
import { normalizePostType, postTypeLabel, contentAccessLabel, typeStatusLabel } from "@/lib/postMeta"; import { normalizePostType, postTypeLabel, contentAccessLabel, typeStatusLabel, typeStatusClass } from "@/lib/postMeta";
interface PageProps { interface PageProps {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@@ -128,7 +128,7 @@ function BoardCard({ board }: { board: Board }) {
{board.name.slice(0, 1)} {board.name.slice(0, 1)}
</span> </span>
<span <span
className="font-bold text-[14.5px] truncate group-hover:text-[var(--accent)] transition-colors" className="font-semibold text-[14.5px] truncate group-hover:text-[var(--accent)] transition-colors"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{board.name} {board.name}
@@ -163,7 +163,7 @@ function AuthorRailCard({ profile }: { profile: UserProfile }) {
<div className="min-w-0"> <div className="min-w-0">
<Link <Link
href={`/u/${u.id}`} href={`/u/${u.id}`}
className="block font-bold text-[14.5px] truncate hover:text-[var(--accent)] transition-colors" className="block font-semibold text-[14.5px] truncate hover:text-[var(--accent)] transition-colors"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{u.nickname} {u.nickname}
@@ -205,7 +205,7 @@ function AuthorPosts({ posts }: { posts: PostListItem[] }) {
className="block rounded-[10px] px-2 py-2 transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]" className="block rounded-[10px] px-2 py-2 transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
> >
<span <span
className="block text-[13px] font-semibold leading-snug line-clamp-2" className="block text-[13px] leading-snug line-clamp-2"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{p.title} {p.title}
@@ -247,7 +247,7 @@ function RelatedPosts({
<span className="j13-rank-no">{i + 1}</span> <span className="j13-rank-no">{i + 1}</span>
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span <span
className="block text-[13px] font-semibold leading-snug line-clamp-2" className="block text-[13px] leading-snug line-clamp-2"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{p.title} {p.title}
@@ -267,7 +267,7 @@ function RelatedPosts({
</ol> </ol>
<Link <Link
href={`/board/${boardId}`} href={`/board/${boardId}`}
className="j13-raillink justify-center mt-1.5 text-[12.5px] font-semibold" className="j13-raillink justify-center mt-1.5 text-[12.5px]"
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
> >
查看全部 <ChevronRight size={13} /> 查看全部 <ChevronRight size={13} />
@@ -281,7 +281,7 @@ function AuthorCardStacked({ profile }: { profile: UserProfile }) {
return ( return (
<div className="panel p-5 sm:p-6"> <div className="panel p-5 sm:p-6">
<p <p
className="text-[11.5px] font-bold tracking-[0.08em] uppercase mb-3.5" className="text-[11.5px] font-medium tracking-[0.08em] uppercase mb-3.5"
style={{ color: "var(--ink-3)" }} style={{ color: "var(--ink-3)" }}
> >
关于作者 关于作者
@@ -294,7 +294,7 @@ function AuthorCardStacked({ profile }: { profile: UserProfile }) {
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<Link <Link
href={`/u/${profile.user.id}`} href={`/u/${profile.user.id}`}
className="font-bold text-[15px] hover:text-[var(--accent)] transition-colors" className="font-semibold text-[15px] hover:text-[var(--accent)] transition-colors"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{profile.user.nickname} {profile.user.nickname}
@@ -352,6 +352,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
const isPublished = post.status === "published"; const isPublished = post.status === "published";
const postType = normalizePostType(post.post_type); const postType = normalizePostType(post.post_type);
const typeStatus = typeStatusLabel(postType, post.type_status); const typeStatus = typeStatusLabel(postType, post.type_status);
const typeStatusCls = typeStatus ? typeStatusClass(postType, post.type_status) : "";
const canManageTyped = const canManageTyped =
!!me.user && (me.user.id === post.user.id || canModerateBoard(me.user, post.board_id)); !!me.user && (me.user.id === post.user.id || canModerateBoard(me.user, post.board_id));
const canSeeStatus = canManageTyped; const canSeeStatus = canManageTyped;
@@ -449,8 +450,8 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
{postType !== "discussion" && postType !== "question" && ( {postType !== "discussion" && postType !== "question" && (
<span className="badge-type shrink-0 mt-2 sm:mt-3">{postTypeLabel(postType)}</span> <span className="badge-type shrink-0 mt-2 sm:mt-3">{postTypeLabel(postType)}</span>
)} )}
{typeStatus && ( {typeStatus && typeStatusCls && (
<span className="badge-access shrink-0 mt-2 sm:mt-3">{typeStatus}</span> <span className={`${typeStatusCls} shrink-0 mt-2 sm:mt-3`}>{typeStatus}</span>
)} )}
{post.content_access && post.content_access !== "public" && ( {post.content_access && post.content_access !== "public" && (
<span className="badge-access shrink-0 mt-2 sm:mt-3"> <span className="badge-access shrink-0 mt-2 sm:mt-3">
@@ -459,7 +460,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
)} )}
{!isPublished && canSeeStatus && post.status === "pending" && ( {!isPublished && canSeeStatus && post.status === "pending" && (
<span <span
className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11.5px] font-bold shrink-0 mt-2 sm:mt-3" className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11.5px] font-medium shrink-0 mt-2 sm:mt-3"
style={{ background: "var(--gold-soft)", color: "var(--gold)" }} style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
> >
等待审核 等待审核
@@ -467,7 +468,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
)} )}
{!isPublished && canSeeStatus && post.status === "rejected" && ( {!isPublished && canSeeStatus && post.status === "rejected" && (
<span <span
className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11.5px] font-bold shrink-0 mt-2 sm:mt-3" className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11.5px] font-medium shrink-0 mt-2 sm:mt-3"
style={{ background: "var(--danger-soft)", color: "var(--danger)" }} style={{ background: "var(--danger-soft)", color: "var(--danger)" }}
> >
未通过审核 未通过审核
@@ -486,7 +487,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
</Link> </Link>
<Link <Link
href={`/u/${post.user.id}`} href={`/u/${post.user.id}`}
className="min-w-0 truncate text-[15px] font-bold hover:text-[var(--accent)] transition-colors" className="min-w-0 truncate text-[15px] font-semibold hover:text-[var(--accent)] transition-colors"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{post.user.nickname} {post.user.nickname}
@@ -607,7 +608,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
/> />
) : ( ) : (
<section className="panel p-6 sm:p-8 mt-6 text-center" aria-label="评论区"> <section className="panel p-6 sm:p-8 mt-6 text-center" aria-label="评论区">
<p className="text-[14px] font-bold" style={{ color: "var(--ink-2)" }}> <p className="text-[14px]" style={{ color: "var(--ink-2)" }}>
{post.status === "pending" ? "帖子审核通过后开放评论" : "帖子未通过审核,评论已关闭"} {post.status === "pending" ? "帖子审核通过后开放评论" : "帖子未通过审核,评论已关闭"}
</p> </p>
<p className="meta mt-1.5 text-[12.5px]"> <p className="meta mt-1.5 text-[12.5px]">

View File

@@ -143,6 +143,7 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
<Link <Link
href={`/u/${id}?tab=security`} href={`/u/${id}?tab=security`}
aria-label="安全设置" aria-label="安全设置"
data-tip="安全设置"
className="inline-flex w-8 h-8 items-center justify-center rounded-full border transition-colors hover:border-[var(--accent)] hover:text-[var(--accent)]" className="inline-flex w-8 h-8 items-center justify-center rounded-full border transition-colors hover:border-[var(--accent)] hover:text-[var(--accent)]"
style={{ background: "var(--panel)", borderColor: "var(--line-2)", color: "var(--ink-2)" }} style={{ background: "var(--panel)", borderColor: "var(--line-2)", color: "var(--ink-2)" }}
> >

View File

@@ -1,4 +1,4 @@
"use client"; "use client";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -280,7 +280,7 @@ export default function CommentSection({
<button <button
onClick={() => openReply(c)} onClick={() => openReply(c)}
className={`cmt-reply-btn transition-colors ${ className={`cmt-reply-btn transition-colors ${
replyTarget?.id === c.id ? "!text-[var(--accent)] font-semibold" : "" replyTarget?.id === c.id ? "!text-[var(--accent)]" : ""
}`} }`}
aria-label={`回复 ${c.user.nickname || c.user.username}`} aria-label={`回复 ${c.user.nickname || c.user.username}`}
data-tip="回复" data-tip="回复"
@@ -294,7 +294,7 @@ export default function CommentSection({
{canAccept(c) && ( {canAccept(c) && (
<button <button
type="button" type="button"
className="cmt-reply-btn !text-[var(--accent)] font-semibold" className="cmt-reply-btn !text-[var(--accent)]"
disabled={acceptingId === c.id} disabled={acceptingId === c.id}
onClick={() => void handleAccept(c.id, c.user.id)} onClick={() => void handleAccept(c.id, c.user.id)}
data-tip={acceptMode === "question" ? "采纳为最佳答案" : "采纳并发放悬赏积分"} data-tip={acceptMode === "question" ? "采纳为最佳答案" : "采纳并发放悬赏积分"}
@@ -308,7 +308,7 @@ export default function CommentSection({
onClick={() => handleDelete(c.id)} onClick={() => handleDelete(c.id)}
disabled={deletingId === c.id} disabled={deletingId === c.id}
className={`meta transition-colors disabled:opacity-50 p-1 ${ className={`meta transition-colors disabled:opacity-50 p-1 ${
confirmingId === c.id ? "!text-[var(--danger)] font-semibold" : "hover:text-[var(--danger)]" confirmingId === c.id ? "!text-[var(--danger)]" : "hover:text-[var(--danger)]"
}`} }`}
aria-label={confirmingId === c.id ? "再次点击确认删除" : "删除评论"} aria-label={confirmingId === c.id ? "再次点击确认删除" : "删除评论"}
data-tip={confirmingId === c.id ? "再点一次确认删除" : "删除"} data-tip={confirmingId === c.id ? "再点一次确认删除" : "删除"}
@@ -350,7 +350,7 @@ export default function CommentSection({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2.5 mb-1.5"> <div className="flex items-center gap-2.5 mb-1.5">
<Link href={`/u/${c.user.id}`} className="group"> <Link href={`/u/${c.user.id}`} className="group">
<span className="font-semibold truncate group-hover:text-[var(--accent)] transition-colors text-sm" style={{ color: "var(--ink)" }}> <span className="truncate group-hover:text-[var(--accent)] transition-colors text-sm" style={{ color: "var(--ink)" }}>
{c.user.nickname || c.user.username} {c.user.nickname || c.user.username}
</span> </span>
</Link> </Link>
@@ -372,7 +372,7 @@ export default function CommentSection({
<button <button
onClick={() => openReply(c)} onClick={() => openReply(c)}
className={`cmt-reply-btn transition-colors ${ className={`cmt-reply-btn transition-colors ${
replyTarget?.id === c.id ? "!text-[var(--accent)] font-semibold" : "" replyTarget?.id === c.id ? "!text-[var(--accent)]" : ""
}`} }`}
aria-label={`回复 ${c.user.nickname || c.user.username}`} aria-label={`回复 ${c.user.nickname || c.user.username}`}
data-tip="回复" data-tip="回复"
@@ -386,7 +386,7 @@ export default function CommentSection({
{canAccept(c) && ( {canAccept(c) && (
<button <button
type="button" type="button"
className="cmt-reply-btn !text-[var(--accent)] font-semibold" className="cmt-reply-btn !text-[var(--accent)]"
disabled={acceptingId === c.id} disabled={acceptingId === c.id}
onClick={() => void handleAccept(c.id, c.user.id)} onClick={() => void handleAccept(c.id, c.user.id)}
data-tip={acceptMode === "question" ? "采纳为最佳答案" : "采纳并发放悬赏积分"} data-tip={acceptMode === "question" ? "采纳为最佳答案" : "采纳并发放悬赏积分"}
@@ -400,7 +400,7 @@ export default function CommentSection({
onClick={() => handleDelete(c.id)} onClick={() => handleDelete(c.id)}
disabled={deletingId === c.id} disabled={deletingId === c.id}
className={`meta transition-colors disabled:opacity-50 p-1 ${ className={`meta transition-colors disabled:opacity-50 p-1 ${
confirmingId === c.id ? "!text-[var(--danger)] font-semibold" : "hover:text-[var(--danger)]" confirmingId === c.id ? "!text-[var(--danger)]" : "hover:text-[var(--danger)]"
}`} }`}
aria-label={confirmingId === c.id ? "再次点击确认删除" : "删除评论"} aria-label={confirmingId === c.id ? "再次点击确认删除" : "删除评论"}
data-tip={confirmingId === c.id ? "再点一次确认删除" : "删除"} data-tip={confirmingId === c.id ? "再点一次确认删除" : "删除"}
@@ -447,11 +447,11 @@ export default function CommentSection({
return ( return (
<section className="panel p-6 sm:p-8 mt-6" aria-label="评论区"> <section className="panel p-6 sm:p-8 mt-6" aria-label="评论区">
<div className="flex items-center gap-2.5 mb-6"> <div className="flex items-center gap-2.5 mb-6">
<h2 className="text-lg font-extrabold" style={{ color: "var(--ink)" }}> <h2 className="text-lg font-semibold" style={{ color: "var(--ink)" }}>
评论 评论
</h2> </h2>
<span <span
className="text-[12px] font-bold px-2 py-0.5 rounded-full tabular-nums" className="text-[12px] font-medium px-2 py-0.5 rounded-full tabular-nums"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }} style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
> >
{totalComments} {totalComments}
@@ -503,7 +503,7 @@ export default function CommentSection({
{comments.length === 0 && ( {comments.length === 0 && (
<div className="text-center py-14"> <div className="text-center py-14">
<MessagesSquare size={28} className="mx-auto mb-3" style={{ color: "var(--ink-3)" }} /> <MessagesSquare size={28} className="mx-auto mb-3" style={{ color: "var(--ink-3)" }} />
<p className="text-sm font-semibold" style={{ color: "var(--ink-2)" }}> <p className="text-sm" style={{ color: "var(--ink-2)" }}>
暂无评论 暂无评论
</p> </p>
<p className="meta text-[12.5px] mt-1">来说两句,抢占沙发</p> <p className="meta text-[12.5px] mt-1">来说两句,抢占沙发</p>

View File

@@ -228,7 +228,7 @@ export default function Header({
<span className="block font-extrabold text-[17px] tracking-tight" style={{ color: "var(--ink)" }}> <span className="block font-extrabold text-[17px] tracking-tight" style={{ color: "var(--ink)" }}>
姜十三 姜十三
</span> </span>
<span className="block text-[10.5px] font-semibold mt-0.5" style={{ color: "var(--ink-3)" }}> <span className="block text-[10.5px] mt-0.5" style={{ color: "var(--ink-3)" }}>
J13 社区 J13 社区
</span> </span>
</span> </span>
@@ -307,7 +307,7 @@ export default function Header({
<MessageSquare size={17} /> <MessageSquare size={17} />
{chatUnread > 0 && ( {chatUnread > 0 && (
<span <span
className="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 text-[10px] font-bold flex items-center justify-center rounded-full" className="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 text-[10px] font-medium flex items-center justify-center rounded-full"
style={{ style={{
background: "var(--clay)", background: "var(--clay)",
color: "#fff", color: "#fff",
@@ -377,7 +377,7 @@ export default function Header({
<MessageSquare size={15} /> 消息 <MessageSquare size={15} /> 消息
{chatUnread > 0 && ( {chatUnread > 0 && (
<span <span
className="inline-flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-bold rounded-full" className="inline-flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full"
style={{ background: "var(--clay)", color: "#fff" }} style={{ background: "var(--clay)", color: "#fff" }}
> >
{chatUnread > 99 ? "99+" : chatUnread} {chatUnread > 99 ? "99+" : chatUnread}
@@ -431,7 +431,7 @@ function NavPill({
return ( return (
<Link <Link
href={href} href={href}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13.5px] font-semibold transition-colors" className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13.5px] transition-colors"
style={ style={
active active
? { background: "var(--accent-soft)", color: "var(--accent)" } ? { background: "var(--accent-soft)", color: "var(--accent)" }
@@ -456,7 +456,7 @@ function MobileLink({
return ( return (
<Link <Link
href={href} href={href}
className="px-3 py-3 rounded-2xl text-[15px] font-semibold transition-colors" className="px-3 py-3 rounded-2xl text-[15px] transition-colors"
style={{ style={{
color: active ? "var(--accent)" : "var(--ink)", color: active ? "var(--accent)" : "var(--ink)",
background: active ? "var(--accent-soft)" : "transparent", background: active ? "var(--accent-soft)" : "transparent",
@@ -492,7 +492,7 @@ function UserMenu({ user, onLogout }: { user: User; onLogout: () => void }) {
}, [open]); }, [open]);
const itemCls = const itemCls =
"w-full flex items-center gap-2.5 px-4 py-2.5 text-[13.5px] font-semibold transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"; "w-full flex items-center gap-2.5 px-4 py-2.5 text-[13.5px] transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]";
return ( return (
<div className="relative hidden lg:block" ref={menuRef}> <div className="relative hidden lg:block" ref={menuRef}>
@@ -508,7 +508,7 @@ function UserMenu({ user, onLogout }: { user: User; onLogout: () => void }) {
> >
<Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={28} /> <Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={28} />
<span <span
className="text-[13px] font-semibold max-w-[90px] truncate" className="text-[13px] max-w-[90px] truncate"
style={{ color: open ? "var(--accent)" : "var(--ink)" }} style={{ color: open ? "var(--accent)" : "var(--ink)" }}
> >
{user.nickname} {user.nickname}
@@ -540,7 +540,7 @@ function UserMenu({ user, onLogout }: { user: User; onLogout: () => void }) {
> >
<Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={38} /> <Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={38} />
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[14px] font-bold truncate" style={{ color: "var(--ink)" }}> <p className="text-[14px] truncate" style={{ color: "var(--ink)" }}>
{user.nickname} {user.nickname}
</p> </p>
<p className="meta text-xs truncate">@ {user.username}</p> <p className="meta text-xs truncate">@ {user.username}</p>

View File

@@ -75,7 +75,7 @@ export default function LikeButton({
data-tip={liked ? "取消点赞" : "点赞"} data-tip={liked ? "取消点赞" : "点赞"}
aria-label={liked ? "取消点赞" : "点赞"} aria-label={liked ? "取消点赞" : "点赞"}
aria-pressed={liked} aria-pressed={liked}
className={`j13-like inline-flex items-center justify-center font-semibold tabular-nums rounded-full border disabled:opacity-50 ${liked ? "is-liked" : ""} ${sizeCls}`} className={`j13-like inline-flex items-center justify-center tabular-nums rounded-full border disabled:opacity-50 ${liked ? "is-liked" : ""} ${sizeCls}`}
> >
<span key={String(liked)} className={liked ? "j13-heart-pop inline-flex" : "inline-flex"}> <span key={String(liked)} className={liked ? "j13-heart-pop inline-flex" : "inline-flex"}>
<IconHeart size={iconSize} filled={liked} /> <IconHeart size={iconSize} filled={liked} />

View File

@@ -168,7 +168,7 @@ export default function NotificationBell({ initialUnread = 0 }: { initialUnread?
<Bell size={17} /> <Bell size={17} />
{unread > 0 && ( {unread > 0 && (
<span <span
className="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 text-[10px] font-bold flex items-center justify-center rounded-full" className="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 text-[10px] font-medium flex items-center justify-center rounded-full"
style={{ style={{
background: "var(--clay)", background: "var(--clay)",
color: "#fff", color: "#fff",
@@ -194,7 +194,7 @@ export default function NotificationBell({ initialUnread = 0 }: { initialUnread?
className="flex items-center justify-between px-5 py-3.5" className="flex items-center justify-between px-5 py-3.5"
style={{ borderBottom: "1px solid var(--line)" }} style={{ borderBottom: "1px solid var(--line)" }}
> >
<span className="font-bold text-[15px]" style={{ color: "var(--ink)" }}>通知</span> <span className="font-semibold text-[15px]" style={{ color: "var(--ink)" }}>通知</span>
{unread > 0 && ( {unread > 0 && (
<button <button
onClick={handleMarkAllRead} onClick={handleMarkAllRead}
@@ -238,7 +238,7 @@ export default function NotificationBell({ initialUnread = 0 }: { initialUnread?
<p className="text-sm leading-snug" style={{ color: "var(--ink)" }}> <p className="text-sm leading-snug" style={{ color: "var(--ink)" }}>
{approval ? ( {approval ? (
<span <span
className="font-bold" className="font-medium"
style={{ color: n.type === "approved" ? "var(--ok)" : "var(--danger)" }} style={{ color: n.type === "approved" ? "var(--ok)" : "var(--danger)" }}
> >
{actionText(n)} {actionText(n)}

View File

@@ -665,7 +665,7 @@ export default function PostComposer(props: PostComposerProps) {
{/* 类型专属面板 */} {/* 类型专属面板 */}
{postType === "poll" && ( {postType === "poll" && (
<div className="j13-compose-typepanel shrink-0 space-y-3"> <div className="j13-compose-typepanel shrink-0 space-y-3">
<p className="font-bold text-[14px]" style={{ color: "var(--ink)" }}> <p className="font-semibold text-[14px]" style={{ color: "var(--ink)" }}>
投票选项 投票选项
</p> </p>
{!canEditPoll && ( {!canEditPoll && (
@@ -732,7 +732,7 @@ export default function PostComposer(props: PostComposerProps) {
)} )}
{postType === "bounty" && ( {postType === "bounty" && (
<div className="j13-compose-typepanel shrink-0 space-y-3"> <div className="j13-compose-typepanel shrink-0 space-y-3">
<p className="font-bold text-[14px]" style={{ color: "var(--ink)" }}> <p className="font-semibold text-[14px]" style={{ color: "var(--ink)" }}>
悬赏积分 悬赏积分
</p> </p>
<p className="meta text-[12.5px]"> <p className="meta text-[12.5px]">
@@ -773,7 +773,7 @@ export default function PostComposer(props: PostComposerProps) {
)} )}
{postType === "lottery" && ( {postType === "lottery" && (
<div className="j13-compose-typepanel shrink-0 space-y-3"> <div className="j13-compose-typepanel shrink-0 space-y-3">
<p className="font-bold text-[14px]" style={{ color: "var(--ink)" }}> <p className="font-semibold text-[14px]" style={{ color: "var(--ink)" }}>
抽奖设置 抽奖设置
</p> </p>
<p className="meta text-[12.5px]"> <p className="meta text-[12.5px]">
@@ -887,7 +887,7 @@ export default function PostComposer(props: PostComposerProps) {
<li key={a.id} className="j13-compose-attach-row"> <li key={a.id} className="j13-compose-attach-row">
<Download size={15} className="shrink-0 opacity-50" /> <Download size={15} className="shrink-0 opacity-50" />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate font-semibold text-[13.5px]">{a.name}</div> <div className="truncate text-[13.5px]">{a.name}</div>
<div className="meta text-[12px]">{formatSize(a.size)}</div> <div className="meta text-[12px]">{formatSize(a.size)}</div>
</div> </div>
<label className="meta text-[12px] flex items-center gap-1.5 shrink-0"> <label className="meta text-[12px] flex items-center gap-1.5 shrink-0">

View File

@@ -75,7 +75,7 @@ function QuestionPanel({ postId, state }: { postId: number; state: QuestionState
<section className="j13-interact-card"> <section className="j13-interact-card">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<h2 className="font-extrabold text-[15px]" style={{ color: "var(--ink)" }}> <h2 className="font-semibold text-[15px]" style={{ color: "var(--ink)" }}>
问答 · {state.solved ? "已解决" : "未解决"} 问答 · {state.solved ? "已解决" : "未解决"}
</h2> </h2>
<p className="meta text-[12.5px] mt-1"> <p className="meta text-[12.5px] mt-1">
@@ -173,7 +173,7 @@ function PollPanel({
return ( return (
<section className="j13-interact-card"> <section className="j13-interact-card">
<div className="flex items-center justify-between gap-3 mb-3"> <div className="flex items-center justify-between gap-3 mb-3">
<h2 className="font-extrabold text-[15px]" style={{ color: "var(--ink)" }}> <h2 className="font-semibold text-[15px]" style={{ color: "var(--ink)" }}>
投票{state.multi ? "(多选)" : "(单选)"} 投票{state.multi ? "(多选)" : "(单选)"}
{state.anonymous ? " · 匿名" : " · 实名"} {state.anonymous ? " · 匿名" : " · 实名"}
</h2> </h2>
@@ -209,7 +209,7 @@ function PollPanel({
/> />
)} )}
<span className="relative flex items-center justify-between gap-3"> <span className="relative flex items-center justify-between gap-3">
<span className="font-semibold text-[14px]" style={{ color: "var(--ink)" }}> <span className="text-[14px]" style={{ color: "var(--ink)" }}>
{state.can_vote ? (checked ? "✓ " : "") : ""} {state.can_vote ? (checked ? "✓ " : "") : ""}
{o.text} {o.text}
</span> </span>
@@ -226,7 +226,7 @@ function PollPanel({
<li key={v.id}> <li key={v.id}>
<Link <Link
href={`/u/${v.id}`} href={`/u/${v.id}`}
className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[12px] font-semibold" className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[12px]"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }} style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
> >
<Avatar name={v.nickname || v.username} src={v.avatar || undefined} size={16} /> <Avatar name={v.nickname || v.username} src={v.avatar || undefined} size={16} />
@@ -304,7 +304,7 @@ function BountyPanel({ postId, state }: { postId: number; state: BountyState })
<section className="j13-interact-card"> <section className="j13-interact-card">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<h2 className="font-extrabold text-[15px]" style={{ color: "var(--ink)" }}> <h2 className="font-semibold text-[15px]" style={{ color: "var(--ink)" }}>
悬赏 {state.points} 积分 悬赏 {state.points} 积分
</h2> </h2>
<p className="meta text-[12.5px] mt-1"> <p className="meta text-[12.5px] mt-1">
@@ -357,7 +357,7 @@ function LotteryPanel({
<section className="j13-interact-card"> <section className="j13-interact-card">
<div className="flex flex-wrap items-center justify-between gap-3 mb-3"> <div className="flex flex-wrap items-center justify-between gap-3 mb-3">
<div> <div>
<h2 className="font-extrabold text-[15px]" style={{ color: "var(--ink)" }}> <h2 className="font-semibold text-[15px]" style={{ color: "var(--ink)" }}>
抽奖 · {state.slots} 个名额 抽奖 · {state.slots} 个名额
</h2> </h2>
<p className="meta text-[12.5px] mt-1"> <p className="meta text-[12.5px] mt-1">
@@ -395,7 +395,7 @@ function LotteryPanel({
</div> </div>
{state.drawn && ( {state.drawn && (
<div> <div>
<p className="font-bold text-[13px] mb-2" style={{ color: "var(--ink)" }}> <p className="font-semibold text-[13px] mb-2" style={{ color: "var(--ink)" }}>
{state.winners.length > 0 ? "中奖名单" : "无人回帖,开奖结果为空"} {state.winners.length > 0 ? "中奖名单" : "无人回帖,开奖结果为空"}
</p> </p>
{state.winners.length > 0 && ( {state.winners.length > 0 && (
@@ -408,7 +408,7 @@ function LotteryPanel({
style={{ background: "var(--accent-soft)", color: "var(--accent)" }} style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
> >
<Avatar name={w.nickname || w.username} src={w.avatar || undefined} size={22} /> <Avatar name={w.nickname || w.username} src={w.avatar || undefined} size={22} />
<span className="text-[13px] font-bold">{w.nickname || w.username}</span> <span className="text-[13px]">{w.nickname || w.username}</span>
</Link> </Link>
</li> </li>
))} ))}

View File

@@ -2,14 +2,14 @@ import Link from "next/link";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import type { PostListItem } from "@/lib/api"; import type { PostListItem } from "@/lib/api";
import { formatRelative } from "@/lib/format"; import { formatRelative } from "@/lib/format";
import { normalizePostType, postTypeLabel, contentAccessLabel, typeStatusLabel } from "@/lib/postMeta"; import { normalizePostType, postTypeLabel, contentAccessLabel, typeStatusLabel, typeStatusClass } from "@/lib/postMeta";
import LikeButton from "./LikeButton"; import LikeButton from "./LikeButton";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import { IconComment, IconEye } from "./Icons"; import { IconComment, IconEye } from "./Icons";
/** /**
* 帖子列表行(参考社区信息流样式): * 帖子列表行(参考社区信息流样式):
* 大头像 + 加粗标题(徽章内联)+ 第二行「板块 · 作者 · 发布时间 ← 最后回复」+ * 大头像 + 标题(徽章内联)+ 第二行「板块 · 作者 · 发布时间 ← 最后回复」+
* 右侧评论数等统计垂直居中。用于首页/板块页/搜索结果。 * 右侧评论数等统计垂直居中。用于首页/板块页/搜索结果。
*/ */
export default function PostRow({ export default function PostRow({
@@ -31,6 +31,7 @@ export default function PostRow({
const replied = lastReply && new Date(lastReply.created_at).getTime() > new Date(post.created_at).getTime() + 60_000; const replied = lastReply && new Date(lastReply.created_at).getTime() > new Date(post.created_at).getTime() + 60_000;
const pt = normalizePostType(post.post_type); const pt = normalizePostType(post.post_type);
const statusBadge = pt !== "discussion" ? typeStatusLabel(pt, post.type_status) : ""; const statusBadge = pt !== "discussion" ? typeStatusLabel(pt, post.type_status) : "";
const statusClass = statusBadge ? typeStatusClass(pt, post.type_status) : "";
return ( return (
<div <div
@@ -65,9 +66,7 @@ export default function PostRow({
aria-label={post.title} aria-label={post.title}
> >
<h3 <h3
className={`font-bold leading-snug truncate transition-colors group-hover:text-[var(--accent)] ${ className="text-[15px] font-normal leading-snug truncate transition-colors group-hover:text-[var(--accent)]"
compact ? "text-[15px]" : "text-[16px] sm:text-[17px]"
}`}
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{post.title} {post.title}
@@ -78,8 +77,8 @@ export default function PostRow({
{pt !== "discussion" && pt !== "question" && ( {pt !== "discussion" && pt !== "question" && (
<span className="badge-type shrink-0">{postTypeLabel(post.post_type)}</span> <span className="badge-type shrink-0">{postTypeLabel(post.post_type)}</span>
)} )}
{statusBadge && ( {statusBadge && statusClass && (
<span className="badge-access shrink-0">{statusBadge}</span> <span className={`${statusClass} shrink-0`}>{statusBadge}</span>
)} )}
{post.content_access && post.content_access !== "public" && ( {post.content_access && post.content_access !== "public" && (
<span className="badge-access shrink-0">{contentAccessLabel(post.content_access)}</span> <span className="badge-access shrink-0">{contentAccessLabel(post.content_access)}</span>
@@ -98,7 +97,7 @@ export default function PostRow({
)} )}
<Link <Link
href={`/u/${post.user.id}`} href={`/u/${post.user.id}`}
className="font-semibold hover:text-[var(--accent)] transition-colors" className="hover:text-[var(--accent)] transition-colors"
style={{ color: "var(--ink-2)" }} style={{ color: "var(--ink-2)" }}
> >
{post.user.nickname} {post.user.nickname}

View File

@@ -57,7 +57,7 @@ export default function RoleBadge({
return ( return (
<span <span
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-bold leading-[1.6] ${ className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium leading-[1.6] ${
truncate ? "max-w-full whitespace-nowrap" : "whitespace-nowrap" truncate ? "max-w-full whitespace-nowrap" : "whitespace-nowrap"
} ${className}`} } ${className}`}
style={{ background: `var(${meta.softToken})`, color: `var(${meta.colorToken})` }} style={{ background: `var(${meta.softToken})`, color: `var(${meta.colorToken})` }}

View File

@@ -10,6 +10,67 @@ type TipState = {
place: "top" | "bottom"; place: "top" | "bottom";
} | null; } | null;
function norm(s: string): string {
return s.replace(/\s+/g, " ").trim();
}
/** 可见文字:跳过 aria-hidden / svg / img,避免把图标标题算进来 */
function getVisibleText(root: Element): string {
const parts: string[] = [];
const walk = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) {
const t = node.textContent?.replace(/\s+/g, " ").trim();
if (t) parts.push(t);
return;
}
if (!(node instanceof Element)) return;
if (node.getAttribute("aria-hidden") === "true") return;
const tag = node.tagName.toUpperCase();
if (tag === "SVG" || tag === "IMG" || tag === "CANVAS" || tag === "PATH") return;
for (const child of node.childNodes) walk(child);
};
walk(root);
return norm(parts.join(" "));
}
/** 是否存在省略截断(含 line-clamp) */
function isVisuallyTruncated(root: Element): boolean {
const nodes: Element[] = [root, ...Array.from(root.querySelectorAll("*"))];
for (const el of nodes) {
if (!(el instanceof HTMLElement)) continue;
if (el.clientWidth <= 0 && el.clientHeight <= 0) continue;
const st = getComputedStyle(el);
const ellipsis = st.textOverflow === "ellipsis" || el.classList.contains("truncate");
const nowrapHidden =
(st.overflowX === "hidden" || st.overflow === "hidden") &&
(st.whiteSpace === "nowrap" || st.whiteSpace === "pre");
const clampRaw = st.getPropertyValue("-webkit-line-clamp").trim();
const clamped = Boolean(clampRaw) && clampRaw !== "none";
if (ellipsis || nowrapHidden) {
if (el.scrollWidth > el.clientWidth + 1) return true;
}
if (clamped && el.scrollHeight > el.clientHeight + 1) return true;
}
return false;
}
/**
* 仅在以下场景显示气泡:
* 1. 提示文案与可见内容不一致(且提示未完整出现在可见文字中)
* 2. 停留目标无可见文字(图标等)
* 3. 可见内容被省略截断,需要补全
*/
function shouldShowTip(host: Element, tip: string): boolean {
if (isVisuallyTruncated(host)) return true;
const visible = getVisibleText(host);
if (!visible) return true;
const nTip = norm(tip);
const nVis = visible;
if (nTip === nVis) return false;
if (nVis.includes(nTip)) return false;
return true;
}
/** /**
* 全站悬停即显提示:监听 [data-tip],替代浏览器原生 title 的延迟气泡。 * 全站悬停即显提示:监听 [data-tip],替代浏览器原生 title 的延迟气泡。
* 挂在 root layout 即可,业务侧写 data-tip="文案" 并保留 aria-label。 * 挂在 root layout 即可,业务侧写 data-tip="文案" 并保留 aria-label。
@@ -38,6 +99,16 @@ export default function TipHost() {
}); });
}; };
const tryShow = (host: Element) => {
const text = host.getAttribute("data-tip")?.trim();
if (!text || !shouldShowTip(host, text)) {
hide();
return;
}
activeEl.current = host;
placeTip(host, text);
};
const onOver = (e: Event) => { const onOver = (e: Event) => {
const t = e.target; const t = e.target;
if (!(t instanceof Element)) return; if (!(t instanceof Element)) return;
@@ -46,18 +117,12 @@ export default function TipHost() {
if (activeEl.current) hide(); if (activeEl.current) hide();
return; return;
} }
const text = host.getAttribute("data-tip")?.trim(); tryShow(host);
if (!text) {
hide();
return;
}
activeEl.current = host;
placeTip(host, text);
}; };
const onOut = (e: Event) => { const onOut = (e: Event) => {
if (!(e instanceof MouseEvent)) return; if (!(e instanceof MouseEvent) && !(e instanceof FocusEvent)) return;
const related = e.relatedTarget; const related = "relatedTarget" in e ? e.relatedTarget : null;
if ( if (
related instanceof Node && related instanceof Node &&
activeEl.current && activeEl.current &&
@@ -70,12 +135,7 @@ export default function TipHost() {
const onScrollOrResize = () => { const onScrollOrResize = () => {
if (!activeEl.current) return; if (!activeEl.current) return;
const text = activeEl.current.getAttribute("data-tip")?.trim(); tryShow(activeEl.current);
if (!text) {
hide();
return;
}
placeTip(activeEl.current, text);
}; };
// mouseover 冒泡,便于 closest;立即显示,无延时 // mouseover 冒泡,便于 closest;立即显示,无延时

View File

@@ -43,7 +43,7 @@ export function PulseCheckin({ initial }: { initial?: CheckinStatus | null }) {
{checked ? <CalendarCheck size={16} /> : <CalendarDays size={16} />} {checked ? <CalendarCheck size={16} /> : <CalendarDays size={16} />}
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-[12.5px] font-bold leading-tight" style={{ color: "var(--ink)" }}> <p className="text-[12.5px] leading-tight" style={{ color: "var(--ink)" }}>
每日签到 每日签到
</p> </p>
<p className="meta mt-0.5 truncate text-[11px]"> <p className="meta mt-0.5 truncate text-[11px]">

View File

@@ -73,10 +73,10 @@ function PulseCard({ stats, checkin }: { stats: OverviewStats; checkin?: Checkin
实时 实时
</span> </span>
</div> </div>
<div className="j13-pulse mt-3"> <div className="j13-pulse">
{cells.map((c) => ( {cells.map((c) => (
<div key={c.label} className="j13-pulse-cell"> <div key={c.label} className="j13-pulse-cell">
<p className="j13-pulse-lab" style={{ color: "var(--ink-3)" }}> <p className="j13-pulse-lab">
<span style={{ color: "var(--accent)" }} aria-hidden="true"> <span style={{ color: "var(--accent)" }} aria-hidden="true">
{c.icon} {c.icon}
</span> </span>
@@ -102,31 +102,30 @@ function PulseCard({ stats, checkin }: { stats: OverviewStats; checkin?: Checkin
function Announcements({ items }: { items: AnnouncementSummary[] }) { function Announcements({ items }: { items: AnnouncementSummary[] }) {
return ( return (
<section className="j13-module" aria-label="站点公告"> <section className="j13-module" aria-label="站点公告">
<p className="j13-module-title mb-2"> <p className="j13-module-title">
<Megaphone size={15} style={{ color: "var(--accent)" }} /> 站点公告 <Megaphone size={15} style={{ color: "var(--accent)" }} /> 站点公告
</p> </p>
{items.length === 0 ? ( {items.length === 0 ? (
<p className="meta px-1 py-1.5">暂无公告</p> <p className="meta mt-2 px-0.5">暂无公告</p>
) : ( ) : (
<ul> <ul className="j13-anno-list">
{items.map((a) => ( {items.map((a) => (
<li key={a.id}> <li key={a.id}>
<Link <Link
href={`/announcement/${a.id}`} href={`/announcement/${a.id}`}
className="group block -mx-1.5 rounded-lg px-1.5 py-1.5 transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]" className="j13-anno-item"
data-tip={a.title}
> >
<span className="flex items-center justify-between gap-2"> <span className="j13-anno-item-body">
<span className="j13-anno-item-title truncate">{a.title}</span>
<span className="j13-anno-item-meta">
{a.tag ? (
<span className={`j13-anno-tag is-${a.tag_color}`}>{a.tag}</span> <span className={`j13-anno-tag is-${a.tag_color}`}>{a.tag}</span>
<span className="meta shrink-0 text-[11px] tabular-nums"> ) : null}
<span className="meta text-[11px] tabular-nums">
{formatShortDate(a.created_at)} {formatShortDate(a.created_at)}
</span> </span>
</span> </span>
<span
className="mt-1.5 block truncate text-[13px] leading-snug transition-colors group-hover:text-[var(--accent)]"
style={{ color: "var(--ink)" }}
data-tip={a.title}
>
{a.title}
</span> </span>
</Link> </Link>
</li> </li>
@@ -236,7 +235,7 @@ function ActiveUsers({ users }: { users: ActiveUser[] }) {
<Avatar name={name} src={u.avatar} size={30} /> <Avatar name={name} src={u.avatar} size={30} />
<span className="flex-1 min-w-0"> <span className="flex-1 min-w-0">
<span <span
className="block truncate text-[13px] font-semibold leading-tight" className="block truncate text-[13px] leading-tight"
style={{ color: "var(--ink)" }} style={{ color: "var(--ink)" }}
> >
{name} {name}
@@ -251,7 +250,7 @@ function ActiveUsers({ users }: { users: ActiveUser[] }) {
</span> </span>
</span> </span>
<span <span
className="shrink-0 text-[13px] font-extrabold tabular-nums" className="shrink-0 text-[13px] tabular-nums"
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
aria-label={`本周共 ${u.post_count + u.comment_count} 次互动`} aria-label={`本周共 ${u.post_count + u.comment_count} 次互动`}
> >

View File

@@ -60,7 +60,7 @@ export default function FeedNewTip({
<button <button
type="button" type="button"
onClick={handleClick} onClick={handleClick}
className="j13-feed-new-tip w-full inline-flex items-center justify-center gap-1.5 py-2 px-3 text-[13px] font-semibold rounded-[var(--r-sm)] transition-colors" className="j13-feed-new-tip w-full inline-flex items-center justify-center gap-1.5 py-2 px-3 text-[13px] rounded-[var(--r-sm)] transition-colors"
style={{ style={{
color: "var(--accent)", color: "var(--accent)",
background: "var(--accent-soft)", background: "var(--accent-soft)",

View File

@@ -10,23 +10,14 @@ function boardHref(id: number) {
return `/?board=${id}`; return `/?board=${id}`;
} }
// 桌面左栏:发帖 CTA + 板块导航 + 登录态快捷入口 // 桌面左栏:板块导航
export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user: User | null }) { export function WorkspaceNav({ boards }: { boards: BoardWithCount[]; user?: User | null }) {
const params = useSearchParams(); const params = useSearchParams();
const activeBoard = Number(params.get("board")) || 0; const activeBoard = Number(params.get("board")) || 0;
const composePath =
activeBoard > 0 ? `/compose?board=${activeBoard}` : "/compose";
const composeHref = user
? composePath
: `/login?redirect=${encodeURIComponent(composePath)}`;
return ( return (
<nav className="hidden lg:block w-56 xl:w-60 shrink-0" aria-label="工作台导航"> <nav className="hidden lg:block w-56 xl:w-60 shrink-0" aria-label="工作台导航">
<div className="sticky top-24 space-y-5"> <div className="sticky top-24 space-y-5">
<Link href={composeHref} className="btn btn-primary w-full justify-center">
<PenSquare size={16} /> 发起讨论
</Link>
{/* 「全部讨论」是重置筛选的动作入口,与板块目录分区展示;卡片样式与下方板块一致 */} {/* 「全部讨论」是重置筛选的动作入口,与板块目录分区展示;卡片样式与下方板块一致 */}
<div className="space-y-0.5"> <div className="space-y-0.5">
<Link href="/" className="j13-bcard" aria-current={activeBoard === 0 ? "page" : undefined}> <Link href="/" className="j13-bcard" aria-current={activeBoard === 0 ? "page" : undefined}>
@@ -37,7 +28,7 @@ export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user:
> >
<Home size={16} /> <Home size={16} />
</span> </span>
<span className="j13-bcard-name flex-1 truncate text-[13px] font-bold"> <span className="j13-bcard-name flex-1 truncate text-[14px]">
全部讨论 全部讨论
</span> </span>
</Link> </Link>
@@ -48,7 +39,7 @@ export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user:
<div> <div>
<Link <Link
href="/boards" href="/boards"
className="group px-1 mb-2 flex items-center justify-between text-[11.5px] font-bold tracking-[0.08em] uppercase text-[var(--ink-3)] hover:text-[var(--accent)] transition-colors" className="group px-1 mb-2 flex items-center justify-between text-[11.5px] font-medium tracking-[0.08em] uppercase text-[var(--ink-3)] hover:text-[var(--accent)] transition-colors"
aria-label="进入板块目录列表页" aria-label="进入板块目录列表页"
> >
<span className="flex items-center"> <span className="flex items-center">
@@ -60,7 +51,7 @@ export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user:
/> />
</span> </span>
{boards.length > 0 && ( {boards.length > 0 && (
<span className="tracking-normal font-semibold tabular-nums">{boards.length} 个</span> <span className="tracking-normal tabular-nums">{boards.length} 个</span>
)} )}
</Link> </Link>
<div className="space-y-2"> <div className="space-y-2">
@@ -76,7 +67,7 @@ export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user:
{b.name.slice(0, 1)} {b.name.slice(0, 1)}
</span> </span>
<span className="min-w-0 flex-1 flex items-center justify-between gap-2"> <span className="min-w-0 flex-1 flex items-center justify-between gap-2">
<span className="j13-bcard-name truncate text-[13px] font-bold"> <span className="j13-bcard-name truncate text-[14px]">
{b.name} {b.name}
</span> </span>
<span className="meta shrink-0 text-[11px] tabular-nums">{b.post_count} 篇</span> <span className="meta shrink-0 text-[11px] tabular-nums">{b.post_count} 篇</span>

View File

@@ -59,3 +59,31 @@ export function typeStatusLabel(postType?: string, typeStatus?: string): string
return ""; return "";
} }
} }
/** 状态徽章 CSS class(浅色区分) */
export function typeStatusClass(postType?: string, typeStatus?: string): string {
const pt = normalizePostType(postType);
const s = (typeStatus || "").toLowerCase();
let kind = "open";
switch (pt) {
case "question":
kind = s === "solved" ? "solved" : "unsolved";
break;
case "poll":
kind = s === "closed" ? "closed" : "open";
break;
case "bounty":
if (s === "solved") kind = "solved";
else if (s === "expired") kind = "expired";
else kind = "open";
break;
case "lottery":
if (s === "drawn") kind = "drawn";
else if (s === "closed") kind = "closed";
else kind = "open";
break;
default:
return "";
}
return `badge-status badge-status-${kind}`;
}