17 KiB
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
/**
* 工具加载错误覆盖层
*/
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(更新)
增强加载状态跟踪:
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;
}
验收标准:
- iframe 加载超过 10 秒显示超时错误
- 检测到 X-Frame-Options 阻止时显示 CORS 错误
- 加载失败时显示友好的错误提示
- 错误状态下可以重试或移除工具
P0-2: 样式优化(1.5 小时)
目标
优化工具元素的视觉呈现,包括选中态、Hover 态、深色模式适配。
实现内容
2.1 优化工具元素选中态样式(30 分钟)
文件:packages/drawnix/src/components/tool-element/tool.component.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(更新)
移动端优化:
.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 变量适配深色模式:
: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;
}
验收标准:
- 选中工具元素时有明显的边框和阴影
- 移动端工具箱抽屉全屏显示
- 深色模式下所有组件颜色正确
- Hover 效果流畅自然
P1: postMessage 通信协议(2 小时)
目标
实现画布与工具 iframe 之间的双向通信,支持数据交互。
实现内容
3.1 通信协议设计(30 分钟)
文件:packages/drawnix/src/types/tool-communication.types.ts(新建)
/**
* 工具通信消息类型
*/
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(新建)
/**
* 工具通信服务
* 管理画布与工具 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 插件中初始化通信服务:
export const withTool: PlaitPlugin = (board: PlaitBoard) => {
const { drawElement } = board;
// 初始化通信服务
const communicationService = new ToolCommunicationService(board);
// 保存到 board 上以便访问
(board as any).__toolCommunicationService = communicationService;
// ... 其他插件逻辑
return board;
};
验收标准:
- 画布可以接收工具发送的消息
- 画布可以向工具发送消息
- 工具可以请求插入文本到画布
- 工具可以请求插入图片到画布
- 消息验证防止恶意消息
P2: 自定义工具(1.5 小时)
目标
允许用户添加自己的工具,并持久化保存配置。
实现内容
4.1 自定义工具管理界面(1 小时)
文件:packages/drawnix/src/components/custom-tool-dialog/CustomToolDialog.tsx(新建)
/**
* 自定义工具添加对话框
*/
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 持久化自定义工具:
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');
}
}
}
验收标准:
- 用户可以通过对话框添加自定义工具
- 自定义工具保存到 IndexedDB
- 刷新页面后自定义工具仍然存在
- 自定义工具可以删除
- 表单验证阻止无效配置
📂 新增文件清单
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 小时)
- 错误处理增强 → 提升稳定性
- 样式优化 → 提升视觉体验
第二步:P1 任务(2 小时) 3. postMessage 通信 → 解锁工具交互能力
第三步:P2 任务(1.5 小时) 4. 自定义工具 → 提升可扩展性
测试要点
功能测试
- 错误场景覆盖:加载失败、CORS、超时
- 样式在不同设备和主题下正确显示
- postMessage 消息正确发送和接收
- 自定义工具添加、删除、持久化正常
性能测试
- 多个工具同时加载不卡顿
- postMessage 消息处理不阻塞 UI
- 自定义工具数量增加不影响性能
安全测试
- postMessage 消息来源验证
- 自定义工具 URL 验证
- iframe sandbox 权限正确设置
📊 预期成果
完成 Phase 3 后,多功能工具箱将具备:
-
✅ 完善的用户体验
- 友好的错误提示
- 精美的视觉设计
- 响应式适配
- 深色模式支持
-
✅ 强大的交互能力
- 工具与画布双向通信
- 工具可以向画布插入内容
- 画布可以向工具发送数据
-
✅ 高度可扩展
- 用户可以添加任意工具
- 工具配置持久化
- 支持自定义权限和样式
-
✅ 生产级质量
- 完善的错误处理
- 安全的消息验证
- 良好的性能表现
🔗 相关文档
- ARCHITECTURE.md - 整体架构设计
- PHASE1_COMPLETE.md - Phase 1 完成总结
- PHASE2_COMPLETE.md - Phase 2 完成总结
- postMessage API 文档
- localforage 文档
📝 注意事项
技术债务
- postMessage 通信需要工具网页配合实现协议
- CORS 错误检测可能不准确(浏览器限制)
- 自定义工具的安全性依赖用户自觉
兼容性
- postMessage 兼容性良好(IE 8+)
- localforage 自动降级到 localStorage
- CSS 变量需要 IE 11+(可用 PostCSS 处理)
Created by: Claude Code Date: 2025-12-09 Status: ✅ 计划完成,待审核