From 9397e10aad0b6d2b7752bd0628c61e12008f07e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Thu, 30 Jul 2026 23:23:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AD=97=E4=BD=93=E5=88=97=E8=A1=A8/?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E6=8E=A5=E5=85=A5=20font-config=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=20+=20lint=20=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前端: - FontList.vue: 卡片使用 router-link 跳转; 展示 displayName/description/previewText/tags; 底部添加字体投稿联系文案 - FontDetail.vue: 预览文字使用 config.previewText 替代写死文案 工具: - scripts/lint-font-config.ts: 检查 previewText 字符是否被字体支持 - package.json: 添加 lint:font-config 脚本 --- package.json | 1 + scripts/lint-font-config.ts | 97 ++++++++++++++++++++++++++++++++++ src/pages/FontDetail.vue | 28 ++++++++-- src/pages/FontList.vue | 100 +++++++++++++++++++++++++++++------- 4 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 scripts/lint-font-config.ts diff --git a/package.json b/package.json index bf4d38c..7d59e70 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "serve:llrt": "./llrt ./dist_backend/app.cjs", "build": "vite-ssg build", "build_backend": "pnpx tsx scripts/build-backend.ts", + "lint:font-config": "pnpx tsx scripts/lint-font-config.ts", "docker_build": "docker build -t llej0/web-font:${npm_package_version} -t llej0/web-font:latest .", "docker_push": "docker push llej0/web-font:${npm_package_version} && docker push llej0/web-font:latest", "preview": "vite preview", diff --git a/scripts/lint-font-config.ts b/scripts/lint-font-config.ts new file mode 100644 index 0000000..9124c8f --- /dev/null +++ b/scripts/lint-font-config.ts @@ -0,0 +1,97 @@ +/** + * lint-font-config.ts + * + * 检查 font/font-config.json 中每个字体的 previewText 是否都被该字体实际包含。 + * 如果 previewText 中有字体不支持的字符,输出警告并标记失败。 + * + * 用法:pnpm tsx scripts/lint-font-config.ts + * + * 修改 font-config.json 的 previewText 字段后应运行此脚本验证。 + */ +import { readFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import { extractCodePoints } from "../backend/font_util/font_meta.ts"; + +interface FontUserConfig { + displayName?: string; + previewText?: string; +} + +type FontConfig = Record; + +const FONT_DIR = "font"; +const CONFIG_PATH = "font/font-config.json"; + +async function main() { + /** 读取配置 */ + const configRaw = await readFile(CONFIG_PATH, "utf-8"); + const config = JSON.parse(configRaw) as FontConfig; + + /** 扫描字体目录,建立 文件名→路径 映射 */ + const fontFiles = await readdir(FONT_DIR); + const fontPathMap = new Map(); + for (const f of fontFiles) { + if (/\.(ttf|otf|TTF|OTF)$/i.test(f)) { + fontPathMap.set(f, join(FONT_DIR, f)); + } + } + + let hasError = false; + + for (const [fileName, cfg] of Object.entries(config)) { + const previewText = cfg.previewText; + if (!previewText) { + /** 没配 previewText,跳过(用默认值,无法检查) */ + continue; + } + + const fontPath = fontPathMap.get(fileName); + if (!fontPath) { + console.warn(`⚠️ [${fileName}] 字体文件不存在,跳过`); + continue; + } + + /** 读取字体并提取 codepoints */ + const raw = await readFile(fontPath); + const fontBuffer = raw.buffer.slice( + raw.byteOffset, + raw.byteOffset + raw.byteLength, + ); + const supportedCps = extractCodePoints(fontBuffer); + + /** 逐字符检查 previewText */ + const chars = [...previewText]; + /** 空格/换行等空白字符不检查(字体通常都有,但即使没有也不影响预览) */ + 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; + if (missing.length > 0) { + hasError = true; + console.error( + `❌ [${displayName}] (${fileName}) previewText 含 ${missing.length} 个不支持的字符:${missing.join(" ")}`, + ); + console.error(` previewText: "${previewText}"`); + } else { + console.log(`✅ [${displayName}] (${fileName}) previewText 检查通过`); + } + } + + if (hasError) { + console.error("\n💔 存在不支持的字符,请修正 previewText"); + process.exit(1); + } else { + console.log("\n🎉 所有 previewText 检查通过"); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/pages/FontDetail.vue b/src/pages/FontDetail.vue index 55cc8ce..332f6c4 100644 --- a/src/pages/FontDetail.vue +++ b/src/pages/FontDetail.vue @@ -73,6 +73,27 @@ function splitSemicolon(text: string | undefined): string[] { .map((s) => s.trim()) .filter(Boolean); } + +/** 默认预览文案(font-config.json 未配 previewText 时使用) */ +const DEFAULT_PREVIEW = "静心茶舍以茶为媒观自在一叶知秋山间晨露未晞茶人已入林深处指尖轻捻择其嫩芽天地无极乾坤借法"; + +/** 当前字体的预览文案:优先用 config.previewText */ +const previewContent = computed(() => meta.value?.config?.previewText ?? DEFAULT_PREVIEW); + +/** 预览文字拆分:第一段做大标题,其余做副文本 */ +const previewLines = computed(() => { + const text = previewContent.value; + /** 按空格拆分,第一段做大字标题 */ + const parts = text.split(/\s+/).filter(Boolean); + return { + title: parts[0] ?? text, + subtitle: parts.slice(1).join(" ") || "", + full: text, + }; +}); + +/** 显示名:优先 config.displayName */ +const displayName = computed(() => meta.value?.config?.displayName ?? fontName.value);