update at 2026-02-06 16:28:58

This commit is contained in:
douboer
2026-02-06 16:28:58 +08:00
parent a1c70fd3f0
commit 2d18aa5137

114
PLAN.md
View File

@@ -4,6 +4,59 @@
基于 Figma 设计实现一个交互式字体转 SVG 工具的 Web 应用。采用纯前端架构,使用 TypeScript + Vue3 + Vite 技术栈,在浏览器中通过 opentype.js 和 harfbuzz.js (WASM) 实现字体到 SVG 的实时转换。 基于 Figma 设计实现一个交互式字体转 SVG 工具的 Web 应用。采用纯前端架构,使用 TypeScript + Vue3 + Vite 技术栈,在浏览器中通过 opentype.js 和 harfbuzz.js (WASM) 实现字体到 SVG 的实时转换。
**设计来源**: [Figma 设计稿](https://www.figma.com/design/S7WVUzg3Z0DMWjYUC6dJzN/font2svg?node-id=3-5&m=dev)
## 📋 Figma Annotations 功能需求清单
以下所有功能需求来自 Figma 设计中各组件的 annotation 标注:
### 1. 文本输入与预览控制 (节点 8:94, 18:146)
-**输入框**: "此处输入内容"
-**触发方式**: 输入内容后,**回车**或者点击**"预览"按钮**生效
-**预览按钮** (节点 18:157): 点击预览
### 2. 字体大小调整 (节点 7:2720)
-**滑块控件**: 文字预览大小调整
-**实时更新**: 调整时,效果预览窗口文字大小**动态变化**
### 3. 导出功能 (节点 23:77)
-**导出按钮**: 导出选中的预览文字的 svg 图
-**保存位置**: 弹出保存框,默认保存在 **~/Download**
-**批量导出**: 选中多个字体时批量导出
### 4. 字体选择区 (节点 21:185)
-**数据源**: 从 `font/` 目录读取所有字体
-**树状结构**: 字体按**目录树状分组**
-**展开/收拢**: 支持展开和收拢
-**单选**: 字体支持单个选择
-**批量框选**: 支持**鼠标批量框选**Shift/Ctrl 多选)
### 5. 收藏功能
-**未收藏图标** (节点 21:200): 点击收藏该字体。**无填充**表示当前未收藏
-**已收藏图标** (节点 21:206, 21:241): 点击取消收藏该字体。**红色填充**表示当前已收藏
-**预览勾选** (节点 21:205, I21:205): 勾选表示在**效果预览窗口中展示**
### 6. 已收藏字体区 (节点 8:75)
-**展示方式**: 已收藏字体展示,字体按目录树状分组,支持展开和收拢
-**操作方式**: 字体支持单个选择,或者鼠标批量框选
-**取消收藏**: 点击可取消收藏
-**动态分类** (节点 21:258): 如该分类下没有选中字体,该分类删除
### 7. 效果预览区 (节点 8:95)
-**预览内容**: 预览窗口,内容、大小依据设置
-**字体依据**: 字体依据字体选择勾选
-**多字体对比**: 如勾选 2 个字体,显示内容的 2 个字体预览
### 8. 预览项细节 (节点 8:130-8:135)
-**字体名称** (节点 8:134): 预览字体名,取**文件名**
-**预览窗口** (节点 8:135): 某一字体预览窗口
-**选中交互**: 点击任意区域选中或取消选中
-**导出标记**: 选中的可以导出
-**选择框** (节点 I23:55): 选中/未选中
### 9. 版权说明 (节点 5:15)
- ✅ 底部显示:@版权说明所有字体来源互联网分享仅供效果预览不做下载传播如有侵权请告知douboer@gmail.com
## ⚠️ 核心技术决策 ## ⚠️ 核心技术决策
**字体到 SVG 转换实现方式TypeScript 重新实现** **字体到 SVG 转换实现方式TypeScript 重新实现**
@@ -415,6 +468,23 @@ export async function downloadMultipleSvgs(
**Figma 设计参考**: 节点 3:5 **Figma 设计参考**: 节点 3:5
#### 3.1 状态管理 #### 3.1 状态管理
**⚠️ 重要性能策略:按需加载字体**
如果 `font/` 目录有 100+ 个字体文件(总计 1-2GB**绝对不能**在初始化时全部加载到内存。
**正确的加载策略**
1. ✅ **初始化时**只加载字体元数据JSON~10KB
- 字体名称、路径、分类
- 不加载字体文件本身
2. ✅ **按需加载**:用户选择字体时才加载该字体文件
- 点击预览 → 加载字体 → 生成 SVG
3. ✅ **内存缓存**:已加载的字体保存在内存中
- 避免重复加载同一字体
4. ✅ **可选优化**LRU 策略限制缓存数量
- 最多缓存 20 个字体(~200-400MB
- 超出后清除最久未使用的字体
```typescript ```typescript
// src/stores/fontStore.ts // src/stores/fontStore.ts
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
@@ -425,7 +495,7 @@ export interface FontInfo {
name: string; name: string;
category: string; category: string;
path: string; path: string;
font?: Font; font?: Font; // ⚠️ 注意:初始时为 undefined按需加载
} }
export const useFontStore = defineStore('font', { export const useFontStore = defineStore('font', {
@@ -440,7 +510,8 @@ export const useFontStore = defineStore('font', {
actions: { actions: {
async loadFontList() { async loadFontList() {
// 从 fonts.json 加载字体元数据 // ⚠️ 只加载元数据 JSON不加载字体文件
// 这个 JSON 只有 ~10KB包含 100+ 字体的基本信息
const response = await fetch('/fonts.json'); const response = await fetch('/fonts.json');
this.fontList = await response.json(); this.fontList = await response.json();
}, },
@@ -449,8 +520,10 @@ export const useFontStore = defineStore('font', {
const fontInfo = this.fontList.find(f => f.id === fontId); const fontInfo = this.fontList.find(f => f.id === fontId);
if (!fontInfo) return; if (!fontInfo) return;
// ⚠️ 关键:只有当字体未加载时才加载
if (!fontInfo.font) { if (!fontInfo.font) {
const { loadFont } = await import('@/utils/font-loader'); const { loadFont } = await import('@/utils/font-loader');
// 这里才真正加载 10-20MB 的字体文件
fontInfo.font = await loadFont(fontInfo.path); fontInfo.font = await loadFont(fontInfo.path);
} }
@@ -517,6 +590,9 @@ export function useFavorites() {
``` ```
#### 3.2 主布局组件 #### 3.2 主布局组件
**参考**: Figma 节点 3:5 主界面布局 + annotations
```vue ```vue
<!-- src/App.vue --> <!-- src/App.vue -->
<template> <template>
@@ -529,12 +605,15 @@ export function useFavorites() {
</div> </div>
<!-- 字号调整滑块 --> <!-- 字号调整滑块 -->
<!-- Figma 节点 7:2720: 调整时,效果预览窗口文字大小动态变化 -->
<LetterSpacingSlider v-model="fontSize" /> <LetterSpacingSlider v-model="fontSize" />
<!-- 文本输入 --> <!-- 文本输入 -->
<TextInput v-model="text" @preview="handlePreview" /> <!-- Figma 节点 18:146: 输入内容后,回车或点击"预览"按钮生效 -->
<TextInput v-model="text" @preview="handlePreview" @keyup.enter="handlePreview" />
<!-- 预览按钮 --> <!-- 预览按钮 -->
<!-- Figma 节点 18:157: 点击预览 -->
<button <button
@click="handlePreview" @click="handlePreview"
class="bg-purple-600 text-white px-4 py-2 rounded-lg h-22" class="bg-purple-600 text-white px-4 py-2 rounded-lg h-22"
@@ -543,6 +622,7 @@ export function useFavorites() {
</button> </button>
<!-- 导出按钮 --> <!-- 导出按钮 -->
<!-- Figma 节点 23:77: 导出选中的预览文字的svg图弹出保存框默认保存在~/Download -->
<button <button
@click="handleExport" @click="handleExport"
class="bg-blue-800 text-white px-4 py-2 rounded-lg h-22" class="bg-blue-800 text-white px-4 py-2 rounded-lg h-22"
@@ -555,17 +635,22 @@ export function useFavorites() {
<main class="flex-1 flex gap-2 min-h-0"> <main class="flex-1 flex gap-2 min-h-0">
<!-- 左侧:字体选择和收藏 --> <!-- 左侧:字体选择和收藏 -->
<aside class="flex flex-col gap-2 w-92"> <aside class="flex flex-col gap-2 w-92">
<!-- Figma 节点 21:185: 从font目录读取所有字体字体按目录树状分组支持展开和收拢。字体支持单个选择或者鼠标批量框选。 -->
<FontSelector /> <FontSelector />
<!-- Figma 节点 8:75: 已收藏字体展示,字体按目录树状分组,支持展开和收拢。字体支持单个选择,或者鼠标批量框选。点击可取消收藏。 -->
<FavoritesList /> <FavoritesList />
</aside> </aside>
<!-- 右侧:预览区 --> <!-- 右侧:预览区 -->
<!-- Figma 节点 8:95: 预览窗口内容、大小依据设置字体依据字体选择勾选。如勾选2个字体显示内容的2个字体预览。 -->
<section class="flex-1 border border-red-100 rounded-4 p-2"> <section class="flex-1 border border-red-100 rounded-4 p-2">
<SvgPreview /> <SvgPreview />
</section> </section>
</main> </main>
<!-- 底部版权 --> <!-- 底部版权 -->
<!-- Figma 节点 5:15 -->
<footer class="text-gray-400 text-sm"> <footer class="text-gray-400 text-sm">
@版权说明所有字体来源互联网分享仅供效果预览不做下载传播如有侵权请告知douboer@gmail.com @版权说明所有字体来源互联网分享仅供效果预览不做下载传播如有侵权请告知douboer@gmail.com
</footer> </footer>
@@ -655,11 +740,15 @@ function handleToggleSelection(fontId: string) {
``` ```
#### 3.4 字体树组件(支持展开/收拢) #### 3.4 字体树组件(支持展开/收拢)
**参考**: Figma 节点 21:188-21:226支持批量框选Shift/Ctrl
```vue ```vue
<!-- src/components/FontTree.vue --> <!-- src/components/FontTree.vue -->
<template> <template>
<div class="mb-4"> <div class="mb-4">
<!-- 分类标题 --> <!-- 分类标题 -->
<!-- Figma: 支持展开和收拢 -->
<div class="flex items-center gap-2 mb-2 cursor-pointer" @click="toggleExpand"> <div class="flex items-center gap-2 mb-2 cursor-pointer" @click="toggleExpand">
<img <img
:src="isExpanded ? iconExpanded : iconCollapsed" :src="isExpanded ? iconExpanded : iconCollapsed"
@@ -682,6 +771,7 @@ function handleToggleSelection(fontId: string) {
<span class="flex-1 text-gray-400 text-3">{{ font.name }}</span> <span class="flex-1 text-gray-400 text-3">{{ font.name }}</span>
<!-- 选择框 --> <!-- 选择框 -->
<!-- Figma 节点 21:205: 勾选表示在效果预览窗口中展示 -->
<label class="flex items-center"> <label class="flex items-center">
<input <input
type="checkbox" type="checkbox"
@@ -692,10 +782,12 @@ function handleToggleSelection(fontId: string) {
</label> </label>
<!-- 收藏按钮 --> <!-- 收藏按钮 -->
<!-- Figma 节点 21:200/21:206: 无填充=未收藏,红色填充=已收藏 -->
<button @click="$emit('toggle-favorite', font.id)"> <button @click="$emit('toggle-favorite', font.id)">
<img <img
:src="isFavorite(font.id) ? iconHeartFilled : iconHeart" :src="isFavorite(font.id) ? iconHeartFilled : iconHeart"
class="w-4 h-4" class="w-4 h-4"
:title="isFavorite(font.id) ? '取消收藏' : '收藏字体'"
/> />
</button> </button>
</div> </div>
@@ -773,9 +865,11 @@ const fontStore = useFontStore();
<template> <template>
<div class="border-b border-gray-200 pb-2"> <div class="border-b border-gray-200 pb-2">
<!-- 字体名称和选择框 --> <!-- 字体名称和选择框 -->
<!-- Figma 节点 8:134: 预览字体名,取文件名 -->
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<img src="@/assets/icons/file.svg" class="w-3 h-3" /> <img src="@/assets/icons/file.svg" class="w-3 h-3" />
<span class="flex-1 text-gray-400 text-3">{{ fontInfo?.name }}</span> <span class="flex-1 text-gray-400 text-3">{{ fontInfo?.name }}</span>
<!-- Figma 节点 I23:55: 选中/未选中,选中的可以导出 -->
<label> <label>
<input <input
v-model="isSelectedForExport" v-model="isSelectedForExport"
@@ -786,7 +880,11 @@ const fontStore = useFontStore();
</div> </div>
<!-- SVG 预览 --> <!-- SVG 预览 -->
<div class="bg-white p-2 min-h-40 flex items-center justify-center"> <!-- Figma 节点 8:135: 某一字体预览窗口。点击任意区域选中或取消选中 -->
<div
class="bg-white p-2 min-h-40 flex items-center justify-center cursor-pointer"
@click="isSelectedForExport = !isSelectedForExport"
>
<div v-if="isLoading" class="text-gray-400">加载中...</div> <div v-if="isLoading" class="text-gray-400">加载中...</div>
<div v-else-if="error" class="text-red-500">{{ error }}</div> <div v-else-if="error" class="text-red-500">{{ error }}</div>
<div v-else-if="svgContent" v-html="svgContent" /> <div v-else-if="svgContent" v-html="svgContent" />
@@ -820,7 +918,8 @@ const fontInfo = computed(() => {
const debouncedGenerate = debounce(async () => { const debouncedGenerate = debounce(async () => {
if (!fontInfo.value) return; if (!fontInfo.value) return;
// 加载字体 // ⚠️ 按需加载:用户点击预览时才加载字体文件
// 如果字体已加载fontInfo.font 存在),直接使用缓存
await fontStore.loadFont(props.fontId); await fontStore.loadFont(props.fontId);
if (fontStore.currentFont) { if (fontStore.currentFont) {
@@ -848,6 +947,8 @@ watch(
- ✅ 多字体预览正常显示 - ✅ 多字体预览正常显示
- ✅ 输入防抖生效300ms - ✅ 输入防抖生效300ms
- ✅ 响应式布局适配不同屏幕 - ✅ 响应式布局适配不同屏幕
- ✅ **关键**100+ 字体初始化时不卡顿(只加载元数据)
- ✅ **关键**:字体按需加载,已加载字体缓存生效
--- ---
@@ -856,6 +957,9 @@ watch(
**目标**: 配置字体文件的加载和优化 **目标**: 配置字体文件的加载和优化
#### 4.1 字体元数据生成 #### 4.1 字体元数据生成
**⚠️ 重要**:这个脚本生成的 JSON 文件是唯一在初始化时加载的数据,必须保持轻量(~10KB
```python ```python
# scripts/scan-fonts.py # scripts/scan-fonts.py
import os import os