feat(font): 字体分为内置/链接两种来源,链接可存多套并启用

- 设置改为 font.source(builtin|custom) + current + items[]
- 每条链接含 key/name/url/family;后台可加、改、删,选一条启用
- 前端按启用的那条注入 @font-face / CSS 链接,删除当前项时退回第一条
- 校验:条数上限 20、key 唯一、url 仅 http(s):// 或 /fonts/
This commit is contained in:
2026-11-05 18:54:58 +08:00
parent 0bb4cc354a
commit 97b9d00cf3
7 changed files with 232 additions and 67 deletions
+112 -36
View File
@@ -43,6 +43,8 @@ import {
saveSettings,
TONE_OPTIONS,
type BackgroundSettings,
type FontLink,
type FontSource,
type ProfileSettings,
type SearchSettings,
type SettingsDocument,
@@ -2309,7 +2311,29 @@ function SystemPanel({
setNext('')
}
const localFont = font.url.startsWith('/fonts/') ? font.url : ''
const activeKey = font.items.some((item) => item.key === font.current)
? font.current
: (font.items[0]?.key ?? '')
const update = (patch: Partial<SiteSettings['font']>) => onChangeFont({ ...font, ...patch })
const updateItem = (key: string, patch: Partial<FontLink>) =>
update({ items: font.items.map((item) => (item.key === key ? { ...item, ...patch } : item)) })
const removeItem = (key: string) => {
const items = font.items.filter((item) => item.key !== key)
onChangeFont({
...font,
items,
current: font.current === key ? (items[0]?.key ?? '') : font.current,
})
}
const addItem = (url: string, name: string) => {
const key = `font-${Date.now().toString(36)}-${font.items.length}`
onChangeFont({
...font,
source: 'custom',
items: [...font.items, { key, name, url, family: '' }],
current: font.current === '' ? key : font.current,
})
}
return (
<Space direction="column" gap="lg" align="stretch" className="admin-form">
@@ -2317,48 +2341,100 @@ function SystemPanel({
正文字体
</Text>
<Text size="sm" tone="muted">
说明:字体文件放进程序同级的 font/ 目录,这里会自动列出来;也可以直接填字体文件或字体 CSS
的链接。留空使用内置的霞鹜文楷。
说明:内置是霞鹜文楷。选「字体链接」后,可以在下面存几套常用字体(font/ 目录里的文件或外部
链接),需要哪个就启用哪个,不用每次重填。
</Text>
<FormField
label="font 目录里的字体"
htmlFor="font-file"
hint={fonts.length === 0 ? 'font/ 目录里还没有字体文件' : '选中的字体通过 /fonts/ 对外提供'}
>
<FormField label="字体来源" htmlFor="font-source">
<Select
options={[
{ value: '', label: '使用内置字体(霞鹜文楷)' },
...fonts.map((file) => ({ value: file.url, label: file.name })),
{ value: 'builtin', label: '内置(霞鹜文楷)' },
{ value: 'custom', label: '字体链接' },
]}
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 })}
value={font.source}
onChange={(source) => update({ source: source as FontSource })}
/>
</FormField>
{font.source === 'custom' && (
<>
<FormField
label="当前使用的字体"
htmlFor="font-current"
hint={font.items.length === 0 ? '还没有字体,先在下面添加一条' : '从已保存的字体里选一个启用'}
>
<Select
options={font.items.map((item) => ({
value: item.key,
label: item.name === '' ? item.key : item.name,
}))}
value={activeKey}
onChange={(current) => update({ current })}
/>
</FormField>
{font.items.map((item) => (
<div className="font-link" key={item.key}>
<div className="admin-row">
<FormField label="名称" htmlFor={`font-name-${item.key}`}>
<Input
id={`font-name-${item.key}`}
value={item.name}
placeholder="例如 观感字体"
onChange={(event) => updateItem(item.key, { name: event.target.value })}
/>
</FormField>
<FormField
label="字体族名"
htmlFor={`font-family-${item.key}`}
hint="CSS 链接时填 CSS 里声明的 font-family"
>
<Input
id={`font-family-${item.key}`}
value={item.family}
placeholder="字体文件可留空"
onChange={(event) => updateItem(item.key, { family: event.target.value })}
/>
</FormField>
</div>
<FormField
label="字体链接"
htmlFor={`font-url-${item.key}`}
hint="woff2 / woff / ttf / otf 文件,或字体 CSS 链接"
>
<Input
id={`font-url-${item.key}`}
value={item.url}
placeholder="/fonts/xxx.woff2 或 https://…"
onChange={(event) => updateItem(item.key, { url: event.target.value })}
/>
</FormField>
<Space gap="sm" justify="end">
<TextButton onClick={() => removeItem(item.key)}>删除这条</TextButton>
</Space>
</div>
))}
<Space gap="sm">
<Button variant="secondary" onClick={() => addItem('', '新字体')}>
添加字体
</Button>
{fonts.length > 0 && (
<Select
options={[
{ value: '', label: '从 font 目录添加…' },
...fonts.map((file) => ({ value: file.url, label: file.name })),
]}
value=""
onChange={(url) => {
const file = fonts.find((entry) => entry.url === url)
if (file !== undefined) addItem(file.url, file.name.replace(/\.[^.]+$/, ''))
}}
/>
)}
</Space>
</>
)}
<FormField label="当前密码" htmlFor="current-password">
<PasswordInput
id="current-password"