Files

17 KiB
Raw Permalink Blame History

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 小时)

  1. 错误处理增强 → 提升稳定性
  2. 样式优化 → 提升视觉体验

第二步P1 任务2 小时) 3. postMessage 通信 → 解锁工具交互能力

第三步P2 任务1.5 小时) 4. 自定义工具 → 提升可扩展性

测试要点

功能测试

  • 错误场景覆盖加载失败、CORS、超时
  • 样式在不同设备和主题下正确显示
  • postMessage 消息正确发送和接收
  • 自定义工具添加、删除、持久化正常

性能测试

  • 多个工具同时加载不卡顿
  • postMessage 消息处理不阻塞 UI
  • 自定义工具数量增加不影响性能

安全测试

  • postMessage 消息来源验证
  • 自定义工具 URL 验证
  • iframe sandbox 权限正确设置

📊 预期成果

完成 Phase 3 后,多功能工具箱将具备:

  1. 完善的用户体验

    • 友好的错误提示
    • 精美的视觉设计
    • 响应式适配
    • 深色模式支持
  2. 强大的交互能力

    • 工具与画布双向通信
    • 工具可以向画布插入内容
    • 画布可以向工具发送数据
  3. 高度可扩展

    • 用户可以添加任意工具
    • 工具配置持久化
    • 支持自定义权限和样式
  4. 生产级质量

    • 完善的错误处理
    • 安全的消息验证
    • 良好的性能表现

🔗 相关文档


📝 注意事项

技术债务

  • postMessage 通信需要工具网页配合实现协议
  • CORS 错误检测可能不准确(浏览器限制)
  • 自定义工具的安全性依赖用户自觉

兼容性

  • postMessage 兼容性良好IE 8+
  • localforage 自动降级到 localStorage
  • CSS 变量需要 IE 11+(可用 PostCSS 处理)

Created by: Claude Code Date: 2025-12-09 Status: 计划完成,待审核