新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
675 lines
21 KiB
TypeScript
675 lines
21 KiB
TypeScript
import Link from "next/link";
|
||
import {
|
||
Flame,
|
||
Users,
|
||
MessageCircle,
|
||
Heart,
|
||
UserPlus,
|
||
Link2,
|
||
Megaphone,
|
||
BarChart3,
|
||
PenLine,
|
||
Activity,
|
||
TrendingUp,
|
||
Pin,
|
||
ChevronRight,
|
||
CircleDollarSign,
|
||
Handshake,
|
||
} from "lucide-react";
|
||
import type {
|
||
PostListItem,
|
||
ActiveUser,
|
||
OverviewStats,
|
||
AnnouncementSummary,
|
||
NewUser,
|
||
CheckinStatus,
|
||
SidebarPageItem,
|
||
PublicAdItem,
|
||
PublicSponsorItem,
|
||
FriendLink,
|
||
} from "@/lib/api";
|
||
import Avatar from "@/components/Avatar";
|
||
import { AdText } from "@/components/AdText";
|
||
import { getPublicSettingsCached } from "@/lib/serverData";
|
||
import { announcementPath, postPath, userPath, pagePath } from "@/lib/urlStyle";
|
||
import { PulseCheckin } from "./CheckinCard";
|
||
import { formatCompact, formatShortDate } from "@/lib/format";
|
||
import { pageGlyph } from "@/lib/pageGlyph";
|
||
import { AboutSiteHeader } from "@/components/site-doc";
|
||
import {
|
||
AD_IMAGE_HEIGHT,
|
||
AD_IMAGE_SIZE_LABEL,
|
||
AD_IMAGE_WIDTH,
|
||
AD_TIP_DELAY_MS,
|
||
adHoverTip,
|
||
} from "@/lib/ads";
|
||
|
||
/* ---------- 站点数据(2×2 bento 统计瓦片) ---------- */
|
||
|
||
export function PulseCard({
|
||
stats,
|
||
checkin,
|
||
allowComments,
|
||
}: {
|
||
stats: OverviewStats;
|
||
checkin?: CheckinStatus | null;
|
||
allowComments: boolean;
|
||
}) {
|
||
const cells = [
|
||
{
|
||
icon: <Users size={12.5} />,
|
||
label: "注册用户",
|
||
value: formatCompact(stats.users),
|
||
foot: `+${formatCompact(stats.today_users)} 今日`,
|
||
up: true,
|
||
},
|
||
{
|
||
icon: <PenLine size={12.5} />,
|
||
label: "话题总数",
|
||
value: formatCompact(stats.posts),
|
||
foot: `+${formatCompact(stats.today_posts)} 今日`,
|
||
up: true,
|
||
},
|
||
...(allowComments
|
||
? [
|
||
{
|
||
icon: <MessageCircle size={12.5} />,
|
||
label: "回复总数",
|
||
value: formatCompact(stats.comments),
|
||
foot: `+${formatCompact(stats.today_comments)} 今日`,
|
||
up: true,
|
||
},
|
||
]
|
||
: [
|
||
{
|
||
icon: <TrendingUp size={12.5} />,
|
||
label: "今日新帖",
|
||
value: formatCompact(stats.today_posts),
|
||
foot: "本日新增",
|
||
up: stats.today_posts > 0,
|
||
},
|
||
]),
|
||
{
|
||
icon: <Activity size={12.5} />,
|
||
label: "在线用户",
|
||
value: formatCompact(stats.online),
|
||
foot: `峰值 ${formatCompact(stats.peak_online)}`,
|
||
up: false,
|
||
},
|
||
];
|
||
return (
|
||
<section className="j13-module" aria-label="站点数据">
|
||
<div className="flex items-center justify-between">
|
||
<p className="j13-module-title">
|
||
<BarChart3 size={15} style={{ color: "var(--accent)" }} /> 站点数据
|
||
</p>
|
||
<span
|
||
className="inline-flex items-center gap-1.5 text-[11px] font-normal normal-case tracking-normal"
|
||
style={{ color: "var(--ink-3)" }}
|
||
data-tip="近 5 分钟在线"
|
||
>
|
||
<span className="j13-pulse-dot" aria-hidden="true" />
|
||
实时
|
||
</span>
|
||
</div>
|
||
<div className="j13-pulse">
|
||
{cells.map((c) => (
|
||
<div key={c.label} className="j13-pulse-cell">
|
||
<p className="j13-pulse-lab">
|
||
<span style={{ color: "var(--accent)" }} aria-hidden="true">
|
||
{c.icon}
|
||
</span>
|
||
{c.label}
|
||
</p>
|
||
<p className="j13-pulse-num">{c.value}</p>
|
||
<p className={`j13-pulse-foot${c.up ? " is-up" : ""}`}>
|
||
{c.up && <TrendingUp size={10.5} aria-hidden="true" />}
|
||
{c.foot}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="j13-pulse-checkin">
|
||
<PulseCheckin initial={checkin} />
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------- 关于本站(侧栏页目录,置于右栏最底) ---------- */
|
||
|
||
export async function AboutSite({
|
||
pages,
|
||
siteDescription,
|
||
canManagePages,
|
||
}: {
|
||
pages: SidebarPageItem[];
|
||
siteDescription?: string;
|
||
canManagePages?: boolean;
|
||
}) {
|
||
if (pages.length === 0 && !canManagePages) return null;
|
||
const lead = siteDescription?.trim() ?? "";
|
||
const urlStyle = (await getPublicSettingsCached()).url_style;
|
||
|
||
return (
|
||
<section className="j13-module j13-about" aria-label="关于本站">
|
||
<AboutSiteHeader canManage={canManagePages} />
|
||
{lead ? <p className="j13-about-lead">{lead}</p> : null}
|
||
{pages.length === 0 ? (
|
||
<p className="meta mt-2 px-0.5">暂无介绍</p>
|
||
) : (
|
||
<ul className="j13-about-list">
|
||
{pages.map((p) => {
|
||
const Icon = pageGlyph(p.slug);
|
||
return (
|
||
<li key={p.id}>
|
||
<Link href={pagePath(urlStyle, p.slug)} className="j13-about-item">
|
||
<span className="j13-about-ico" aria-hidden="true">
|
||
<Icon size={15} strokeWidth={2} />
|
||
</span>
|
||
<span className="j13-about-body">
|
||
<span className="j13-about-name">{p.title}</span>
|
||
{p.excerpt ? <span className="j13-about-excerpt">{p.excerpt}</span> : null}
|
||
</span>
|
||
<ChevronRight className="j13-about-chev" size={15} aria-hidden="true" />
|
||
</Link>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------- 站点公告 ---------- */
|
||
|
||
export async function Announcements({
|
||
items,
|
||
canManageAnnouncements,
|
||
}: {
|
||
items: AnnouncementSummary[];
|
||
canManageAnnouncements?: boolean;
|
||
}) {
|
||
const urlStyle = (await getPublicSettingsCached()).url_style;
|
||
return (
|
||
<section className="j13-module" aria-label="站点公告">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<p className="j13-module-title !mb-0">
|
||
<Megaphone size={15} style={{ color: "var(--accent)" }} /> 站点公告
|
||
</p>
|
||
<span className="inline-flex items-center gap-2 shrink-0">
|
||
<Link
|
||
href="/announcements"
|
||
className="meta text-[12px] font-normal transition-colors hover:text-[var(--accent)]"
|
||
>
|
||
全部
|
||
</Link>
|
||
{canManageAnnouncements ? (
|
||
<Link
|
||
href="/admin/announcements"
|
||
className="meta text-[12px] font-normal transition-colors hover:text-[var(--accent)]"
|
||
>
|
||
管理
|
||
</Link>
|
||
) : null}
|
||
</span>
|
||
</div>
|
||
{items.length === 0 ? (
|
||
<p className="meta mt-2 px-0.5">暂无公告</p>
|
||
) : (
|
||
<ul className="j13-anno-list">
|
||
{items.map((a) => (
|
||
<li key={a.id}>
|
||
<Link
|
||
href={announcementPath(urlStyle, a.id)}
|
||
className="j13-anno-item"
|
||
data-tip={a.title}
|
||
>
|
||
<span className="j13-anno-item-body">
|
||
<span className="j13-anno-item-title truncate inline-flex items-center gap-1 min-w-0">
|
||
{a.pinned ? (
|
||
<Pin
|
||
size={11}
|
||
className="shrink-0"
|
||
style={{ color: "var(--accent)" }}
|
||
aria-label="置顶"
|
||
/>
|
||
) : null}
|
||
<span className="truncate">{a.title}</span>
|
||
</span>
|
||
<span className="j13-anno-item-meta">
|
||
{a.tag ? (
|
||
<span className={`j13-anno-tag is-${a.tag_color}`}>{a.tag}</span>
|
||
) : null}
|
||
<span className="meta text-[11px] tabular-nums">
|
||
{formatShortDate(a.created_at)}
|
||
</span>
|
||
</span>
|
||
</span>
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------- 热门榜 ---------- */
|
||
|
||
export async function HotRanking({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
|
||
const urlStyle = (await getPublicSettingsCached()).url_style;
|
||
return (
|
||
<section className="j13-module" aria-label="热门讨论">
|
||
<p className="j13-module-title mb-1.5">
|
||
<Flame size={15} style={{ color: "var(--clay)" }} /> 热门讨论
|
||
</p>
|
||
{posts.length === 0 ? (
|
||
<p className="meta px-1.5 py-3">还没有热门讨论</p>
|
||
) : (
|
||
<ol className="j13-ranklist">
|
||
{posts.map((p, i) => (
|
||
<li key={p.id}>
|
||
<Link href={postPath(urlStyle, p.id)} className="j13-rank" data-tip={p.title}>
|
||
<span className="j13-rank-no">{i + 1}</span>
|
||
<span className="min-w-0 flex-1">
|
||
<span className="j13-rank-title truncate">
|
||
{p.title}
|
||
</span>
|
||
<span className="mt-1 flex items-center gap-2.5 meta text-[11.5px]">
|
||
{allowComments ? (
|
||
<span className="inline-flex items-center gap-1">
|
||
<MessageCircle size={11.5} /> {p.comment_count}
|
||
</span>
|
||
) : null}
|
||
<span className="inline-flex items-center gap-1">
|
||
<Heart size={11.5} /> {p.like_count}
|
||
</span>
|
||
</span>
|
||
</span>
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------- 最新注册 ---------- */
|
||
|
||
export async function NewUsers({ users }: { users: NewUser[] }) {
|
||
if (users.length === 0) return null;
|
||
const urlStyle = (await getPublicSettingsCached()).url_style;
|
||
return (
|
||
<section className="j13-module" aria-label="最新注册">
|
||
<p className="j13-module-title mb-3">
|
||
<UserPlus size={15} style={{ color: "var(--accent)" }} /> 最新注册
|
||
</p>
|
||
<ul className="grid grid-cols-4 gap-y-3">
|
||
{users.map((u) => {
|
||
const name = u.nickname || u.username;
|
||
return (
|
||
<li key={u.id}>
|
||
<Link
|
||
href={userPath(urlStyle, u.id)}
|
||
className="flex flex-col items-center gap-1.5 rounded-lg px-1 py-1 transition-colors hover:bg-[var(--item-hover-bg)] focus-visible:bg-[var(--item-hover-bg)]"
|
||
>
|
||
<Avatar name={name} src={u.avatar} size={38} />
|
||
<span
|
||
className="w-full truncate text-center text-[11px]"
|
||
style={{ color: "var(--ink-2)" }}
|
||
data-tip={name}
|
||
>
|
||
{name}
|
||
</span>
|
||
</Link>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------- 活跃成员(本周发帖 / 回复) ---------- */
|
||
|
||
export async function ActiveUsers({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
|
||
const urlStyle = (await getPublicSettingsCached()).url_style;
|
||
return (
|
||
<section className="j13-module" aria-label="本周活跃成员">
|
||
<p className="j13-module-title mb-2">
|
||
<Users size={15} style={{ color: "var(--accent)" }} /> 活跃成员
|
||
<span className="ml-auto font-normal normal-case tracking-normal text-[11px]" style={{ color: "var(--ink-3)" }}>
|
||
本周
|
||
</span>
|
||
</p>
|
||
{users.length === 0 ? (
|
||
<p className="meta px-1.5 py-2">本周还没有人互动</p>
|
||
) : (
|
||
<ul className="space-y-0.5">
|
||
{users.map((u) => {
|
||
const name = u.nickname || u.username;
|
||
return (
|
||
<li key={u.id}>
|
||
<Link href={userPath(urlStyle, u.id)} className="j13-raillink" data-tip={name}>
|
||
<Avatar name={name} src={u.avatar} size={30} />
|
||
<span className="flex-1 min-w-0">
|
||
<span
|
||
className="block truncate text-[13px] font-medium leading-tight"
|
||
style={{ color: "var(--ink)" }}
|
||
>
|
||
{name}
|
||
</span>
|
||
<span className="mt-px flex items-center gap-2.5 text-[11px] meta">
|
||
<span className="inline-flex items-center gap-1">
|
||
<PenLine size={10.5} /> 发帖 {u.post_count}
|
||
</span>
|
||
{allowComments ? (
|
||
<span className="inline-flex items-center gap-1">
|
||
<MessageCircle size={10.5} /> 回复 {u.comment_count}
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
</span>
|
||
<span
|
||
className="shrink-0 text-[13px] tabular-nums"
|
||
style={{ color: "var(--accent)" }}
|
||
aria-label={
|
||
allowComments
|
||
? `本周共 ${u.post_count + u.comment_count} 次互动`
|
||
: `本周发帖 ${u.post_count}`
|
||
}
|
||
>
|
||
{allowComments ? u.post_count + u.comment_count : u.post_count}
|
||
</span>
|
||
</Link>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------- 友情链接 ---------- */
|
||
|
||
export function FriendLinks({ links }: { links: FriendLink[] }) {
|
||
// 块显隐由侧栏布局配置控制;列表为空时仍渲染(保留「申请」入口),仅展示占位提示
|
||
return (
|
||
<section className="j13-module" aria-label="友情链接">
|
||
<p className="j13-module-title mb-3">
|
||
<Link2 size={15} style={{ color: "var(--accent)" }} /> 友情链接
|
||
<span className="ml-auto inline-flex items-center gap-3 text-[12px] font-normal">
|
||
<Link href="/links" className="meta transition-colors hover:text-[var(--accent)]">
|
||
全部
|
||
</Link>
|
||
<Link
|
||
href="/links#apply"
|
||
className="meta transition-colors hover:text-[var(--accent)]"
|
||
>
|
||
申请
|
||
</Link>
|
||
</span>
|
||
</p>
|
||
{links.length === 0 ? (
|
||
<p className="meta text-[12.5px]">暂无友链,欢迎申请</p>
|
||
) : (
|
||
<div className="flex flex-wrap gap-2">
|
||
{links.map((l) => (
|
||
<a
|
||
key={l.id}
|
||
href={l.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="j13-chip inline-flex items-center gap-1.5"
|
||
title={l.description || l.name}
|
||
>
|
||
{l.logo_url ? (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
src={l.logo_url}
|
||
alt=""
|
||
width={14}
|
||
height={14}
|
||
className="object-contain"
|
||
style={{ width: 14, height: 14 }}
|
||
/>
|
||
) : null}
|
||
{l.name}
|
||
</a>
|
||
))}
|
||
</div>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------- 桌面 / 移动两种装配 ---------- */
|
||
|
||
type PanelsProps = {
|
||
stats: OverviewStats;
|
||
hot: PostListItem[];
|
||
activeUsers: ActiveUser[];
|
||
announcements: AnnouncementSummary[];
|
||
newUsers: NewUser[];
|
||
checkin?: CheckinStatus | null;
|
||
allowComments: boolean;
|
||
canManageAnnouncements?: boolean;
|
||
canManagePages?: boolean;
|
||
sidebarPages?: SidebarPageItem[];
|
||
siteDescription?: string;
|
||
ads?: PublicAdItem[];
|
||
adsPanelTitle?: string;
|
||
adsEnabled?: boolean;
|
||
sponsors?: PublicSponsorItem[];
|
||
sponsorsPanelTitle?: string;
|
||
sponsorsEnabled?: boolean;
|
||
friendLinks?: FriendLink[];
|
||
};
|
||
|
||
export function AdsPanel({
|
||
items,
|
||
title,
|
||
enabled,
|
||
}: {
|
||
items: PublicAdItem[];
|
||
title: string;
|
||
enabled: boolean;
|
||
}) {
|
||
if (!enabled) return null;
|
||
|
||
const IMAGE_SLOTS = 5;
|
||
const TEXT_SLOTS = 6;
|
||
const images = items.filter((a) => a.kind === "image").slice(0, IMAGE_SLOTS);
|
||
const texts = items.filter((a) => a.kind === "text").slice(0, TEXT_SLOTS);
|
||
const imageSlots = Array.from({ length: IMAGE_SLOTS }, (_, i) => images[i] ?? null);
|
||
const textSlots = Array.from({ length: TEXT_SLOTS }, (_, i) => texts[i] ?? null);
|
||
|
||
return (
|
||
<section className="j13-module" aria-label={title || "自助推广"}>
|
||
<p className="j13-module-title mb-3">
|
||
<CircleDollarSign size={15} style={{ color: "var(--accent)" }} />
|
||
{title || "自助推广"}
|
||
<Link href="/ads/buy" className="j13-ad-buy-btn ml-auto">
|
||
购买广告位
|
||
</Link>
|
||
</p>
|
||
|
||
<div className="j13-ad-promo">
|
||
<ul className="space-y-2" aria-label="图片广告位">
|
||
{imageSlots.map((ad, i) =>
|
||
ad ? (
|
||
<li key={`img-${ad.id}`}>
|
||
<a
|
||
href={ad.link_url}
|
||
target="_blank"
|
||
rel="noopener noreferrer sponsored"
|
||
className="j13-ad-image-link"
|
||
data-tip={adHoverTip(ad.ends_at)}
|
||
data-tip-delay={AD_TIP_DELAY_MS}
|
||
>
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img
|
||
src={ad.image_url}
|
||
alt="推广"
|
||
className="j13-ad-image"
|
||
width={AD_IMAGE_WIDTH}
|
||
height={AD_IMAGE_HEIGHT}
|
||
loading="lazy"
|
||
/>
|
||
</a>
|
||
</li>
|
||
) : (
|
||
<li key={`img-slot-${i}`}>
|
||
<Link href="/ads/buy" className="j13-ad-slot j13-ad-slot-image">
|
||
<span>图片广告位 {AD_IMAGE_SIZE_LABEL}</span>
|
||
</Link>
|
||
</li>
|
||
)
|
||
)}
|
||
</ul>
|
||
|
||
<ul className="j13-ad-text-grid" aria-label="文字广告位">
|
||
{textSlots.map((ad, i) =>
|
||
ad ? (
|
||
<li key={`txt-${ad.id}`}>
|
||
<AdText
|
||
title={ad.title ?? ""}
|
||
textColor={ad.text_color || "#1E293B"}
|
||
bgColor={ad.bg_color || "#F1F5F9"}
|
||
href={ad.link_url}
|
||
endsAt={ad.ends_at}
|
||
/>
|
||
</li>
|
||
) : (
|
||
<li key={`txt-slot-${i}`}>
|
||
<Link href="/ads/buy" className="j13-ad-slot j13-ad-slot-text">
|
||
文字广告位
|
||
</Link>
|
||
</li>
|
||
)
|
||
)}
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
export function SponsorsPanel({
|
||
items,
|
||
title,
|
||
enabled,
|
||
}: {
|
||
items: PublicSponsorItem[];
|
||
title: string;
|
||
enabled: boolean;
|
||
}) {
|
||
if (!enabled || items.length === 0) return null;
|
||
return (
|
||
<section className="j13-module" aria-label={title || "赞助商"}>
|
||
<p className="j13-module-title mb-3">
|
||
<Handshake size={15} style={{ color: "var(--accent)" }} />
|
||
{title || "赞助商"}
|
||
</p>
|
||
<ul className="j13-sponsors-grid">
|
||
{items.map((sp) => {
|
||
const tip = (sp.hover_text || sp.name || "").trim();
|
||
const href = sp.link_url?.trim() || undefined;
|
||
const inner = (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img src={sp.logo_url} alt={sp.name} className="j13-sponsor-logo" loading="lazy" />
|
||
);
|
||
return (
|
||
<li key={sp.id} className="j13-sponsor">
|
||
{href ? (
|
||
<a
|
||
href={href}
|
||
target="_blank"
|
||
rel="noopener noreferrer sponsored"
|
||
className="j13-sponsor-hit"
|
||
aria-label={sp.name}
|
||
data-tip={tip || undefined}
|
||
>
|
||
{inner}
|
||
</a>
|
||
) : (
|
||
<span
|
||
className="j13-sponsor-hit"
|
||
tabIndex={0}
|
||
aria-label={sp.name}
|
||
data-tip={tip || undefined}
|
||
>
|
||
{inner}
|
||
</span>
|
||
)}
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function CommunityPanelsBody({
|
||
stats,
|
||
hot,
|
||
activeUsers,
|
||
announcements,
|
||
newUsers,
|
||
checkin,
|
||
allowComments,
|
||
canManageAnnouncements,
|
||
canManagePages,
|
||
sidebarPages = [],
|
||
siteDescription,
|
||
ads = [],
|
||
adsPanelTitle = "自助推广",
|
||
adsEnabled = true,
|
||
sponsors = [],
|
||
sponsorsPanelTitle = "赞助商",
|
||
sponsorsEnabled = true,
|
||
friendLinks = [],
|
||
}: PanelsProps) {
|
||
return (
|
||
<>
|
||
<PulseCard stats={stats} checkin={checkin} allowComments={allowComments} />
|
||
<Announcements items={announcements} canManageAnnouncements={canManageAnnouncements} />
|
||
<AdsPanel items={ads} title={adsPanelTitle} enabled={adsEnabled} />
|
||
<SponsorsPanel items={sponsors} title={sponsorsPanelTitle} enabled={sponsorsEnabled} />
|
||
<HotRanking posts={hot} allowComments={allowComments} />
|
||
<NewUsers users={newUsers} />
|
||
<ActiveUsers users={activeUsers} allowComments={allowComments} />
|
||
<FriendLinks links={friendLinks} />
|
||
<AboutSite
|
||
pages={sidebarPages}
|
||
siteDescription={siteDescription}
|
||
canManagePages={canManagePages}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
|
||
export function CommunityPanels(props: PanelsProps) {
|
||
return (
|
||
<aside
|
||
className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]"
|
||
aria-label="社区面板"
|
||
>
|
||
<div className="space-y-4">
|
||
<CommunityPanelsBody {...props} />
|
||
</div>
|
||
</aside>
|
||
);
|
||
}
|
||
|
||
export function CommunityPanelsMobile(props: PanelsProps) {
|
||
return (
|
||
<div className="space-y-4 lg:hidden" aria-label="社区面板">
|
||
<CommunityPanelsBody {...props} />
|
||
</div>
|
||
);
|
||
}
|