Files
jiang13-bbs/frontend/app/admin/settings/UrlStylePanel.tsx
freefire 4f3f3a265c feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块:
- 友链(friendlink)后端处理 + 前端管理页与友链板
- 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot
- 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉
- 排行榜 service 测试、站点页面测试、时间线发布(timeline_release)

其它改动:
- 后端 handlers/services 全量小幅调整
- 前端组件、库函数、URL/品牌/站点 URL 工具更新
- Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
2026-09-30 16:30:07 +08:00

136 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { AdminSettingsActions, AdminSettingsGroup } from "@/components/admin";
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
import {
announcementPath,
leaderboardPath,
libraryDocPath,
libraryPath,
pagePath,
postPath,
userPath,
type UrlStyle,
} from "@/lib/urlStyle";
import { toast } from "@/lib/toast";
/** 三档风格展示信息:示例 id/slug 固定,方便站长对比形态 */
const STYLE_OPTIONS: { value: UrlStyle; label: string; desc: string }[] = [
{ value: "default", label: "标准动态", desc: "框架默认路由,带路径分段" },
{ value: "html", label: "静态页 .html", desc: "常见 CMS 伪静态形态" },
{ value: "discuz", label: "Discuz 风格", desc: "经典论坛 thread- 形态" },
];
function samplePaths(style: UrlStyle) {
return [
{ kind: "帖子", path: postPath(style, 123) },
{ kind: "用户", path: userPath(style, 5) },
{ kind: "公告", path: announcementPath(style, 1) },
{ kind: "自建页", path: pagePath(style, "about") },
{ kind: "排行榜", path: leaderboardPath(style) },
{ kind: "书库", path: libraryPath(style) },
{ kind: "书籍", path: libraryDocPath(style, "slug") },
];
}
export default function UrlStylePanel({
initial,
onSaved,
onDirtyChange,
}: {
initial: PublicSettings;
onSaved?: (next: PublicSettings) => void;
onDirtyChange?: (dirty: boolean) => void;
}) {
const [style, setStyle] = useState<UrlStyle>(initial.url_style);
const [savedStyle, setSavedStyle] = useState<UrlStyle>(initial.url_style);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const dirty = style !== savedStyle;
useEffect(() => {
onDirtyChange?.(dirty);
}, [dirty, onDirtyChange]);
const discard = () => {
setStyle(savedStyle);
setError("");
};
const save = async () => {
if (saving || !dirty) return;
setSaving(true);
setError("");
try {
const res = await apiUpdateSiteSettings({ url_style: style });
setSavedStyle(res.url_style);
setStyle(res.url_style);
onSaved?.(res);
toast("已保存,全站链接风格已切换", "ok");
} catch (e) {
const msg = e instanceof Error ? e.message : "保存失败";
setError(msg);
toast(msg);
} finally {
setSaving(false);
}
};
return (
<AdminSettingsGroup
id="settings-url-style"
title="链接风格(伪静态)"
description="帖子、用户主页、公告等详情页的 URL 形态。"
footer={
<AdminSettingsActions
dirty={dirty}
saving={saving}
saveLabel="保存"
error={error}
onSave={() => void save()}
onDiscard={discard}
/>
}
>
<div className="grid gap-3 sm:grid-cols-3" role="radiogroup" aria-label="链接风格">
{STYLE_OPTIONS.map((opt) => {
const on = style === opt.value;
return (
<button
key={opt.value}
type="button"
role="radio"
aria-checked={on}
className={`admin-url-style-card${on ? " is-on" : ""}`}
onClick={() => setStyle(opt.value)}
>
<span className="flex items-center justify-between gap-2">
<span className="text-[13.5px] font-semibold">{opt.label}</span>
<span className={`admin-url-style-dot${on ? " is-on" : ""}`} aria-hidden />
</span>
<span className="meta mt-0.5 block text-[11.5px]">{opt.desc}</span>
<span className="admin-url-style-examples">
{samplePaths(opt.value).map((s) => (
<span key={s.kind} className="admin-url-style-example">
<span className="meta">{s.kind}</span>
<code>{s.path}</code>
</span>
))}
</span>
</button>
);
})}
</div>
<ul className="admin-url-style-notes meta mt-3 text-[12px]">
<li>保存后全站前台链接立即按新风格生成;旧风格链接自动 301 到新风格,收录与外链不断链。</li>
<li>301 会被浏览器长期缓存,切换前请想清楚;建议长期固定一种风格,不要频繁来回切换。</li>
<li>自建页与排行榜同样参与伪静态;自建页 slug 含中文时自动 URL 编码,地址栏多数浏览器仍显示中文。</li>
<li>书库列表与书籍详情参与伪静态(两种伪静态风格同模板);阅读页保持标准路径不参与。</li>
</ul>
</AdminSettingsGroup>
);
}