feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Ellipsis, ExternalLink, FileText, Plus } from "lucide-react";
|
||||
import { apiAdminDeleteSitePage, type SitePage } from "@/lib/api";
|
||||
import { apiAdminDeleteSitePage, apiAdminPurgeSitePage, type SitePage } from "@/lib/api";
|
||||
import { formatDateTime, formatRelative } from "@/lib/format";
|
||||
import { sitePagePublicPath } from "@/lib/sitePagePath";
|
||||
import { useUrlStyle } from "@/components/SiteBrandProvider";
|
||||
@@ -61,8 +61,14 @@ export default function PagesAdmin({
|
||||
const [menuUp, setMenuUp] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const publishedCount = useMemo(() => list.filter((p) => p.published).length, [list]);
|
||||
const draftCount = useMemo(() => list.filter((p) => !p.published).length, [list]);
|
||||
const publishedCount = useMemo(
|
||||
() => list.filter((p) => p.published && !p.deleted).length,
|
||||
[list]
|
||||
);
|
||||
const draftCount = useMemo(
|
||||
() => list.filter((p) => !p.published && !p.deleted).length,
|
||||
[list]
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (statusFilter === "published") return list.filter((p) => p.published);
|
||||
@@ -93,9 +99,16 @@ export default function PagesAdmin({
|
||||
async function remove(p: SitePage) {
|
||||
setDeletingId(p.id);
|
||||
try {
|
||||
await apiAdminDeleteSitePage(p.id);
|
||||
setList((l) => l.filter((x) => x.id !== p.id));
|
||||
toast("页面已删除", "ok");
|
||||
if (p.deleted) {
|
||||
// 已软删的页面:彻底删除(不可恢复,释放 slug)
|
||||
await apiAdminPurgeSitePage(p.id);
|
||||
setList((l) => l.filter((x) => x.id !== p.id));
|
||||
toast("页面已彻底删除,地址可重新使用", "ok");
|
||||
} else {
|
||||
await apiAdminDeleteSitePage(p.id);
|
||||
setList((l) => l.map((x) => (x.id === p.id ? { ...x, deleted: true } : x)));
|
||||
toast("页面已删除,可在列表中彻底删除以释放地址", "ok");
|
||||
}
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "删除失败");
|
||||
} finally {
|
||||
@@ -170,20 +183,30 @@ export default function PagesAdmin({
|
||||
const updated = p.updated_at || "";
|
||||
const path = sitePagePublicPath(p.slug, urlStyle);
|
||||
return (
|
||||
<tr key={p.id}>
|
||||
<tr key={p.id} style={p.deleted ? { opacity: 0.65 } : undefined}>
|
||||
<td data-label="页面">
|
||||
<Link href={editHref(p.id, statusFilter)} className="admin-doc-title" title={p.title}>
|
||||
{p.title}
|
||||
</Link>
|
||||
{p.deleted ? (
|
||||
<span className="admin-doc-title" title={p.title}>
|
||||
{p.title}
|
||||
</span>
|
||||
) : (
|
||||
<Link href={editHref(p.id, statusFilter)} className="admin-doc-title" title={p.title}>
|
||||
{p.title}
|
||||
</Link>
|
||||
)}
|
||||
<p className="admin-doc-path">
|
||||
<code>{path}</code>
|
||||
</p>
|
||||
{p.excerpt ? <p className="admin-doc-excerpt">{p.excerpt}</p> : null}
|
||||
</td>
|
||||
<td data-label="状态">
|
||||
<AdminStatusChip tone={p.published ? "ok" : "warn"}>
|
||||
{p.published ? "已发布" : "草稿"}
|
||||
</AdminStatusChip>
|
||||
{p.deleted ? (
|
||||
<AdminStatusChip tone="danger">已删除</AdminStatusChip>
|
||||
) : (
|
||||
<AdminStatusChip tone={p.published ? "ok" : "warn"}>
|
||||
{p.published ? "已发布" : "草稿"}
|
||||
</AdminStatusChip>
|
||||
)}
|
||||
</td>
|
||||
<td data-label="展示位置">{placeLabel(p)}</td>
|
||||
<td data-label="排序" className="tabular-nums">
|
||||
@@ -200,10 +223,12 @@ export default function PagesAdmin({
|
||||
</td>
|
||||
<td data-label="操作">
|
||||
<div className="admin-doc-row-actions">
|
||||
<Link href={editHref(p.id, statusFilter)} className="admin-doc-textbtn">
|
||||
编辑
|
||||
</Link>
|
||||
{p.published ? (
|
||||
{!p.deleted ? (
|
||||
<Link href={editHref(p.id, statusFilter)} className="admin-doc-textbtn">
|
||||
编辑
|
||||
</Link>
|
||||
) : null}
|
||||
{p.published && !p.deleted ? (
|
||||
<a
|
||||
href={path}
|
||||
target="_blank"
|
||||
@@ -248,7 +273,7 @@ export default function PagesAdmin({
|
||||
setDeleteTarget(p);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
{p.deleted ? "彻底删除" : "删除"}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -277,15 +302,17 @@ export default function PagesAdmin({
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
title="删除单页"
|
||||
title={deleteTarget?.deleted ? "彻底删除单页" : "删除单页"}
|
||||
message={
|
||||
deleteTarget
|
||||
? `确定删除「${deleteTarget.title}」吗?当前为${deleteTarget.published ? "已发布" : "草稿"}。删除后前台不再显示,后台也不能恢复。`
|
||||
? deleteTarget.deleted
|
||||
? `确定彻底删除「${deleteTarget.title}」吗?删除后不可恢复,地址将释放可重新使用。`
|
||||
: `确定删除「${deleteTarget.title}」吗?当前为${deleteTarget.published ? "已发布" : "草稿"}。删除后前台不再显示,可在列表中彻底删除以释放地址。`
|
||||
: ""
|
||||
}
|
||||
danger
|
||||
busy={deletingId != null}
|
||||
confirmLabel="删除"
|
||||
confirmLabel={deleteTarget?.deleted ? "彻底删除" : "删除"}
|
||||
onCancel={() => deletingId == null && setDeleteTarget(null)}
|
||||
onConfirm={() => {
|
||||
if (deleteTarget) void remove(deleteTarget);
|
||||
|
||||
Reference in New Issue
Block a user