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