feat(font): 系统管理里可设置正文字体(font 目录或链接)

- 新增 GET /api/fonts 列出二进制同级 font/ 目录里的字体文件,/fonts/<文件名> 对外提供(只认一级文件名,拒绝穿越)
- 站点设置新增 font.url / font.family,校验只接受 http(s):// 或 /fonts/ 开头的链接
- 前台按设置注入 @font-face 或 CSS 链接并覆盖 --su-font-sans;留空回到内置文楷
- 后台「系统管理」新增字体选择 + 链接 + 族名;撤销/保存按钮覆盖该页
- 撤掉上一轮的霞鹜新晰黑改动
This commit is contained in:
2026-11-05 18:12:29 +08:00
parent e243a20b69
commit 0bb4cc354a
13 changed files with 316 additions and 15 deletions
+70 -4
View File
@@ -67,7 +67,7 @@ import {
} from '../../manage'
import { createItem, deleteItem, fetchItem, saveItem } from '../../items'
import { useProjects } from '../../useProjects'
import type { Project } from '../../api'
import { fetchFonts, type FontFile, type Project } from '../../api'
import siteBgLight from '../../assets/site-bg.jpg'
import siteBgDark from '../../assets/site-bg-dark.jpg'
@@ -411,12 +411,18 @@ export function AdminNext() {
/>
)}
{tab === 'system' && <SystemPanel />}
{tab === 'system' && (
<SystemPanel
font={draft.font}
onChangeFont={(font) => setDraft({ ...draft, font })}
/>
)}
{(tab === 'home-header' ||
tab === 'home-background' ||
tab === 'home-search' ||
tab === 'profile') && (
tab === 'profile' ||
tab === 'system') && (
<Space gap="sm" justify="end" className="admin-actions">
<Button
variant="secondary"
@@ -2271,11 +2277,23 @@ function ProfilePanel({
)
}
function SystemPanel() {
function SystemPanel({
font,
onChangeFont,
}: {
font: SiteSettings['font']
onChangeFont: (font: SiteSettings['font']) => void
}) {
const [current, setCurrent] = useState('')
const [next, setNext] = useState('')
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState<{ tone: 'info' | 'error'; text: string } | null>(null)
const [fonts, setFonts] = useState<FontFile[]>([])
// 字体列表来自服务端 font/ 目录;目录空时下拉只有「内置」
useEffect(() => {
void fetchFonts().then(setFonts)
}, [])
const change = async () => {
setBusy(true)
@@ -2291,8 +2309,56 @@ function SystemPanel() {
setNext('')
}
const localFont = font.url.startsWith('/fonts/') ? font.url : ''
return (
<Space direction="column" gap="lg" align="stretch" className="admin-form">
<Text size="sm" strong>
正文字体
</Text>
<Text size="sm" tone="muted">
说明:字体文件放进程序同级的 font/ 目录,这里会自动列出来;也可以直接填字体文件或字体 CSS
的链接。留空使用内置的霞鹜文楷。
</Text>
<FormField
label="font 目录里的字体"
htmlFor="font-file"
hint={fonts.length === 0 ? 'font/ 目录里还没有字体文件' : '选中的字体通过 /fonts/ 对外提供'}
>
<Select
options={[
{ value: '', label: '使用内置字体(霞鹜文楷)' },
...fonts.map((file) => ({ value: file.url, label: file.name })),
]}
value={localFont}
onChange={(url) => onChangeFont({ ...font, url })}
/>
</FormField>
<FormField
label="或:字体链接"
htmlFor="font-url"
hint="woff2 / woff / ttf / otf 文件,或字体 CSS 链接;填了以这里为准"
>
<Input
id="font-url"
value={font.url}
placeholder="https://… 或 /fonts/xxx.woff2"
onChange={(event) => onChangeFont({ ...font, url: event.target.value })}
/>
</FormField>
<FormField
label="字体族名"
htmlFor="font-family"
hint="链接到 CSS 时填 CSS 里声明的 font-family;字体文件可留空"
>
<Input
id="font-family"
value={font.family}
placeholder="例如 LXGW Neo XiHei"
onChange={(event) => onChangeFont({ ...font, family: event.target.value })}
/>
</FormField>
<FormField label="当前密码" htmlFor="current-password">
<PasswordInput
id="current-password"