mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 23:02:27 +08:00
feat: 详情页全面接入 font-config + 防闪烁
- font-config.json: 新增 bodyTitle/bodyText/charsetPreview 字段 为每个字体配置通过 lint 检查的专属文案 - FontDetail.vue: 正文/字符预览使用 config 字段替代写死文案 大标题/正文/字符预览加 v-if=meta 守卫,消除 hydrate 闪烁 h1 标题使用 displayName - lint-font-config.ts: 扩展检查范围至所有文本字段 - FontUserConfig 类型同步更新(后端+前端)
This commit is contained in:
parent
9397e10aad
commit
a095cd0590
@ -396,6 +396,12 @@ export interface FontUserConfig {
|
|||||||
homepage?: string;
|
homepage?: string;
|
||||||
/** 默认预览文字 */
|
/** 默认预览文字 */
|
||||||
previewText?: string;
|
previewText?: string;
|
||||||
|
/** 详情页正文标题 */
|
||||||
|
bodyTitle?: string;
|
||||||
|
/** 详情页正文段落 */
|
||||||
|
bodyText?: string;
|
||||||
|
/** 详情页字符预览行 */
|
||||||
|
charsetPreview?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 字体元数据结果 */
|
/** 字体元数据结果 */
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* lint-font-config.ts
|
* lint-font-config.ts
|
||||||
*
|
*
|
||||||
* 检查 font/font-config.json 中每个字体的 previewText 是否都被该字体实际包含。
|
* 检查 font/font-config.json 中每个字体的文本字段(previewText / bodyTitle / bodyText / charsetPreview)
|
||||||
* 如果 previewText 中有字体不支持的字符,输出警告并标记失败。
|
* 是否都被该字体实际包含。如果有不支持的字符,输出警告并标记失败。
|
||||||
*
|
*
|
||||||
* 用法:pnpm tsx scripts/lint-font-config.ts
|
* 用法:pnpm lint:font-config
|
||||||
*
|
*
|
||||||
* 修改 font-config.json 的 previewText 字段后应运行此脚本验证。
|
* 修改 font-config.json 中任何文本字段后应运行此脚本验证。
|
||||||
*/
|
*/
|
||||||
import { readFile, readdir } from "node:fs/promises";
|
import { readFile, readdir } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@ -15,8 +15,19 @@ import { extractCodePoints } from "../backend/font_util/font_meta.ts";
|
|||||||
interface FontUserConfig {
|
interface FontUserConfig {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
previewText?: string;
|
previewText?: string;
|
||||||
|
bodyTitle?: string;
|
||||||
|
bodyText?: string;
|
||||||
|
charsetPreview?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 需要检查的文本字段及显示名 */
|
||||||
|
const TEXT_FIELDS: Array<{ key: keyof FontUserConfig; label: string }> = [
|
||||||
|
{ key: "previewText", label: "previewText" },
|
||||||
|
{ key: "bodyTitle", label: "bodyTitle" },
|
||||||
|
{ key: "bodyText", label: "bodyText" },
|
||||||
|
{ key: "charsetPreview", label: "charsetPreview" },
|
||||||
|
];
|
||||||
|
|
||||||
type FontConfig = Record<string, FontUserConfig>;
|
type FontConfig = Record<string, FontUserConfig>;
|
||||||
|
|
||||||
const FONT_DIR = "font";
|
const FONT_DIR = "font";
|
||||||
@ -36,58 +47,69 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 字体 codepoints 缓存(同一字体只解析一次) */
|
||||||
|
const cpCache = new Map<string, Set<number>>();
|
||||||
|
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
||||||
for (const [fileName, cfg] of Object.entries(config)) {
|
for (const [fileName, cfg] of Object.entries(config)) {
|
||||||
const previewText = cfg.previewText;
|
|
||||||
if (!previewText) {
|
|
||||||
/** 没配 previewText,跳过(用默认值,无法检查) */
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fontPath = fontPathMap.get(fileName);
|
const fontPath = fontPathMap.get(fileName);
|
||||||
if (!fontPath) {
|
if (!fontPath) {
|
||||||
console.warn(`⚠️ [${fileName}] 字体文件不存在,跳过`);
|
console.warn(`⚠️ [${fileName}] 字体文件不存在,跳过`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 读取字体并提取 codepoints */
|
/** 读取字体 codepoints(有缓存则复用) */
|
||||||
const raw = await readFile(fontPath);
|
let supportedCps = cpCache.get(fileName);
|
||||||
const fontBuffer = raw.buffer.slice(
|
if (!supportedCps) {
|
||||||
raw.byteOffset,
|
const raw = await readFile(fontPath);
|
||||||
raw.byteOffset + raw.byteLength,
|
const fontBuffer = raw.buffer.slice(
|
||||||
);
|
raw.byteOffset,
|
||||||
const supportedCps = extractCodePoints(fontBuffer);
|
raw.byteOffset + raw.byteLength,
|
||||||
|
);
|
||||||
/** 逐字符检查 previewText */
|
supportedCps = extractCodePoints(fontBuffer);
|
||||||
const chars = [...previewText];
|
cpCache.set(fileName, supportedCps);
|
||||||
/** 空格/换行等空白字符不检查(字体通常都有,但即使没有也不影响预览) */
|
|
||||||
const missing: string[] = [];
|
|
||||||
for (const ch of chars) {
|
|
||||||
if (/\s/.test(ch)) continue;
|
|
||||||
const cp = ch.codePointAt(0)!;
|
|
||||||
if (!supportedCps.has(cp)) {
|
|
||||||
missing.push(ch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = cfg.displayName ?? fileName;
|
const displayName = cfg.displayName ?? fileName;
|
||||||
if (missing.length > 0) {
|
let fontHasError = false;
|
||||||
hasError = true;
|
|
||||||
console.error(
|
/** 逐字段检查 */
|
||||||
`❌ [${displayName}] (${fileName}) previewText 含 ${missing.length} 个不支持的字符:${missing.join(" ")}`,
|
for (const { key, label } of TEXT_FIELDS) {
|
||||||
);
|
const text = cfg[key];
|
||||||
console.error(` previewText: "${previewText}"`);
|
if (!text) continue;
|
||||||
} else {
|
|
||||||
console.log(`✅ [${displayName}] (${fileName}) previewText 检查通过`);
|
const chars = [...text];
|
||||||
|
const missing: string[] = [];
|
||||||
|
for (const ch of chars) {
|
||||||
|
/** 空白字符不检查 */
|
||||||
|
if (/\s/.test(ch)) continue;
|
||||||
|
const cp = ch.codePointAt(0)!;
|
||||||
|
if (!supportedCps!.has(cp)) {
|
||||||
|
missing.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
fontHasError = true;
|
||||||
|
hasError = true;
|
||||||
|
console.error(
|
||||||
|
`❌ [${displayName}] ${label} 含 ${missing.length} 个不支持的字符:${missing.join(" ")}`,
|
||||||
|
);
|
||||||
|
console.error(` ${label}: "${text}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fontHasError) {
|
||||||
|
console.log(`✅ [${displayName}] (${fileName}) 所有文本字段检查通过`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
console.error("\n💔 存在不支持的字符,请修正 previewText");
|
console.error("\n💔 存在不支持的字符,请修正相关文本字段");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else {
|
} else {
|
||||||
console.log("\n🎉 所有 previewText 检查通过");
|
console.log("\n🎉 所有文本字段检查通过");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,12 @@ export interface FontUserConfig {
|
|||||||
homepage?: string;
|
homepage?: string;
|
||||||
/** 默认预览文字 */
|
/** 默认预览文字 */
|
||||||
previewText?: string;
|
previewText?: string;
|
||||||
|
/** 详情页正文标题 */
|
||||||
|
bodyTitle?: string;
|
||||||
|
/** 详情页正文段落 */
|
||||||
|
bodyText?: string;
|
||||||
|
/** 详情页字符预览行 */
|
||||||
|
charsetPreview?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 字体元数据 */
|
/** 字体元数据 */
|
||||||
|
|||||||
@ -92,8 +92,14 @@ const previewLines = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 显示名:优先 config.displayName */
|
/** 正文标题:优先 config.bodyTitle */
|
||||||
const displayName = computed(() => meta.value?.config?.displayName ?? fontName.value);
|
const bodyTitle = computed(() => meta.value?.config?.bodyTitle ?? "清风明月");
|
||||||
|
|
||||||
|
/** 正文段落:优先 config.bodyText */
|
||||||
|
const bodyText = computed(() => meta.value?.config?.bodyText ?? "山中有桂树,常伴青云飞。");
|
||||||
|
|
||||||
|
/** 字符预览行:优先 config.charsetPreview */
|
||||||
|
const charsetPreview = computed(() => meta.value?.config?.charsetPreview ?? "天地玄黄宇宙洪荒:0123456789 ABCDEF");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -148,7 +154,7 @@ const displayName = computed(() => meta.value?.config?.displayName ?? fontName.v
|
|||||||
:data-font="fontName"
|
:data-font="fontName"
|
||||||
style="font-size: 36px; font-weight: 700; color: #2c2c2c; margin: 0; line-height: 1.3"
|
style="font-size: 36px; font-weight: 700; color: #2c2c2c; margin: 0; line-height: 1.3"
|
||||||
>
|
>
|
||||||
{{ fontName }}
|
{{ meta?.config?.displayName ?? fontName }}
|
||||||
</h1>
|
</h1>
|
||||||
<p v-if="notFound" style="font-size: 14px; color: #e74c3c; margin: 12px 0 0">
|
<p v-if="notFound" style="font-size: 14px; color: #e74c3c; margin: 12px 0 0">
|
||||||
⚠ 该字体不存在或已被删除
|
⚠ 该字体不存在或已被删除
|
||||||
@ -162,6 +168,7 @@ const displayName = computed(() => meta.value?.config?.displayName ?? fontName.v
|
|||||||
<div style="max-width: 800px; margin: 0 auto; padding: 0 24px 80px">
|
<div style="max-width: 800px; margin: 0 auto; padding: 0 24px 80px">
|
||||||
<!-- 大标题预览 -->
|
<!-- 大标题预览 -->
|
||||||
<div
|
<div
|
||||||
|
v-if="meta"
|
||||||
style="
|
style="
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -198,6 +205,7 @@ const displayName = computed(() => meta.value?.config?.displayName ?? fontName.v
|
|||||||
|
|
||||||
<!-- 正文预览 -->
|
<!-- 正文预览 -->
|
||||||
<div
|
<div
|
||||||
|
v-if="meta"
|
||||||
style="
|
style="
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -215,7 +223,7 @@ const displayName = computed(() => meta.value?.config?.displayName ?? fontName.v
|
|||||||
color: '#3a3a3a',
|
color: '#3a3a3a',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
一叶知秋
|
{{ bodyTitle }}
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
:style="{
|
:style="{
|
||||||
@ -227,12 +235,13 @@ const displayName = computed(() => meta.value?.config?.displayName ?? fontName.v
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
山间晨露未晞,茶人已入林深处。指尖轻捻,择其嫩芽一二,置于竹篮之中。此乃一年之始,亦是一叶与万物的初遇。
|
{{ bodyText }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 字符集预览 -->
|
<!-- 字符集预览 -->
|
||||||
<div
|
<div
|
||||||
|
v-if="meta"
|
||||||
style="
|
style="
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -253,7 +262,7 @@ const displayName = computed(() => meta.value?.config?.displayName ?? fontName.v
|
|||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
天地无极乾坤借法:0123456789 ABCDEF
|
{{ charsetPreview }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user