Files
TrueGrowth/specs/008-multifunctional-toolbox/PHASE3_PLAN.md

702 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**: ✅ 计划完成,待审核