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:
@@ -139,6 +139,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 后台:一条已保存字体的编辑框 */
|
||||
.font-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--su-space-3);
|
||||
padding: var(--su-space-4);
|
||||
border: 1px solid var(--su-border);
|
||||
border-radius: var(--su-radius-sm);
|
||||
}
|
||||
|
||||
/* 后台「首页 Logo」的实时预览:外观交给库里的 Brand,这里只管外框 */
|
||||
.brand-preview {
|
||||
display: flex;
|
||||
|
||||
@@ -333,7 +333,7 @@ export default function AppNext() {
|
||||
show_nav: true,
|
||||
},
|
||||
background: cachedBackground ?? { light: '', dark: '' },
|
||||
font: { url: '', family: '' },
|
||||
font: { source: 'builtin', current: '', items: [] },
|
||||
search: {
|
||||
default: 'internal',
|
||||
engines: DEFAULT_SEARCH_ENGINES.map((engine) => ({ ...engine })),
|
||||
@@ -409,15 +409,19 @@ export default function AppNext() {
|
||||
}
|
||||
}, [scheme])
|
||||
|
||||
// 正文字体:空链接用内置文楷;否则注入 @font-face(字体文件)或 CSS 链接,并覆盖字体令牌
|
||||
// 正文字体:内置文楷,或启用「字体链接」里存好的某一条。
|
||||
// 自定义时注入 @font-face(字体文件)或 CSS 链接,并覆盖字体令牌。
|
||||
useEffect(() => {
|
||||
const url = site.font.url.trim()
|
||||
const family = site.font.family.trim()
|
||||
const root = document.documentElement
|
||||
|
||||
document.getElementById('stash-font-link')?.remove()
|
||||
document.getElementById('stash-font-face')?.remove()
|
||||
|
||||
// current 指向的条目被删掉时退回第一条
|
||||
const active =
|
||||
site.font.items.find((item) => item.key === site.font.current) ?? site.font.items[0]
|
||||
const url = site.font.source === 'custom' && active !== undefined ? active.url.trim() : ''
|
||||
const family = active?.family.trim() ?? ''
|
||||
|
||||
if (url === '') {
|
||||
root.style.removeProperty('--su-font-sans')
|
||||
return
|
||||
@@ -442,7 +446,7 @@ export default function AppNext() {
|
||||
'--su-font-sans',
|
||||
`${JSON.stringify(familyName)}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Hiragino Sans GB', 'Microsoft YaHei', 'Source Han Sans CN', sans-serif`,
|
||||
)
|
||||
}, [site.font.url, site.font.family])
|
||||
}, [site.font])
|
||||
|
||||
/** 页头的明暗切换:只写本机偏好,不需要登录;站点默认值在后台改 */
|
||||
const toggleTheme = () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -30,13 +30,26 @@ export type BackgroundSettings = {
|
||||
dark: string
|
||||
}
|
||||
|
||||
export type FontSettings = {
|
||||
/** 字体文件或 CSS 链接;空表示用内置霞鹜文楷 */
|
||||
export type FontSource = 'builtin' | 'custom'
|
||||
|
||||
/** 一条存下来的字体配置:名字只用于后台下拉,url 才是真正加载的东西 */
|
||||
export type FontLink = {
|
||||
key: string
|
||||
name: string
|
||||
url: string
|
||||
/** 字体族名:CSS 链接时填 CSS 里声明的名字;字体文件留空则用 Stash Font */
|
||||
family: string
|
||||
}
|
||||
|
||||
export type FontSettings = {
|
||||
/** 字体来源:内置霞鹜文楷,或下面保存的字体链接 */
|
||||
source: FontSource
|
||||
/** 当前启用的 FontLink.key;为空时用第一条 */
|
||||
current: string
|
||||
/** 存下来的字体:常用字体放这里,切换时不用重新填 */
|
||||
items: FontLink[]
|
||||
}
|
||||
|
||||
const BACKGROUND_CACHE_KEY = 'stash-background'
|
||||
|
||||
/**
|
||||
@@ -203,7 +216,7 @@ export function defaultSettings(): SiteSettings {
|
||||
show_nav: true,
|
||||
},
|
||||
background: { light: '', dark: '' },
|
||||
font: { url: '', family: '' },
|
||||
font: { source: 'builtin', current: '', items: [] },
|
||||
search: {
|
||||
default: 'internal',
|
||||
engines: DEFAULT_SEARCH_ENGINES.map((engine) => ({ ...engine })),
|
||||
|
||||
Reference in New Issue
Block a user