update at 2026-02-06 16:28:58
This commit is contained in:
114
PLAN.md
114
PLAN.md
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user