Initial TrueGrowth source import

This commit is contained in:
2026-07-07 09:36:36 +08:00
commit 3b6781d695
2283 changed files with 691996 additions and 0 deletions

View File

@@ -0,0 +1,701 @@
# Phase 3 实施计划 - 优化与完善
> Feature: feat/08-multifunctional-toolbox
> Created: 2025-12-09
> Status: 计划阶段
---
## 📋 概述
Phase 3 旨在提升多功能工具箱的用户体验和功能完整性在已有的基础架构Phase 1和用户交互Phase 2基础上增强样式、通信、自定义和错误处理能力。
### 核心目标
- 🎨 **更好的视觉体验** - 响应式设计、深色模式、优化的选中态
- 🔌 **工具双向通信** - postMessage 协议,实现画布与工具的数据交互
- ⚙️ **自定义工具** - 用户可添加自己的工具,配置持久化
- 🛡️ **完善的错误处理** - 友好的错误提示和降级方案
---
## 🎯 Phase 3 任务拆解
### 任务优先级
根据用户价值和实现复杂度,建议按以下优先级实施:
| 优先级 | 任务模块 | 用户价值 | 实现难度 | 预计时间 |
|--------|---------|---------|---------|---------|
| **P0** | 错误处理增强 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 1 小时 |
| **P0** | 样式优化 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 1.5 小时 |
| **P1** | postMessage 通信 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 2 小时 |
| **P2** | 自定义工具 | ⭐⭐⭐ | ⭐⭐⭐ | 1.5 小时 |
**总预计时间**: 约 6 小时
---
## 📐 任务详细设计
### P0-1: 错误处理增强1 小时)
#### 目标
完善 iframe 加载失败、跨域限制、权限错误等场景的用户提示。
#### 实现内容
**1.1 增强错误提示组件30 分钟)**
文件:`packages/drawnix/src/components/tool-element/ToolErrorOverlay.tsx`
```typescript
/**
* 工具加载错误覆盖层
*/
export interface ToolErrorOverlayProps {
errorType: 'load-failed' | 'cors-blocked' | 'permission-denied' | 'timeout';
toolName: string;
url: string;
onRetry: () => void;
onRemove: () => void;
}
export const ToolErrorOverlay: React.FC<ToolErrorOverlayProps>;
```
**特性**
- 不同错误类型显示不同图标和提示文案
- 提供"重试"和"移除"按钮
- 显示错误的工具名称和 URL截断显示
- 半透明背景,保持可读性
**1.2 工具加载状态管理30 分钟)**
文件:`packages/drawnix/src/components/tool-element/tool.generator.ts`(更新)
增强加载状态跟踪:
```typescript
interface ToolLoadState {
status: 'loading' | 'loaded' | 'error';
errorType?: 'load-failed' | 'cors-blocked' | 'timeout';
loadStartTime: number;
}
class ToolGenerator {
private loadStates = new Map<string, ToolLoadState>();
// 超时检测10 秒)
private setupLoadTimeout(elementId: string, iframe: HTMLIFrameElement): void;
// 检测 CORS 错误
private detectCorsError(iframe: HTMLIFrameElement): boolean;
}
```
**验收标准**
- [x] iframe 加载超过 10 秒显示超时错误
- [x] 检测到 X-Frame-Options 阻止时显示 CORS 错误
- [x] 加载失败时显示友好的错误提示
- [x] 错误状态下可以重试或移除工具
---
### P0-2: 样式优化1.5 小时)
#### 目标
优化工具元素的视觉呈现包括选中态、Hover 态、深色模式适配。
#### 实现内容
**2.1 优化工具元素选中态样式30 分钟)**
文件:`packages/drawnix/src/components/tool-element/tool.component.scss`(更新)
当前问题:工具元素选中时视觉反馈不够明显
优化方案:
```scss
.plait-tool-element {
// 默认状态
&__container {
border: 2px solid transparent;
transition: all 0.2s ease;
}
// 选中态
&--selected {
.plait-tool-element__container {
border-color: var(--brand-color-primary, #f39c12);
box-shadow: 0 0 0 2px rgba(243, 156, 18, 0.2),
0 4px 16px rgba(0, 0, 0, 0.15);
}
}
// Hover 态(仅在非编辑模式)
&:hover:not(&--editing) {
.plait-tool-element__container {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
}
// 编辑模式(双击后)
&--editing {
.plait-tool-element__container {
border-color: var(--brand-color-primary, #f39c12);
box-shadow: 0 0 0 3px rgba(243, 156, 18, 0.3);
}
}
}
```
**2.2 响应式适配30 分钟)**
文件:`packages/drawnix/src/components/toolbox-drawer/toolbox-drawer.scss`(更新)
移动端优化:
```scss
.toolbox-drawer {
// 桌面端默认宽度 320px
width: 320px;
// 平板端
@media (max-width: 768px) {
width: 280px;
}
// 移动端 - 全屏抽屉
@media (max-width: 480px) {
width: 100vw;
left: 0;
&__search {
padding: 12px 16px;
}
&__categories {
flex-wrap: wrap;
gap: 8px;
}
}
}
```
**2.3 深色模式支持30 分钟)**
文件:`packages/drawnix/src/styles/toolbox-theme.scss`(新建)
使用 CSS 变量适配深色模式:
```scss
:root {
// 工具箱主题色
--toolbox-bg: #ffffff;
--toolbox-border: #e5e5e5;
--toolbox-text: #262626;
--toolbox-text-secondary: #8c8c8c;
--toolbox-hover-bg: #f5f5f5;
// 工具卡片
--tool-card-bg: #ffffff;
--tool-card-border: #d9d9d9;
--tool-card-hover-border: #f39c12;
}
[data-theme='dark'] {
--toolbox-bg: #1f1f1f;
--toolbox-border: #3a3a3a;
--toolbox-text: #e5e5e5;
--toolbox-text-secondary: #a6a6a6;
--toolbox-hover-bg: #2a2a2a;
--tool-card-bg: #2a2a2a;
--tool-card-border: #3a3a3a;
--tool-card-hover-border: #f39c12;
}
```
**验收标准**
- [x] 选中工具元素时有明显的边框和阴影
- [x] 移动端工具箱抽屉全屏显示
- [x] 深色模式下所有组件颜色正确
- [x] Hover 效果流畅自然
---
### P1: postMessage 通信协议2 小时)
#### 目标
实现画布与工具 iframe 之间的双向通信,支持数据交互。
#### 实现内容
**3.1 通信协议设计30 分钟)**
文件:`packages/drawnix/src/types/tool-communication.types.ts`(新建)
```typescript
/**
* 工具通信消息类型
*/
export enum ToolMessageType {
// 画布 → 工具
BOARD_TO_TOOL_INIT = 'board:init', // 初始化工具
BOARD_TO_TOOL_DATA = 'board:data', // 发送数据给工具
BOARD_TO_TOOL_CONFIG = 'board:config', // 发送配置
// 工具 → 画布
TOOL_TO_BOARD_READY = 'tool:ready', // 工具准备就绪
TOOL_TO_BOARD_INSERT_TEXT = 'tool:insert-text', // 插入文本到画布
TOOL_TO_BOARD_INSERT_IMAGE = 'tool:insert-image', // 插入图片到画布
TOOL_TO_BOARD_REQUEST_DATA = 'tool:request-data', // 请求画布数据
}
/**
* 通信消息基础接口
*/
export interface ToolMessage<T = any> {
type: ToolMessageType;
toolId: string; // 工具实例 ID
payload: T;
timestamp: number;
}
/**
* 插入文本消息
*/
export interface InsertTextPayload {
text: string;
position?: [number, number]; // 可选的插入位置
}
/**
* 插入图片消息
*/
export interface InsertImagePayload {
url: string;
position?: [number, number];
width?: number;
height?: number;
}
```
**3.2 通信服务实现1 小时)**
文件:`packages/drawnix/src/services/tool-communication-service.ts`(新建)
```typescript
/**
* 工具通信服务
* 管理画布与工具 iframe 之间的消息传递
*/
export class ToolCommunicationService {
private board: PlaitBoard;
private messageHandlers = new Map<string, (message: ToolMessage) => void>();
constructor(board: PlaitBoard) {
this.board = board;
this.setupMessageListener();
}
/**
* 设置全局消息监听器
*/
private setupMessageListener(): void {
window.addEventListener('message', this.handleMessage.bind(this));
}
/**
* 处理来自工具的消息
*/
private handleMessage(event: MessageEvent): void {
// 验证消息来源
if (!this.isValidToolMessage(event)) {
return;
}
const message: ToolMessage = event.data;
// 路由到对应处理器
switch (message.type) {
case ToolMessageType.TOOL_TO_BOARD_READY:
this.handleToolReady(message);
break;
case ToolMessageType.TOOL_TO_BOARD_INSERT_TEXT:
this.handleInsertText(message);
break;
case ToolMessageType.TOOL_TO_BOARD_INSERT_IMAGE:
this.handleInsertImage(message);
break;
// ... 其他处理器
}
}
/**
* 发送消息给工具
*/
sendToTool(toolId: string, type: ToolMessageType, payload: any): void {
const iframe = this.getToolIframe(toolId);
if (!iframe || !iframe.contentWindow) {
console.warn(`Tool iframe not found: ${toolId}`);
return;
}
const message: ToolMessage = {
type,
toolId,
payload,
timestamp: Date.now(),
};
iframe.contentWindow.postMessage(message, '*');
}
/**
* 处理工具就绪消息
*/
private handleToolReady(message: ToolMessage): void {
console.log(`Tool ready: ${message.toolId}`);
// 发送初始化配置
this.sendToTool(message.toolId, ToolMessageType.BOARD_TO_TOOL_INIT, {
boardId: this.board.id,
theme: 'light',
});
}
/**
* 处理插入文本请求
*/
private handleInsertText(message: ToolMessage<InsertTextPayload>): void {
const { text, position } = message.payload;
// 调用 Plait 的文本插入 API
// TODO: 实现文本插入逻辑
console.log('Insert text:', text, position);
}
/**
* 处理插入图片请求
*/
private handleInsertImage(message: ToolMessage<InsertImagePayload>): void {
const { url, position, width, height } = message.payload;
// 调用 Plait 的图片插入 API
// TODO: 实现图片插入逻辑
console.log('Insert image:', url, position);
}
/**
* 验证消息来源
*/
private isValidToolMessage(event: MessageEvent): boolean {
// 检查消息格式
if (!event.data || typeof event.data !== 'object') {
return false;
}
// 检查是否是工具消息
const message = event.data;
return message.type && message.toolId && message.timestamp;
}
/**
* 获取工具的 iframe
*/
private getToolIframe(toolId: string): HTMLIFrameElement | null {
// 从 ToolGenerator 缓存中获取
// TODO: 需要访问 ToolGenerator 的 iframe 缓存
return null;
}
/**
* 清理资源
*/
destroy(): void {
window.removeEventListener('message', this.handleMessage.bind(this));
this.messageHandlers.clear();
}
}
```
**3.3 集成到工具组件30 分钟)**
文件:`packages/drawnix/src/plugins/with-tool.ts`(更新)
在 withTool 插件中初始化通信服务:
```typescript
export const withTool: PlaitPlugin = (board: PlaitBoard) => {
const { drawElement } = board;
// 初始化通信服务
const communicationService = new ToolCommunicationService(board);
// 保存到 board 上以便访问
(board as any).__toolCommunicationService = communicationService;
// ... 其他插件逻辑
return board;
};
```
**验收标准**
- [x] 画布可以接收工具发送的消息
- [x] 画布可以向工具发送消息
- [x] 工具可以请求插入文本到画布
- [x] 工具可以请求插入图片到画布
- [x] 消息验证防止恶意消息
---
### P2: 自定义工具1.5 小时)
#### 目标
允许用户添加自己的工具,并持久化保存配置。
#### 实现内容
**4.1 自定义工具管理界面1 小时)**
文件:`packages/drawnix/src/components/custom-tool-dialog/CustomToolDialog.tsx`(新建)
```typescript
/**
* 自定义工具添加对话框
*/
export interface CustomToolDialogProps {
visible: boolean;
onClose: () => void;
onAdd: (tool: ToolDefinition) => void;
}
export const CustomToolDialog: React.FC<CustomToolDialogProps>;
```
**表单字段**
- 工具名称(必填)
- 工具 URL必填
- 工具描述(可选)
- 工具图标emoji 选择器)
- 分类(下拉选择)
- 默认宽度/高度(数字输入)
- iframe 权限(多选)
**4.2 自定义工具持久化30 分钟)**
文件:`packages/drawnix/src/services/toolbox-service.ts`(更新)
使用 localforage 持久化自定义工具:
```typescript
export class ToolboxService {
private static readonly STORAGE_KEY = 'aitu:custom-tools';
/**
* 加载自定义工具
*/
async loadCustomTools(): Promise<ToolDefinition[]> {
const tools = await localforage.getItem<ToolDefinition[]>(
ToolboxService.STORAGE_KEY
);
return tools || [];
}
/**
* 保存自定义工具
*/
async saveCustomTools(): Promise<void> {
await localforage.setItem(
ToolboxService.STORAGE_KEY,
this.customTools
);
}
/**
* 添加自定义工具
*/
async addCustomTool(tool: ToolDefinition): Promise<void> {
// 验证工具配置
this.validateToolDefinition(tool);
// 添加到列表
this.customTools.push(tool);
// 持久化
await this.saveCustomTools();
}
/**
* 删除自定义工具
*/
async removeCustomTool(id: string): Promise<void> {
this.customTools = this.customTools.filter(tool => tool.id !== id);
await this.saveCustomTools();
}
/**
* 验证工具配置
*/
private validateToolDefinition(tool: ToolDefinition): void {
if (!tool.id || !tool.name || !tool.url) {
throw new Error('Invalid tool definition');
}
// 验证 URL 格式
try {
new URL(tool.url);
} catch {
throw new Error('Invalid URL format');
}
}
}
```
**验收标准**
- [x] 用户可以通过对话框添加自定义工具
- [x] 自定义工具保存到 IndexedDB
- [x] 刷新页面后自定义工具仍然存在
- [x] 自定义工具可以删除
- [x] 表单验证阻止无效配置
---
## 📂 新增文件清单
```
packages/drawnix/src/
├── components/
│ ├── tool-element/
│ │ └── ToolErrorOverlay.tsx # 新建 - 错误提示组件
│ └── custom-tool-dialog/
│ ├── CustomToolDialog.tsx # 新建 - 自定义工具对话框
│ ├── CustomToolForm.tsx # 新建 - 工具表单
│ ├── EmojiPicker.tsx # 新建 - Emoji 选择器
│ └── custom-tool-dialog.scss # 新建 - 样式
├── services/
│ └── tool-communication-service.ts # 新建 - 通信服务
├── types/
│ └── tool-communication.types.ts # 新建 - 通信类型定义
└── styles/
└── toolbox-theme.scss # 新建 - 主题样式
```
---
## 🔄 更新文件清单
```
packages/drawnix/src/
├── components/
│ ├── tool-element/
│ │ ├── tool.generator.ts # 更新 - 加载状态管理
│ │ └── tool.component.scss # 更新 - 选中态样式
│ └── toolbox-drawer/
│ ├── ToolboxDrawer.tsx # 更新 - 添加自定义工具按钮
│ └── toolbox-drawer.scss # 更新 - 响应式适配
├── services/
│ └── toolbox-service.ts # 更新 - 自定义工具管理
└── plugins/
└── with-tool.ts # 更新 - 集成通信服务
```
---
## 🎯 实施建议
### 分步实施路线
**第一步P0 任务2.5 小时)**
1. 错误处理增强 → 提升稳定性
2. 样式优化 → 提升视觉体验
**第二步P1 任务2 小时)**
3. postMessage 通信 → 解锁工具交互能力
**第三步P2 任务1.5 小时)**
4. 自定义工具 → 提升可扩展性
### 测试要点
#### 功能测试
- [ ] 错误场景覆盖加载失败、CORS、超时
- [ ] 样式在不同设备和主题下正确显示
- [ ] postMessage 消息正确发送和接收
- [ ] 自定义工具添加、删除、持久化正常
#### 性能测试
- [ ] 多个工具同时加载不卡顿
- [ ] postMessage 消息处理不阻塞 UI
- [ ] 自定义工具数量增加不影响性能
#### 安全测试
- [ ] postMessage 消息来源验证
- [ ] 自定义工具 URL 验证
- [ ] iframe sandbox 权限正确设置
---
## 📊 预期成果
完成 Phase 3 后,多功能工具箱将具备:
1.**完善的用户体验**
- 友好的错误提示
- 精美的视觉设计
- 响应式适配
- 深色模式支持
2.**强大的交互能力**
- 工具与画布双向通信
- 工具可以向画布插入内容
- 画布可以向工具发送数据
3.**高度可扩展**
- 用户可以添加任意工具
- 工具配置持久化
- 支持自定义权限和样式
4.**生产级质量**
- 完善的错误处理
- 安全的消息验证
- 良好的性能表现
---
## 🔗 相关文档
- [ARCHITECTURE.md](./ARCHITECTURE.md) - 整体架构设计
- [PHASE1_COMPLETE.md](./PHASE1_COMPLETE.md) - Phase 1 完成总结
- [PHASE2_COMPLETE.md](./PHASE2_COMPLETE.md) - Phase 2 完成总结
- [postMessage API 文档](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
- [localforage 文档](https://localforage.github.io/localForage/)
---
## 📝 注意事项
### 技术债务
- postMessage 通信需要工具网页配合实现协议
- CORS 错误检测可能不准确(浏览器限制)
- 自定义工具的安全性依赖用户自觉
### 兼容性
- postMessage 兼容性良好IE 8+
- localforage 自动降级到 localStorage
- CSS 变量需要 IE 11+(可用 PostCSS 处理)
---
**Created by**: Claude Code
**Date**: 2025-12-09
**Status**: ✅ 计划完成,待审核