Skip to content

UI 组件开发 ​

编程式场景支持自定义 React 面板组件,实现丰富的用户界面。

组件注册 ​

第一步:定义面板 ​

在 getPlugin() 的 ui.panels 中声明面板:

typescript
getPlugin() {
  return {
    ui: {
      layout: 'dashboard-centric',
      panels: [
        {
          id: 'dashboard',
          component: 'DashboardPanel',
          region: 'primary',
          defaultVisible: true,
          resizable: true,
          minWidth: 300,
          maxWidth: 800,
        },
        {
          id: 'settings',
          component: 'SettingsPanel',
          region: 'secondary',
          defaultVisible: false,
          resizable: true,
          minWidth: 250,
          maxWidth: 500,
        },
        {
          id: 'chat',
          component: 'ChatPanel',
          region: 'auxiliary',
          defaultVisible: true,
          resizable: true,
          minWidth: 300,
          maxWidth: 600,
        },
      ],
    },
  }
}

面板区域 ​

区域说明位置
primary主面板中央区域
secondary次面板左侧或右侧
auxiliary辅助面板聊天面板
bottom底部面板终端/输出
overlay浮层面板弹窗覆盖

面板配置 ​

字段类型说明
idstring面板唯一标识
componentstring组件名(对应 getComponents 的 key)
regionstring面板区域
defaultVisibleboolean默认是否可见
resizableboolean是否可调整大小
minWidthnumber最小宽度(px)
maxWidthnumber最大宽度(px)
minHeightnumber最小高度(px)
maxHeightnumber最大高度(px)

第二步:注册组件 ​

在 getComponents() 中返回组件映射:

typescript
import { DashboardPanel } from './components/DashboardPanel.js'
import { SettingsPanel } from './components/SettingsPanel.js'

getComponents() {
  return {
    DashboardPanel,
    SettingsPanel,
  }
}

组件开发 ​

基础组件 ​

tsx
import React from 'react'

export const DashboardPanel: React.FC = () => {
  return (
    <div className="dashboard-panel">
      <h2>仪表盘</h2>
      <p>欢迎使用场景仪表盘</p>
    </div>
  )
}

使用共享依赖 ​

组件可以使用客户端注入的共享依赖:

tsx
import React, { useState, useEffect } from 'react'
import { BarChart3, TrendingUp } from 'lucide-react'

interface StatsData {
  total: number
  completed: number
  pending: number
}

export const StatsPanel: React.FC = () => {
  const [stats, setStats] = useState<StatsData>({
    total: 0,
    completed: 0,
    pending: 0,
  })

  useEffect(() => {
    // 获取数据
    loadStats()
  }, [])

  const loadStats = async () => {
    // 通过场景数据通道获取数据
    // 具体实现取决于场景的数据源
  }

  return (
    <div className="stats-panel p-4">
      <h3 className="text-lg font-semibold mb-4">统计数据</h3>
      <div className="grid grid-cols-3 gap-4">
        <div className="stat-card">
          <BarChart3 className="w-6 h-6 text-blue-500" />
          <span className="text-2xl font-bold">{stats.total}</span>
          <span className="text-sm text-gray-500">总计</span>
        </div>
        <div className="stat-card">
          <TrendingUp className="w-6 h-6 text-green-500" />
          <span className="text-2xl font-bold">{stats.completed}</span>
          <span className="text-sm text-gray-500">已完成</span>
        </div>
        <div className="stat-card">
          <div className="w-6 h-6 text-yellow-500" />
          <span className="text-2xl font-bold">{stats.pending}</span>
          <span className="text-sm text-gray-500">待处理</span>
        </div>
      </div>
    </div>
  )
}

数据通信 ​

组件通过数据通道与场景通信:

tsx
import React, { useEffect } from 'react'

export const DataPanel: React.FC = () => {
  useEffect(() => {
    // 订阅数据通道
    const handleData = (event: CustomEvent) => {
      console.log('收到数据:', event.detail)
    }

    window.addEventListener('scenario:data-update', handleData as EventListener)
    return () => {
      window.removeEventListener('scenario:data-update', handleData as EventListener)
    }
  }, [])

  return <div>Data Panel</div>
}

样式 ​

组件使用 TailwindCSS 或自定义 CSS:

tsx
// 使用 TailwindCSS
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow">

// 或使用 CSS Modules
import styles from './DashboardPanel.module.css'
<div className={styles.container}>

仪表盘卡片 ​

场景可以在仪表盘页面展示数据卡片,让用户快速查看关键信息。

声明卡片 ​

在 getPlugin() 的 ui.widgetCards 中声明卡片:

typescript
getPlugin() {
  return {
    ui: {
      layout: 'dashboard-centric',
      widgetCards: [
        {
          id: 'code-reviewer:stats',
          icon: 'BarChart3',
          label: 'Review Stats',
          labelZh: '审查统计',
          description: 'Code review statistics',
          descriptionZh: '代码审查统计数据',
          tier: 'core',
          previewComponent: 'StatsCard',
        },
        {
          id: 'code-reviewer:recent',
          icon: 'Clock',
          label: 'Recent Reviews',
          labelZh: '最近审查',
          description: 'Recent code reviews',
          descriptionZh: '最近的代码审查',
          tier: 'enhanced',
          previewComponent: 'RecentReviewsCard',
        },
      ],
    },
  }
}

卡片配置 ​

字段类型说明
idstring卡片唯一标识,格式:<scenarioId>:<cardName>
iconstringlucide 图标名
labelstring英文标题
labelZhstring中文标题
descriptionstring英文描述(可选)
descriptionZhstring中文描述(可选)
tier'core' | 'enhanced'刷新优先级(可选,默认 'enhanced')
previewComponentstring预览组件名(对应 getComponents() 的 key)

刷新优先级 ​

  • core:高频刷新(15秒),适用于核心数据卡片
  • enhanced:低频刷新(60秒),适用于一般数据卡片

实现卡片组件 ​

在 getComponents() 中注册卡片组件:

typescript
import { StatsCard } from './components/StatsCard.js'
import { RecentReviewsCard } from './components/RecentReviewsCard.js'

getComponents() {
  return {
    StatsCard,
    RecentReviewsCard,
  }
}

卡片组件签名:

tsx
import React from 'react'

interface WidgetCardPreviewProps {
  /** 卡片数据 */
  data: Record<string, unknown> | null
  /** 是否正在加载 */
  loading: boolean
  /** 手动刷新回调 */
  onRefresh: () => void
}

export const StatsCard: React.FC<WidgetCardPreviewProps> = ({
  data,
  loading,
  onRefresh,
}) => {
  if (loading) {
    return <div className="p-4 animate-pulse">加载中...</div>
  }

  if (!data) {
    return (
      <div className="p-4 text-center">
        <p>暂无数据</p>
        <button onClick={onRefresh}>刷新</button>
      </div>
    )
  }

  return (
    <div className="p-4">
      <h3>统计</h3>
      <p>总计: {data.total}</p>
    </div>
  )
}

提供卡片数据 ​

实现 getCardData() 方法为卡片提供数据:

typescript
const module: ScenarioModule = {
  // ... 其他配置

  getCardData: async (cardId: string) => {
    switch (cardId) {
      case 'code-reviewer:stats':
        return {
          total: 156,
          approved: 120,
          rejected: 24,
          pending: 12,
        }
      case 'code-reviewer:recent':
        return {
          reviews: [
            { id: 1, title: 'Fix login bug', status: 'approved' },
            { id: 2, title: 'Add user API', status: 'pending' },
          ],
        }
      default:
        return null
    }
  },
}

卡片生命周期 ​

  • 场景激活时,卡片自动注册到仪表盘
  • 场景停用时,卡片自动从仪表盘移除
  • 卡片数据根据 tier 配置自动刷新
  • 用户可以手动添加/移除场景卡片

最佳实践 ​

  1. 组件轻量:保持组件简洁,避免复杂逻辑
  2. 响应式:使用 TailwindCSS 响应式类
  3. 性能:避免不必要的重渲染
  4. 错误边界:添加错误边界处理异常
  5. 加载状态:处理数据加载中的状态
  6. 空状态:处理无数据的情况

AweeClaw AI 应用构建平台