feat(home): add homepage hero banner and migrate community stats
- 新增首页欢迎横幅组件,包含时段问候、用户问候语、发帖引导按钮和社区统计数据 - 从CommunityPanels中移除stats相关逻辑,将社区统计迁移到新的HomeHero组件中 - 优化全局样式,添加横幅相关的CSS动画和样式类
This commit is contained in:
@@ -582,6 +582,77 @@ h1, h2, h3, h4 {
|
|||||||
border-color: color-mix(in srgb, var(--accent) 32%, var(--line));
|
border-color: color-mix(in srgb, var(--accent) 32%, var(--line));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 首页欢迎横幅 */
|
||||||
|
.j13-hero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 20px 22px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
background:
|
||||||
|
linear-gradient(115deg, color-mix(in srgb, var(--accent-soft) 75%, var(--panel)) 0%, var(--panel) 60%);
|
||||||
|
}
|
||||||
|
.j13-hero-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: -72px;
|
||||||
|
right: -52px;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.j13-hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.j13-hero-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.j13-hero-stat-num {
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
color: var(--ink);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.j13-hero-stat-num.is-hot { color: var(--accent); }
|
||||||
|
.j13-hero-stat-lab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.j13-dot-pulse {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--accent);
|
||||||
|
animation: j13-dot-pulse 2.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.j13-hero { padding: 22px 26px; }
|
||||||
|
.j13-hero-stats {
|
||||||
|
display: flex;
|
||||||
|
width: auto;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.j13-hero-stat + .j13-hero-stat {
|
||||||
|
margin-left: 22px;
|
||||||
|
padding-left: 22px;
|
||||||
|
border-left: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 移动端横向板块条(滚动条隐藏,保留触控惯性) */
|
/* 移动端横向板块条(滚动条隐藏,保留触控惯性) */
|
||||||
.j13-chips {
|
.j13-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -739,6 +810,11 @@ h1, h2, h3, h4 {
|
|||||||
}
|
}
|
||||||
.j13-fade-up { animation: j13-fade-up 0.2s ease-out both; }
|
.j13-fade-up { animation: j13-fade-up 0.2s ease-out both; }
|
||||||
|
|
||||||
|
@keyframes j13-dot-pulse {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.35; transform: scale(0.8); }
|
||||||
|
}
|
||||||
|
|
||||||
/* 跳到主内容 */
|
/* 跳到主内容 */
|
||||||
.skip-link {
|
.skip-link {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import PostRow from "@/components/PostRow";
|
|||||||
import FeedTabs, { type FeedSort } from "@/components/home/FeedTabs";
|
import FeedTabs, { type FeedSort } from "@/components/home/FeedTabs";
|
||||||
import { WorkspaceNav, WorkspaceNavMobile } from "@/components/home/WorkspaceNav";
|
import { WorkspaceNav, WorkspaceNavMobile } from "@/components/home/WorkspaceNav";
|
||||||
import { CommunityPanels, CommunityPanelsMobile } from "@/components/home/CommunityPanels";
|
import { CommunityPanels, CommunityPanelsMobile } from "@/components/home/CommunityPanels";
|
||||||
|
import HomeHero from "@/components/home/HomeHero";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "姜十三论坛 · 技术讨论社区",
|
title: "姜十三论坛 · 技术讨论社区",
|
||||||
@@ -150,6 +151,8 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<HomeHero user={user} stats={overview?.stats ?? null} />
|
||||||
|
|
||||||
<WorkspaceNavMobile boards={boards} user={user} />
|
<WorkspaceNavMobile boards={boards} user={user} />
|
||||||
|
|
||||||
<div className="flex items-start gap-6 xl:gap-7 mt-5 lg:mt-0">
|
<div className="flex items-start gap-6 xl:gap-7 mt-5 lg:mt-0">
|
||||||
@@ -201,22 +204,14 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
|||||||
|
|
||||||
{/* 右栏:社区面板 */}
|
{/* 右栏:社区面板 */}
|
||||||
{overview && (
|
{overview && (
|
||||||
<CommunityPanels
|
<CommunityPanels hot={overview.hot} activeUsers={overview.active_users} />
|
||||||
stats={overview.stats}
|
|
||||||
hot={overview.hot}
|
|
||||||
activeUsers={overview.active_users}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 窄屏:面板下沉到帖子流之后 */}
|
{/* 窄屏:面板下沉到帖子流之后 */}
|
||||||
{overview && (
|
{overview && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<CommunityPanelsMobile
|
<CommunityPanelsMobile hot={overview.hot} activeUsers={overview.active_users} />
|
||||||
stats={overview.stats}
|
|
||||||
hot={overview.hot}
|
|
||||||
activeUsers={overview.active_users}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,36 +1,8 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BarChart3, Flame, Users, MessagesSquare, MessageCircle, Heart, Sparkles } from "lucide-react";
|
import { Flame, Users, MessageCircle, Heart } from "lucide-react";
|
||||||
import type { OverviewStats, PostListItem, ActiveUser } from "@/lib/api";
|
import type { PostListItem, ActiveUser } from "@/lib/api";
|
||||||
import Avatar from "@/components/Avatar";
|
import Avatar from "@/components/Avatar";
|
||||||
|
|
||||||
/* ---------- 统计 2×2 ---------- */
|
|
||||||
|
|
||||||
function StatsGrid({ stats }: { stats: OverviewStats }) {
|
|
||||||
const items = [
|
|
||||||
{ icon: MessagesSquare, label: "讨论", value: stats.posts },
|
|
||||||
{ icon: Users, label: "成员", value: stats.users },
|
|
||||||
{ icon: MessageCircle, label: "评论", value: stats.comments },
|
|
||||||
{ icon: Sparkles, label: "今日", value: stats.today_posts },
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<section className="j13-module" aria-label="社区数据">
|
|
||||||
<p className="j13-module-title mb-3">
|
|
||||||
<BarChart3 size={15} style={{ color: "var(--accent)" }} /> 社区数据
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{items.map(({ icon: Icon, label, value }) => (
|
|
||||||
<div key={label} className="j13-stat">
|
|
||||||
<span className="j13-stat-num">{value}</span>
|
|
||||||
<span className="j13-stat-lab">
|
|
||||||
<Icon size={12} /> {label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 热门榜 ---------- */
|
/* ---------- 热门榜 ---------- */
|
||||||
|
|
||||||
function HotRanking({ posts }: { posts: PostListItem[] }) {
|
function HotRanking({ posts }: { posts: PostListItem[] }) {
|
||||||
@@ -77,8 +49,8 @@ function HotRanking({ posts }: { posts: PostListItem[] }) {
|
|||||||
|
|
||||||
function ActiveUsers({ users }: { users: ActiveUser[] }) {
|
function ActiveUsers({ users }: { users: ActiveUser[] }) {
|
||||||
return (
|
return (
|
||||||
<section className="j13-module" aria-labelledby="active-users-title">
|
<section className="j13-module" aria-label="活跃成员">
|
||||||
<p className="j13-module-title mb-3" id="active-users-title">
|
<p className="j13-module-title mb-3">
|
||||||
<Users size={15} style={{ color: "var(--accent)" }} /> 活跃成员
|
<Users size={15} style={{ color: "var(--accent)" }} /> 活跃成员
|
||||||
</p>
|
</p>
|
||||||
{users.length === 0 ? (
|
{users.length === 0 ? (
|
||||||
@@ -104,18 +76,15 @@ function ActiveUsers({ users }: { users: ActiveUser[] }) {
|
|||||||
/* ---------- 桌面 / 移动两种装配 ---------- */
|
/* ---------- 桌面 / 移动两种装配 ---------- */
|
||||||
|
|
||||||
export function CommunityPanels({
|
export function CommunityPanels({
|
||||||
stats,
|
|
||||||
hot,
|
hot,
|
||||||
activeUsers,
|
activeUsers,
|
||||||
}: {
|
}: {
|
||||||
stats: OverviewStats;
|
|
||||||
hot: PostListItem[];
|
hot: PostListItem[];
|
||||||
activeUsers: ActiveUser[];
|
activeUsers: ActiveUser[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<aside className="hidden lg:block w-[280px] xl:w-[300px] shrink-0" aria-label="社区面板">
|
<aside className="hidden lg:block w-[280px] xl:w-[300px] shrink-0" aria-label="社区面板">
|
||||||
<div className="sticky top-24 space-y-4">
|
<div className="sticky top-24 space-y-4">
|
||||||
<StatsGrid stats={stats} />
|
|
||||||
<HotRanking posts={hot} />
|
<HotRanking posts={hot} />
|
||||||
<ActiveUsers users={activeUsers} />
|
<ActiveUsers users={activeUsers} />
|
||||||
</div>
|
</div>
|
||||||
@@ -124,17 +93,14 @@ export function CommunityPanels({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CommunityPanelsMobile({
|
export function CommunityPanelsMobile({
|
||||||
stats,
|
|
||||||
hot,
|
hot,
|
||||||
activeUsers,
|
activeUsers,
|
||||||
}: {
|
}: {
|
||||||
stats: OverviewStats;
|
|
||||||
hot: PostListItem[];
|
hot: PostListItem[];
|
||||||
activeUsers: ActiveUser[];
|
activeUsers: ActiveUser[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="lg:hidden space-y-4" aria-label="社区面板">
|
<div className="lg:hidden space-y-4" aria-label="社区面板">
|
||||||
<StatsGrid stats={stats} />
|
|
||||||
<HotRanking posts={hot} />
|
<HotRanking posts={hot} />
|
||||||
<ActiveUsers users={activeUsers} />
|
<ActiveUsers users={activeUsers} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
76
frontend/components/home/HomeHero.tsx
Normal file
76
frontend/components/home/HomeHero.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { PenSquare, LogIn, UserPlus, MessagesSquare, Users, MessageCircle, Sparkles } from "lucide-react";
|
||||||
|
import type { OverviewStats } from "@/lib/api";
|
||||||
|
|
||||||
|
type HeroUser = { username: string; nickname: string } | null;
|
||||||
|
|
||||||
|
function greetingOf(h: number): string {
|
||||||
|
if (h >= 5 && h < 11) return "早上好";
|
||||||
|
if (h >= 11 && h < 14) return "中午好";
|
||||||
|
if (h >= 14 && h < 18) return "下午好";
|
||||||
|
if (h >= 18 && h < 23) return "晚上好";
|
||||||
|
return "夜深了";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首页欢迎横幅:时段问候 + 站点主张 + 发帖引导 + 内联社区数据。纯服务端组件。
|
||||||
|
export default function HomeHero({ user, stats }: { user: HeroUser; stats: OverviewStats | null }) {
|
||||||
|
const name = user ? user.nickname || user.username : null;
|
||||||
|
const hello = greetingOf(new Date().getHours());
|
||||||
|
const today = stats?.today_posts ?? 0;
|
||||||
|
|
||||||
|
const statItems = stats
|
||||||
|
? [
|
||||||
|
{ icon: MessagesSquare, label: "讨论", value: stats.posts, hot: false },
|
||||||
|
{ icon: Users, label: "成员", value: stats.users, hot: false },
|
||||||
|
{ icon: MessageCircle, label: "评论", value: stats.comments, hot: false },
|
||||||
|
{ icon: Sparkles, label: today > 0 ? "今日新帖" : "今日", value: today, hot: today > 0 },
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="j13-hero mb-5 lg:mb-6" aria-label="欢迎">
|
||||||
|
<span aria-hidden className="j13-hero-glow" />
|
||||||
|
<div className="relative flex flex-col lg:flex-row lg:items-center lg:justify-between gap-5">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="meta text-[13px]">{name ? `${hello},${name}` : `${hello},欢迎来到姜十三`}</p>
|
||||||
|
<h2
|
||||||
|
className="mt-1.5 text-[19px] sm:text-[22px] font-extrabold tracking-tight"
|
||||||
|
style={{ color: "var(--ink)" }}
|
||||||
|
>
|
||||||
|
分享技术、讨论问题、交流心得
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2.5">
|
||||||
|
{user ? (
|
||||||
|
<Link href="/compose" className="btn btn-primary w-full sm:w-auto justify-center">
|
||||||
|
<PenSquare size={15} /> 发起讨论
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link href="/login?redirect=/" className="btn btn-primary w-full sm:w-auto justify-center">
|
||||||
|
<LogIn size={15} /> 登录后参与讨论
|
||||||
|
</Link>
|
||||||
|
<Link href="/register" className="btn btn-line w-full sm:w-auto justify-center">
|
||||||
|
<UserPlus size={15} /> 注册
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{statItems.length > 0 && (
|
||||||
|
<dl className="j13-hero-stats shrink-0" aria-label="社区数据">
|
||||||
|
{statItems.map(({ icon: Icon, label, value, hot }) => (
|
||||||
|
<div key={label} className="j13-hero-stat">
|
||||||
|
<dd className={`j13-hero-stat-num${hot ? " is-hot" : ""}`}>{value}</dd>
|
||||||
|
<dt className="j13-hero-stat-lab">
|
||||||
|
{hot && <span className="j13-dot-pulse" aria-hidden />}
|
||||||
|
<Icon size={12} /> {label}
|
||||||
|
</dt>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user