mirror of
https://github.com/2234839/web-font.git
synced 2026-05-13 00:38:12 +08:00
222 lines
11 KiB
Vue
222 lines
11 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted } from "vue";
|
||
import { fetchFonts, fetchConfig } from "./api";
|
||
import type { FontInfo, ServerConfig } from "./api";
|
||
|
||
const isDev = import.meta.env.DEV;
|
||
const origin = location.origin;
|
||
import UploadSection from "./UploadSection.vue";
|
||
import StatsPanel from "./StatsPanel.vue";
|
||
import SelectorRow from "./FontSelector.vue";
|
||
import FontDebugPreview from "./FontDebugPreview.vue";
|
||
|
||
const SLOGAN = "如清风似闪电,超级快的字体子集化裁剪";
|
||
|
||
const text = ref("天地无极,乾坤借法");
|
||
const fonts = ref<FontInfo[]>([]);
|
||
const selectedFont = ref("");
|
||
const outType = ref<"woff2" | "ttf">("ttf");
|
||
const serverConfig = ref<ServerConfig>({
|
||
enableTempUpload: false,
|
||
adminUploadEnabled: false,
|
||
supportedOutTypes: ["woff2", "ttf"],
|
||
});
|
||
|
||
onMounted(async () => {
|
||
const [fontList, config] = await Promise.all([
|
||
fetchFonts().catch(() => [] as FontInfo[]),
|
||
fetchConfig().catch((): ServerConfig => ({ enableTempUpload: false, adminUploadEnabled: false })),
|
||
]);
|
||
fonts.value = fontList;
|
||
serverConfig.value = config;
|
||
|
||
if (!config.supportedOutTypes?.includes(outType.value)) {
|
||
outType.value = config.supportedOutTypes?.[0] || "ttf";
|
||
}
|
||
|
||
if (fontList.length > 0) {
|
||
const usableFonts = fontList.filter((f) => /\.(ttf)$/i.test(f.name));
|
||
const randomFont = usableFonts[Math.floor(Math.random() * usableFonts.length)];
|
||
(globalThis as any).WebFont?.loadText({
|
||
fontName: randomFont.name,
|
||
text: SLOGAN,
|
||
family: "SloganFont",
|
||
});
|
||
const sloganEl = document.getElementById("slogan");
|
||
if (sloganEl) {
|
||
sloganEl.style.fontFamily = '"SloganFont", sans-serif';
|
||
sloganEl.title = randomFont.name;
|
||
}
|
||
selectedFont.value = fontList[0].name;
|
||
}
|
||
});
|
||
|
||
const cssStyle = computed(() => {
|
||
const font = selectedFont.value;
|
||
const ot = outType.value;
|
||
if (!font) return "";
|
||
const formatStr = ot === "woff2" ? "woff2" : "truetype";
|
||
return `@font-face {
|
||
font-family: "CustomFont";
|
||
src: url("${origin}/api?font=${font}&text=${encodeURIComponent(text.value)}&outType=${ot}") format("${formatStr}");
|
||
}
|
||
.custom-font {
|
||
color: red;
|
||
font-family: "CustomFont";
|
||
}`;
|
||
});
|
||
|
||
let textLoader: { update: (text: string) => void; dispose: () => void } | null = null;
|
||
|
||
function onTextChange(value: string) {
|
||
text.value = value;
|
||
textLoader?.update(value);
|
||
}
|
||
|
||
const textareaRows = computed(() => {
|
||
const lines = text.value.split("\n").length;
|
||
return Math.max(2, Math.min(lines, 10));
|
||
});
|
||
|
||
let lastLoadKey = "";
|
||
|
||
function reloadFont(font: string, ot: "woff2" | "ttf") {
|
||
const key = `${font}|${ot}`;
|
||
if (!font || key === lastLoadKey) return;
|
||
lastLoadKey = key;
|
||
if (textLoader) textLoader.dispose();
|
||
textLoader = (globalThis as any).WebFont?.loadText({
|
||
fontName: font,
|
||
text: text.value,
|
||
family: "CustomFont",
|
||
outType: ot,
|
||
}) ?? null;
|
||
const el = document.getElementById("webfont-preview");
|
||
if (el) el.style.fontFamily = '"CustomFont", sans-serif';
|
||
}
|
||
|
||
function onFontChange(font: string) {
|
||
selectedFont.value = font;
|
||
}
|
||
|
||
watch([selectedFont, outType], ([font, ot]) => {
|
||
reloadFont(font, ot as "woff2" | "ttf");
|
||
});
|
||
|
||
async function refreshFonts() {
|
||
const fontList = await fetchFonts();
|
||
fonts.value = fontList;
|
||
if (fontList.length > 0 && !selectedFont.value) {
|
||
onFontChange(fontList[0].name);
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div style="max-width: 720px; margin: 0 auto; padding: 48px 24px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1a1a1a; line-height: 1.6">
|
||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||
<h1 style="font-size: 22px; font-weight: 600; margin: 0 0 4px 0">Web Font</h1>
|
||
<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 id="slogan" style="font-size: 24px; color: #888; margin: 0 0 36px 0">{{ SLOGAN }}</p>
|
||
|
||
<section style="margin-bottom: 28px">
|
||
<SelectorRow
|
||
:fonts="fonts"
|
||
:selectedFont="selectedFont"
|
||
:onFontChange="onFontChange"
|
||
:supportedOutTypes="serverConfig.supportedOutTypes || ['woff2', 'ttf']"
|
||
:outType="outType"
|
||
:onOutTypeChange="(v: 'woff2' | 'ttf') => outType = v"
|
||
/>
|
||
</section>
|
||
|
||
<section style="margin-bottom: 28px">
|
||
<label style="display: block; font-size: 13px; color: #555; margin-bottom: 6px">输入文本预览效果</label>
|
||
<textarea
|
||
id="webfont-preview"
|
||
:rows="textareaRows"
|
||
:value="text"
|
||
@input="onTextChange(($event.target as HTMLTextAreaElement).value)"
|
||
placeholder="在此输入文本..."
|
||
style="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"
|
||
/>
|
||
</section>
|
||
|
||
<section v-if="selectedFont" style="margin-bottom: 28px">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px">
|
||
<label style="display: block; font-size: 13px; color: #555; margin: 0">CSS 代码</label>
|
||
<div style="display: flex; gap: 6px">
|
||
<button
|
||
style="padding: 3px 12px; font-size: 12px; border: 1px solid #d9d9d9; border-radius: 6px; cursor: pointer; background: #fff; color: #333"
|
||
@click="() => {
|
||
const a = document.createElement('a');
|
||
a.href = `/api?font=${selectedFont}&text=${encodeURIComponent(text)}&outType=${outType}`;
|
||
a.download = selectedFont.replace(/\.[^.]+$/, '') + `_subset.${outType}`;
|
||
a.click();
|
||
}"
|
||
>
|
||
下载字体
|
||
</button>
|
||
<button
|
||
style="padding: 3px 12px; font-size: 12px; border: 1px solid #d9d9d9; border-radius: 6px; cursor: pointer; background: #fff; color: #333"
|
||
@click="async (e: MouseEvent) => {
|
||
const btn = e.currentTarget as HTMLButtonElement;
|
||
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="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">{{ cssStyle }}</pre>
|
||
</section>
|
||
|
||
<FontDebugPreview v-if="isDev" />
|
||
|
||
<UploadSection :config="serverConfig" :onUploaded="refreshFonts" />
|
||
|
||
<StatsPanel />
|
||
|
||
<section style="margin-bottom: 28px; font-size: 12px; color: #aaa; line-height: 1.8">
|
||
<p><b>原理:</b>服务端根据 text 参数裁剪字体,只返回所需字符的子集。相同 URL 的请求会被浏览器自动缓存。</p>
|
||
<p><b>基础用法:</b>将 CSS 复制到你的页面,修改 text 参数中的文字即可:</p>
|
||
<pre style="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-top: 4px">{{ `<style>\n@font-face {\n font-family: "MyFont";\n src: url("${origin}/api?font=字体名&text=你的文字") format("woff2");\n}\n.title { font-family: "MyFont"; }\n</style>\n<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="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-top: 4px">{{ `<script src="${origin}/webfont-sdk.js"></script>\n<script>\n WebFont.loadFont({\n fontName: "字体文件名.ttf",\n selector: ".my-element",\n family: "MyFont",\n interval: 1000,\n });\n</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>
|
||
<p style="margin-top: 8px">觉得好用?<a href="https://shenzilong.cn/%E5%85%B3%E4%BA%8E/%E8%B5%9E%E5%8A%A9.html#" target="_blank" rel="noopener noreferrer" style="color: #e6a700; text-decoration: underline">请作者喝杯咖啡</a>,支持持续开发</p>
|
||
</footer>
|
||
|
||
<a
|
||
href="https://shenzilong.cn/%E5%85%B3%E4%BA%8E/%E8%B5%9E%E5%8A%A9.html#"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style="position: fixed; right: 0; top: 50%; transform: translateY(-50%); background: #e6a700; color: #fff; padding: 12px 6px; font-size: 12px; writing-mode: vertical-rl; text-decoration: none; border-radius: 6px 0 0 6px; box-shadow: -2px 0 8px rgba(0,0,0,0.1); z-index: 999; transition: padding 0.2s"
|
||
onmouseover="this.style.paddingRight='10px'"
|
||
onmouseout="this.style.paddingRight='6px'"
|
||
>
|
||
赞助支持
|
||
</a>
|
||
</div>
|
||
</template>
|