mirror of
https://github.com/2234839/web-font.git
synced 2026-06-30 04:38:24 +08:00
281 lines
10 KiB
TypeScript
281 lines
10 KiB
TypeScript
import { createMemo, createSignal, onMount, Show, For } from "solid-js";
|
||
import { fetchFonts, fetchConfig, type FontInfo, type ServerConfig } from "./api";
|
||
import UploadSection from "./UploadSection";
|
||
|
||
const s = {
|
||
wrap: {
|
||
"max-width": "720px",
|
||
margin: "0 auto",
|
||
padding: "48px 24px",
|
||
"font-family": "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||
color: "#1a1a1a",
|
||
"line-height": "1.6",
|
||
} as const,
|
||
h1: {
|
||
"font-size": "22px",
|
||
"font-weight": 600,
|
||
margin: "0 0 4px 0",
|
||
} as const,
|
||
desc: {
|
||
"font-size": "14px",
|
||
color: "#888",
|
||
margin: "0 0 36px 0",
|
||
} as const,
|
||
label: {
|
||
display: "block",
|
||
"font-size": "13px",
|
||
color: "#555",
|
||
"margin-bottom": "6px",
|
||
} as const,
|
||
select: {
|
||
width: "100%",
|
||
padding: "8px 12px",
|
||
"font-size": "15px",
|
||
border: "1px solid #d9d9d9",
|
||
"border-radius": "6px",
|
||
outline: "none",
|
||
"box-sizing": "border-box",
|
||
} as const,
|
||
textarea: {
|
||
width: "100%",
|
||
padding: "8px 12px",
|
||
"font-size": "32px",
|
||
border: "1px solid #d9d9d9",
|
||
"border-radius": "6px",
|
||
resize: "none",
|
||
"box-sizing": "border-box",
|
||
outline: "none",
|
||
color: "#e74c3c",
|
||
"line-height": "1.4",
|
||
} as const,
|
||
pre: {
|
||
background: "#f7f7f8",
|
||
padding: "16px",
|
||
"border-radius": "6px",
|
||
"font-size": "13px",
|
||
"font-family": "'SF Mono', Menlo, Consolas, monospace",
|
||
overflow: "auto",
|
||
"white-space": "pre-wrap",
|
||
"word-break": "break-all",
|
||
"line-height": "1.5",
|
||
margin: "0",
|
||
} as const,
|
||
section: {
|
||
"margin-bottom": "28px",
|
||
} as const,
|
||
card: {
|
||
padding: "16px",
|
||
border: "1px solid #e8e8e8",
|
||
"border-radius": "8px",
|
||
"margin-bottom": "16px",
|
||
} as const,
|
||
btn: {
|
||
padding: "6px 20px",
|
||
"font-size": "14px",
|
||
border: "1px solid #d9d9d9",
|
||
"border-radius": "6px",
|
||
cursor: "pointer",
|
||
background: "#fff",
|
||
color: "#333",
|
||
} as const,
|
||
input: {
|
||
padding: "6px 12px",
|
||
"font-size": "14px",
|
||
border: "1px solid #d9d9d9",
|
||
"border-radius": "6px",
|
||
outline: "none",
|
||
"box-sizing": "border-box",
|
||
} as const,
|
||
};
|
||
|
||
function App() {
|
||
const [text, set_text] = createSignal("天地无极,乾坤借法");
|
||
const [fonts, set_fonts] = createSignal<FontInfo[]>([]);
|
||
const [selectedFont, set_selectedFont] = createSignal("");
|
||
const [serverConfig, set_serverConfig] = createSignal<ServerConfig>({
|
||
enableTempUpload: false,
|
||
adminUploadEnabled: false,
|
||
});
|
||
|
||
onMount(async () => {
|
||
const [fontList, config] = await Promise.all([fetchFonts().catch(() => []), fetchConfig().catch(() => ({ enableTempUpload: false, adminUploadEnabled: false }))]);
|
||
set_fonts(fontList);
|
||
set_serverConfig(config);
|
||
if (fontList.length > 0) {
|
||
onFontChange(fontList[0].name);
|
||
}
|
||
});
|
||
|
||
const cssStyle = createMemo(() => {
|
||
const font = selectedFont();
|
||
if (!font) return "";
|
||
return `@font-face {
|
||
font-family: "CustomFont";
|
||
src: url("${location.origin}/api?font=${font}&text=${encodeURIComponent(text())}") format("truetype");
|
||
}
|
||
.custom-font {
|
||
color: red;
|
||
font-family: "CustomFont";
|
||
}`;
|
||
});
|
||
|
||
/** loadText loader 引用,字体或文本变化时增量加载 */
|
||
let textLoader: { update: (text: string) => void; dispose: () => void } | null = null;
|
||
|
||
/** 文本变化时增量加载字体 */
|
||
const onTextChange = (value: string) => {
|
||
set_text(value);
|
||
if (!textLoader) return;
|
||
textLoader.update(value);
|
||
};
|
||
|
||
/** 根据文本行数动态计算 textarea 高度 */
|
||
const textareaRows = createMemo(() => {
|
||
const lines = text().split("\n").length;
|
||
return Math.max(2, Math.min(lines, 10));
|
||
});
|
||
|
||
/** 字体切换时为当前文本加载新字体 */
|
||
const onFontChange = (font: string) => {
|
||
set_selectedFont(font);
|
||
if (!font) return;
|
||
if (textLoader) textLoader.dispose();
|
||
textLoader = (globalThis as any).WebFont?.loadText({
|
||
fontName: font,
|
||
text: text(),
|
||
family: "CustomFont",
|
||
}) ?? null;
|
||
const el = document.getElementById("webfont-preview");
|
||
if (el) el.style.fontFamily = '"CustomFont", sans-serif';
|
||
};
|
||
|
||
async function refreshFonts() {
|
||
const fontList = await fetchFonts();
|
||
set_fonts(fontList);
|
||
if (fontList.length > 0 && !selectedFont()) {
|
||
onFontChange(fontList[0].name);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div style={s.wrap}>
|
||
<div style={{ display: "flex", "align-items": "center", "justify-content": "space-between" }}>
|
||
<h1 style={s.h1}>Web Font</h1>
|
||
<p style={{ "font-size": "13px", color: "#bbb", margin: "0" }}>如清风似闪电,超级快的字体子集化裁剪</p>
|
||
<a
|
||
href="https://github.com/2234839/web-font"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style={{ display: "inline-flex", "align-items": "center", "gap": "4px", "font-size": "13px", color: "#888", "text-decoration": "none", border: "1px solid #d9d9d9", "border-radius": "6px", padding: "4px 10px" }}
|
||
>
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
||
Star on GitHub
|
||
</a>
|
||
</div>
|
||
<p style={s.desc}>输入文本,获取仅包含所需字符的子集字体 CSS</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>
|
||
</section>
|
||
|
||
<section style={s.section}>
|
||
<label style={s.label}>输入文本预览效果</label>
|
||
<textarea
|
||
id="webfont-preview"
|
||
style={{
|
||
...s.textarea,
|
||
}}
|
||
rows={textareaRows()}
|
||
value={text()}
|
||
onInput={(e) => onTextChange(e.target.value)}
|
||
placeholder="在此输入文本..."
|
||
/>
|
||
</section>
|
||
|
||
<Show when={selectedFont()}>
|
||
<section style={s.section}>
|
||
<div style={{ display: "flex", "justify-content": "space-between", "align-items": "center", "margin-bottom": "6px" }}>
|
||
<label style={{ ...s.label, margin: "0" }}>CSS 代码</label>
|
||
<div style={{ display: "flex", gap: "6px" }}>
|
||
<button
|
||
style={{ ...s.btn, padding: "3px 12px", "font-size": "12px" }}
|
||
onClick={() => {
|
||
const a = document.createElement("a");
|
||
a.href = `/api?font=${selectedFont()}&text=${encodeURIComponent(text())}`;
|
||
a.download = selectedFont().replace(/\.[^.]+$/, "") + "_subset.ttf";
|
||
a.click();
|
||
}}
|
||
>
|
||
下载字体
|
||
</button>
|
||
<button
|
||
style={{ ...s.btn, padding: "3px 12px", "font-size": "12px" }}
|
||
onClick={async (e) => {
|
||
const btn = e.currentTarget;
|
||
try {
|
||
await navigator.clipboard.writeText(cssStyle());
|
||
btn.textContent = "已复制";
|
||
setTimeout(() => { btn.textContent = "复制 CSS"; }, 1500);
|
||
} catch {
|
||
btn.textContent = "复制失败";
|
||
setTimeout(() => { btn.textContent = "复制 CSS"; }, 1500);
|
||
}
|
||
}}
|
||
>
|
||
复制 CSS
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<pre style={s.pre}>{cssStyle()}</pre>
|
||
</section>
|
||
</Show>
|
||
|
||
<UploadSection config={serverConfig()} onUploaded={refreshFonts} />
|
||
|
||
<section style={{ ...s.section, "font-size": "12px", color: "#aaa", "line-height": "1.8" }}>
|
||
<p><b>原理:</b>服务端根据 text 参数裁剪字体,只返回所需字符的子集。相同 URL 的请求会被浏览器自动缓存。</p>
|
||
<p><b>基础用法:</b>将 CSS 复制到你的页面,修改 text 参数中的文字即可:</p>
|
||
<pre style={{ ...s.pre, "font-size": "12px", "margin-top": "4px" }}>{`<style>
|
||
@font-face {
|
||
font-family: "MyFont";
|
||
src: url("${location.origin}/api?font=字体名&text=你的文字") format("truetype");
|
||
}
|
||
.title { font-family: "MyFont"; }
|
||
</style>
|
||
<h1 class="title">你的文字</h1>`}</pre>
|
||
<p style={{ "margin-top": "12px" }}><b>JS SDK(推荐):</b>增量加载字体片段,按需请求,不会出现全量字体闪烁。<a href="/webfont-sdk.js" download="webfont-sdk.js">下载 SDK</a></p>
|
||
<pre style={{ ...s.pre, "font-size": "12px", "margin-top": "4px" }}>{`<script src="${location.origin}/webfont-sdk.js"><\/script>
|
||
<script>
|
||
WebFont.loadFont({
|
||
fontName: "字体文件名.ttf",
|
||
selector: ".my-element",
|
||
family: "MyFont",
|
||
interval: 1000,
|
||
});
|
||
<\/script>`}</pre>
|
||
<p style={{ "margin-top": "8px" }}>还支持 <code>WebFont.observeFont()</code>(MutationObserver 事件驱动)和 <code>WebFont.loadText()</code>(手动传文本)两种方式,多种方式可同时使用,SDK 内部自动按字体去重增量加载。</p>
|
||
</section>
|
||
|
||
<footer style={{ "margin-top": "48px", "padding-top": "16px", "border-top": "1px solid #eee", "font-size": "12px", color: "#999", "text-align": "center" }}>
|
||
<p>感谢 <a href="https://www.ruanyifeng.com/blog/2020/03/weekly-issue-100.html" target="_blank" rel="noopener noreferrer" style={{ color: "#999" }}>阮一峰科技爱好者周刊(第 100 期)</a> 收录本项目</p>
|
||
</footer>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default App;
|