# 组件基础示例

Page（页面）和 Widget（卡片）组件的基础用法对比。

---

## 📋 核心差异

| 特性 | Page | Widget |
|------|------|--------|
| **使用场景** | 全屏页面、九宫格、浮窗 | 聊天流中的卡片 |
| **定义方式** | 默认导出组件函数 | 默认导出组件函数 |
| **输入数据** | URL 参数、页面配置 | AI 生成的结构化数据 |
| **显示位置** | 独立页面/窗口 | 聊天对话中 |
| **交互方式** | 完整的页面交互 | 卡片内交互 + 消息发送 |
| **生命周期** | 函数入口使用 `useCreated`, `useMounted`, `useShow`, `useHide`, `useDestroy`, `useError`；对象入口使用对应的 `onXxx` 字段 | 与 Page 相同，函数入口额外支持 `useForeground`, `useBackground`；对象入口使用对应的 `onXxx` 字段 |

---

## 🎯 Page 基础示例

### 用户资料页面

**文件结构**：
```
src/pages/user-profile/
├── index.tsx       # 组件逻辑
└── index.scss      # 组件样式
```

### app.config.ts

`app.config.ts` 需要配置 `appId` 和 `name`。这里额外配置 `pages` 是为了补充标题和描述；页面使用
`/pages/user-profile/index` 作为完整路径。

```ts
import { defineAppConfig } from '@doubao-dev/framework/config';

export default defineAppConfig({
  appId: 'db_xxxxxx',
  name: '我的豆包应用',
  pages: [
    {
      entry: 'pages/user-profile/index',
      title: '用户资料',
      description: '显示用户个人信息和统计数据'
    }
  ]
});
```

### index.tsx

```tsx
import { useEffect, useHide, useShow, useState, useViewData } from '@doubao-dev/framework';
import './index.scss';

interface UserProfilePageData {
  userId?: string;
  title?: string;
}

interface UserProfile {
  id: string;
  name: string;
  avatar: string;
  email: string;
  stats: {
    posts: number;
    followers: number;
    following: number;
  };
}

export default function UserProfilePage() {
  const viewData = useViewData<UserProfilePageData>();
  const [user, setUser] = useState<UserProfile | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  useShow(() => console.log('页面显示'));
  useHide(() => console.log('页面隐藏'));

  // 数据获取
  const fetchUserProfile = async () => {
    try {
      setLoading(true);
      setError(null);

      // 模拟 API 请求
      await new Promise(resolve => setTimeout(resolve, 1000));

      const mockData: UserProfile = {
        id: viewData.userId || '12345',
        name: '张三',
        avatar: 'https://via.placeholder.com/150',
        email: 'zhangsan@example.com',
        stats: {
          posts: 42,
          followers: 1234,
          following: 567
        }
      };

      setUser(mockData);
    } catch (err) {
      setError('加载失败，请重试');
    } finally {
      setLoading(false);
    }
  };

  // 编辑按钮
  const handleEdit = () => {
    console.log('编辑用户资料');
  };

  useEffect(() => {
    void fetchUserProfile();
  }, [viewData.userId]);

  // 渲染
  if (loading) {
    return (
      <view className="profile-container">
        <view className="loading">加载中...</view>
      </view>
    );
  }

  if (error) {
    return (
      <view className="profile-container">
        <view className="error">{error}</view>
        <button onClick={fetchUserProfile}>重试</button>
      </view>
    );
  }

  if (!user) {
    return null;
  }

  return (
    <view className="profile-container">
      {/* 头部 */}
      <view className="profile-header">
        <text className="page-title">{viewData.title || '用户资料'}</text>
        <image className="avatar" src={user.avatar} />
        <text className="name">{user.name}</text>
        <text className="email">{user.email}</text>
        <button className="edit-btn" onClick={handleEdit}>
          编辑资料
        </button>
      </view>

      {/* 数据统计 */}
      <view className="profile-stats">
        <view className="stat-item">
          <text className="stat-value">{user.stats.posts}</text>
          <text className="stat-label">帖子</text>
        </view>
        <view className="stat-item">
          <text className="stat-value">{user.stats.followers}</text>
          <text className="stat-label">关注者</text>
        </view>
        <view className="stat-item">
          <text className="stat-value">{user.stats.following}</text>
          <text className="stat-label">关注中</text>
        </view>
      </view>
    </view>
  );
}
```

### index.scss

```scss
.profile-container {
  padding: 32px;
  background-color: #f5f5f5;

  .loading,
  .error {
    text-align: center;
    padding: 40px;
    font-size: 28px;
    color: #666;
  }

  .profile-header {
    background-color: #fff;
    border-radius: 16px;
    padding: 40px;
    text-align: center;
    margin-bottom: 32px;

    .page-title {
      display: block;
      font-size: 32px;
      font-weight: 600;
      color: #222;
      margin-bottom: 24px;
    }

    .avatar {
      width: 160px;
      height: 160px;
      border-radius: 80px;
      margin-bottom: 24px;
    }

    .name {
      font-size: 36px;
      font-weight: bold;
      color: #333;
      margin-bottom: 12px;
    }

    .email {
      font-size: 28px;
      color: #666;
      margin-bottom: 24px;
    }

    .edit-btn {
      padding: 16px 48px;
      background-color: #1890ff;
      color: #fff;
      border-radius: 8px;
      font-size: 28px;
    }
  }

  .profile-stats {
    display: flex;
    justify-content: space-around;
    background-color: #fff;
    border-radius: 16px;
    padding: 32px;

    .stat-item {
      text-align: center;

      .stat-value {
        font-size: 36px;
        font-weight: bold;
        color: #333;
        margin-bottom: 8px;
      }

      .stat-label {
        font-size: 24px;
        color: #999;
      }
    }
  }
}
```

---

## 🎴 Widget 基础示例

Widget 卡片内容和布局必须使用 [Widget 模板库](../widget-templates/overview.md) 中的 `@doubao-dev/template` 模板。先根据业务场景在模板库中选型，
再在入口组件中读取 `viewData` 并直接返回模板组件。

### 列表卡片

**文件结构**：
```
src/widgets/recommend-list/
├── index.tsx       # 组件逻辑
```

### index.tsx

```tsx
import { useDestroy, useMounted, useViewData } from '@doubao-dev/framework';
import { ContentCard, type ContentCardItem } from '@doubao-dev/template';

interface RecommendListData {
  items: ContentCardItem[];
  actionText?: string;
}

export default function RecommendList() {
  const viewData = useViewData<RecommendListData>();
  useMounted(() => console.log('卡片挂载'));
  useDestroy(() => console.log('卡片销毁'));

  return (
    <ContentCard
      items={viewData.items}
      footer={
        viewData.actionText
          ? {
              primaryActionButton: {
                text: viewData.actionText,
                onClick: () => console.log('查看更多')
              }
            }
          : undefined
      }
    />
  );
}
```

---

## 🔑 关键要点

### Page 开发要点

1. **完整的页面生命周期**
   - `useShow()` - 页面显示时调用
   - `useHide()` - 页面隐藏时调用
   - `useDestroy()` - 页面销毁时调用

2. **页面配置（可选 pages）**
   - 一级目录不写进 `pages` 也会自动发现，默认 `id` 是目录名
   - 需要 `title`、`description`、固定首页或多级目录时再配置
   - 多级目录需要显式写 `entry`

3. **状态管理**
   - 使用 React Hooks（useState, useEffect）
   - 处理加载、错误、数据状态

4. **用户交互**
   - 按钮点击、表单提交
   - 页面跳转、数据更新

### Widget 开发要点

1. **卡片生命周期**
   - `useShow()` / `useHide()` - 卡片显示/隐藏
   - `useForeground()` / `useBackground()` - 应用前后台切换
   - `useMounted()` / `useDestroy()` - 挂载/销毁

2. **ViewData 定义**
   - 使用 TypeScript 类型定义 `useViewData<T>()` 的 viewData 结构
   - 数据结构优先贴近 `@doubao-dev/template` 的模板 props 或列表项类型
   - 对可选字段和空数据做兜底处理
   - 避免直接使用 `any`

3. **卡片类型 (boxType)**
   - `inbox` - 普通卡片
   - `full_box` - 全宽卡片

4. **消息交互**
   - 接收 AI 传入的数据
   - 可以发送消息回 Bot
   - 支持通过模板按钮和事件进行卡片内交互

---

## 📚 延伸阅读

- **组件开发完整指南** → [../guides/component-development.md](../guides/component-development.md)
- **常用开发模式** → [./common-patterns.md](./common-patterns.md)
- **豆包智能服务的端能力 API 使用配方** → [./doubao-agentic-service-api-recipes.md](./doubao-agentic-service-api-recipes.md)
- **Page 组件配方** → [./page-widget-recipes.md](./page-widget-recipes.md)
- **Framework 核心入口、生命周期和 Hooks** → [../framework/core.md](../framework/core.md)
- **豆包智能服务的端能力 API 速查** → [../doubao-agentic-service-api/quick-reference.md](../doubao-agentic-service-api/quick-reference.md)
