# 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; ``` **特性**: - 不同错误类型显示不同图标和提示文案 - 提供"重试"和"移除"按钮 - 显示错误的工具名称和 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(); // 超时检测(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 { 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 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): void { const { text, position } = message.payload; // 调用 Plait 的文本插入 API // TODO: 实现文本插入逻辑 console.log('Insert text:', text, position); } /** * 处理插入图片请求 */ private handleInsertImage(message: ToolMessage): 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; ``` **表单字段**: - 工具名称(必填) - 工具 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 { const tools = await localforage.getItem( ToolboxService.STORAGE_KEY ); return tools || []; } /** * 保存自定义工具 */ async saveCustomTools(): Promise { await localforage.setItem( ToolboxService.STORAGE_KEY, this.customTools ); } /** * 添加自定义工具 */ async addCustomTool(tool: ToolDefinition): Promise { // 验证工具配置 this.validateToolDefinition(tool); // 添加到列表 this.customTools.push(tool); // 持久化 await this.saveCustomTools(); } /** * 删除自定义工具 */ async removeCustomTool(id: string): Promise { 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**: ✅ 计划完成,待审核