feat: 可配置站点 URL 风格
- 新增 urlStyle 工具与测试:postPath/userPath/sitePagePath 统一生成链接 - 管理端新增 UrlStylePanel 设置面板,settings 服务与 handler 适配 - 全站路由链接与 JSON-LD/sitemap 切换为统一 URL 生成 - middleware 与相关组件适配
This commit is contained in:
@@ -6,6 +6,7 @@ import useUnsavedGuard from "@/hooks/useUnsavedGuard";
|
||||
import BasicIdentityPanel from "./BasicIdentityPanel";
|
||||
import BrandSeoPanel from "./BrandSeoPanel";
|
||||
import FooterLinksPanel from "./FooterLinksPanel";
|
||||
import UrlStylePanel from "./UrlStylePanel";
|
||||
import type { PublicSettings } from "@/lib/api";
|
||||
|
||||
export default function BasicSettings({ initial }: { initial: PublicSettings }) {
|
||||
@@ -13,17 +14,20 @@ export default function BasicSettings({ initial }: { initial: PublicSettings })
|
||||
const [basicDirty, setBasicDirty] = useState(false);
|
||||
const [brandDirty, setBrandDirty] = useState(false);
|
||||
const [seoDirty, setSeoDirty] = useState(false);
|
||||
const dirty = basicDirty || brandDirty || seoDirty;
|
||||
const [urlStyleDirty, setUrlStyleDirty] = useState(false);
|
||||
const dirty = basicDirty || brandDirty || seoDirty || urlStyleDirty;
|
||||
const leave = useUnsavedGuard(dirty);
|
||||
|
||||
const onBasicDirty = useCallback((d: boolean) => setBasicDirty(d), []);
|
||||
const onBrandDirty = useCallback((d: boolean) => setBrandDirty(d), []);
|
||||
const onSeoDirty = useCallback((d: boolean) => setSeoDirty(d), []);
|
||||
const onUrlStyleDirty = useCallback((d: boolean) => setUrlStyleDirty(d), []);
|
||||
|
||||
return (
|
||||
<div className="admin-settings-stack">
|
||||
<BasicIdentityPanel initial={live} onSaved={setLive} onDirtyChange={onBasicDirty} />
|
||||
<BrandSeoPanel initial={live} onSaved={setLive} onDirtyChange={onBrandDirty} />
|
||||
<UrlStylePanel initial={live} onSaved={setLive} onDirtyChange={onUrlStyleDirty} />
|
||||
<FooterLinksPanel initial={live} onSaved={setLive} onDirtyChange={onSeoDirty} />
|
||||
<ConfirmDialog
|
||||
open={leave.leaveOpen}
|
||||
|
||||
123
frontend/app/admin/settings/UrlStylePanel.tsx
Normal file
123
frontend/app/admin/settings/UrlStylePanel.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AdminSettingsActions, AdminSettingsGroup } from "@/components/admin";
|
||||
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
|
||||
import { announcementPath, leaderboardPath, 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) },
|
||||
];
|
||||
}
|
||||
|
||||
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>
|
||||
</ul>
|
||||
</AdminSettingsGroup>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user