feat: 前端格式选择器组件,服务端支持能力透传

- 新增 FontSelector.tsx 独立组件,字体和输出格式并排选择
- /api/config 新增 supportedOutTypes 字段(LLRT 仅 ttf)
- 前端加载配置后自动适配可用格式

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-04-09 11:56:10 +08:00
parent 820fc71166
commit e2cc7c1144
5 changed files with 143 additions and 19 deletions

View File

@ -199,11 +199,14 @@ async function handleListFonts(req: Request, res: Response) {
/** GET /api/config — 返回公开配置 */
async function handleGetConfig(req: Request, res: Response) {
const isLlrt = release_name === "llrt";
return {
req,
res: jsonResponse({
enableTempUpload,
adminUploadEnabled: !!adminApiKey,
/** LLRT 不支持 wasm无法输出 woff2 */
supportedOutTypes: isLlrt ? ["ttf"] : ["woff2", "ttf"],
}),
};
}

View File

@ -1,7 +1,7 @@
{
"name": "webfont",
"private": true,
"version": "1.3.1",
"version": "1.3.2",
"type": "module",
"scripts": {
"dev": "pnpx tsx scripts/dev-all.ts",

View File

@ -1,6 +1,7 @@
import { createMemo, createSignal, onMount, Show, For } from "solid-js";
import { fetchFonts, fetchConfig, type FontInfo, type ServerConfig } from "./api";
import UploadSection from "./UploadSection";
import { SelectorRow } from "./FontSelector";
const s = {
wrap: {
@ -92,9 +93,11 @@ function App() {
const [text, set_text] = createSignal("天地无极,乾坤借法");
const [fonts, set_fonts] = createSignal<FontInfo[]>([]);
const [selectedFont, set_selectedFont] = createSignal("");
const [outType, set_outType] = createSignal<"woff2" | "ttf">("woff2");
const [serverConfig, set_serverConfig] = createSignal<ServerConfig>({
enableTempUpload: false,
adminUploadEnabled: false,
supportedOutTypes: ["woff2", "ttf"],
});
const SLOGAN = "如清风似闪电,超级快的字体子集化裁剪";
@ -103,6 +106,10 @@ function App() {
const [fontList, config] = await Promise.all([fetchFonts().catch(() => []), fetchConfig().catch(() => ({ enableTempUpload: false, adminUploadEnabled: false }))]);
set_fonts(fontList);
set_serverConfig(config);
/** 服务端不支持当前 outType 时自动回退 */
if (!config.supportedOutTypes?.includes(outType())) {
set_outType(config.supportedOutTypes?.[0] || "ttf");
}
if (fontList.length > 0) {
/** 标语随机使用一个字体展示 */
const randomFont = fontList[Math.floor(Math.random() * fontList.length)];
@ -123,10 +130,11 @@ function App() {
const cssStyle = createMemo(() => {
const font = selectedFont();
const ot = outType();
if (!font) return "";
return `@font-face {
font-family: "CustomFont";
src: url("${location.origin}/api?font=${font}&text=${encodeURIComponent(text())}&outType=woff2") format("woff2");
src: url("${location.origin}/api?font=${font}&text=${encodeURIComponent(text())}&outType=${ot}") format("${ot}");
}
.custom-font {
color: red;
@ -189,21 +197,14 @@ function App() {
<p id="slogan" style={s.desc}>{SLOGAN}</p>
<section style={s.section}>
<label style={s.label}></label>
<select
style={s.select}
value={selectedFont()}
onChange={(e) => onFontChange(e.target.value)}
>
<option value="">-- --</option>
<For each={fonts()}>
{(f) => (
<option value={f.name}>
{f.name} ({f.dir})
</option>
)}
</For>
</select>
<SelectorRow
fonts={fonts()}
selectedFont={selectedFont()}
onFontChange={onFontChange}
supportedOutTypes={serverConfig().supportedOutTypes || ["woff2", "ttf"]}
outType={outType()}
onOutTypeChange={set_outType}
/>
</section>
<section style={s.section}>
@ -228,9 +229,10 @@ function App() {
<button
style={{ ...s.btn, padding: "3px 12px", "font-size": "12px" }}
onClick={() => {
const ot = outType();
const a = document.createElement("a");
a.href = `/api?font=${selectedFont()}&text=${encodeURIComponent(text())}&outType=woff2`;
a.download = selectedFont().replace(/\.[^.]+$/, "") + "_subset.woff2";
a.href = `/api?font=${selectedFont()}&text=${encodeURIComponent(text())}&outType=${ot}`;
a.download = selectedFont().replace(/\.[^.]+$/, "") + `_subset.${ot}`;
a.click();
}}
>

118
src/FontSelector.tsx Normal file
View File

@ -0,0 +1,118 @@
import { For } from "solid-js";
interface FontSelectorProps {
fonts: Array<{ name: string; dir: string }>;
selectedFont: string;
onFontChange: (font: string) => void;
}
interface OutTypeSelectorProps {
supportedOutTypes: ("woff2" | "ttf")[];
outType: "woff2" | "ttf";
onOutTypeChange: (type: "woff2" | "ttf") => void;
}
const outTypeLabels: Record<string, string> = {
woff2: "WOFF2 体积更小",
ttf: "TTF 速度更快",
};
const outTypeDescs: Record<string, string> = {
woff2: "约压缩 50%,适合生产",
ttf: "无编码开销,适合开发",
};
const s = {
wrap: {
display: "flex",
gap: "12px",
} as const,
col: {
flex: 1,
} as const,
label: {
display: "block",
"font-size": "13px",
color: "#555",
"margin-bottom": "6px",
} as const,
select: {
width: "100%",
padding: "8px 12px",
"font-size": "14px",
border: "1px solid #d9d9d9",
"border-radius": "6px",
outline: "none",
"box-sizing": "border-box",
cursor: "pointer",
appearance: "none",
"background-image": "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E\")",
"background-repeat": "no-repeat",
"background-position": "right 10px center",
"padding-right": "28px",
} as const,
desc: {
"font-size": "11px",
color: "#bbb",
"margin-top": "4px",
} as const,
};
export function FontSelector(props: FontSelectorProps) {
return (
<div style={s.col}>
<label style={s.label}></label>
<select
style={s.select}
value={props.selectedFont}
onChange={(e) => props.onFontChange(e.target.value)}
>
<option value="">-- --</option>
<For each={props.fonts}>
{(f) => (
<option value={f.name}>
{f.name}
</option>
)}
</For>
</select>
</div>
);
}
export function OutTypeSelector(props: OutTypeSelectorProps) {
return (
<div style={{ width: "160px" }}>
<label style={s.label}></label>
<select
style={s.select}
value={props.outType}
onChange={(e) => props.onOutTypeChange(e.target.value as "woff2" | "ttf")}
>
<For each={props.supportedOutTypes}>
{(t) => (
<option value={t}>{outTypeLabels[t]}</option>
)}
</For>
</select>
<p style={s.desc}>{outTypeDescs[props.outType]}</p>
</div>
);
}
export function SelectorRow(props: FontSelectorProps & OutTypeSelectorProps) {
return (
<div style={s.wrap}>
<FontSelector
fonts={props.fonts}
selectedFont={props.selectedFont}
onFontChange={props.onFontChange}
/>
<OutTypeSelector
supportedOutTypes={props.supportedOutTypes}
outType={props.outType}
onOutTypeChange={props.onOutTypeChange}
/>
</div>
);
}

View File

@ -6,6 +6,7 @@ export interface FontInfo {
export interface ServerConfig {
enableTempUpload: boolean;
adminUploadEnabled: boolean;
supportedOutTypes: ("woff2" | "ttf")[];
}
export interface UploadResult {