@doubao-dev/plugin-app-dev 0.0.0-stage → 0.0.40-canary-dea0ddc73-20261026073950
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/147.js +9 -0
- package/dist/assets.d.ts +5 -0
- package/dist/assets.js +4 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +1378 -0
- package/dist/plugin.d.ts +102 -0
- package/dist/plugin.js +4995 -0
- package/dist/preview.js +16 -0
- package/dist/rslib-runtime.js +33 -0
- package/dist/skills/doubao-plugin-app/SKILL.md +21 -0
- package/dist/skills/doubao-plugin-app/references/app-development.md +85 -0
- package/dist/skills/doubao-plugin-app/references/frontend/components/container-overlay.md +131 -0
- package/dist/skills/doubao-plugin-app/references/frontend/components/form-controls.md +146 -0
- package/dist/skills/doubao-plugin-app/references/frontend/components/interactive-controls.md +120 -0
- package/dist/skills/doubao-plugin-app/references/frontend/components/map-canvas.md +107 -0
- package/dist/skills/doubao-plugin-app/references/frontend/components/overview.md +43 -0
- package/dist/skills/doubao-plugin-app/references/frontend/components/scroll-list.md +149 -0
- package/dist/skills/doubao-plugin-app/references/frontend/components/text-media.md +209 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/01-/345/237/272/347/241/200.md +1389 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/02-/347/263/273/347/273/237.md +1376 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/03-/350/207/252/345/256/232/344/271/211/351/200/232/344/277/241.md +127 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/04-/345/256/232/344/275/215.md +784 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/05-/345/255/230/345/202/250.md +1176 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/06-/350/267/257/347/224/261.md +839 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/07-/347/225/214/351/235/242.md +376 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/08-/344/272/244/344/272/222.md +1319 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/09-/350/276/223/345/205/245.md +192 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/10-/347/275/221/347/273/234.md +900 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/11-/345/252/222/344/275/223.md +1496 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/12-/344/270/232/345/212/241/350/203/275/345/212/233.md +1281 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/13-/347/231/273/345/275/225.md +511 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/14-/346/224/257/344/273/230.md +943 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/15-/346/225/260/346/215/256/345/210/206/346/236/220.md +124 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/16-/350/223/235/347/211/231.md +3364 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/17-wi-fi.md +1028 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/18-/345/212/240/351/200/237/345/272/246/350/256/241.md +354 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/19-ibeacon.md +508 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/20-/347/275/227/347/233/230.md +320 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/21-/350/256/276/345/244/207/346/226/271/345/220/221.md +351 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/22-/351/231/200/350/236/272/344/273/252.md +354 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/23-/345/252/222/344/275/223-/350/256/276/345/244/207-/347/275/221/347/273/234.md +264 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/24-/347/237/255/344/277/241.md +155 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/25-/346/227/240/351/232/234/347/242/215.md +144 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/26-/345/237/272/347/241/200/344/277/241/346/201/257.md +238 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/27-/347/224/265/346/261/240.md +235 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/28-/345/211/252/350/264/264/346/235/277.md +303 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/29-/345/212/240/345/257/206.md +126 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/30-/346/227/245/345/216/206.md +417 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/31-/347/224/265/350/257/235.md +144 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/32-/346/211/253/347/240/201.md +236 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/33-/345/261/217/345/271/225.md +747 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/34-/351/234/207/345/212/250.md +277 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/35-/346/226/207/344/273/266/347/263/273/347/273/237.md +431 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/common-errors.md +69 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/groups.md +43 -0
- package/dist/skills/doubao-plugin-app/references/frontend/doubao-agentic-service-api/quick-reference.md +436 -0
- package/dist/skills/doubao-plugin-app/references/frontend/examples/common-patterns.md +590 -0
- package/dist/skills/doubao-plugin-app/references/frontend/examples/doubao-agentic-service-api-recipes.md +209 -0
- package/dist/skills/doubao-plugin-app/references/frontend/examples/page-widget-basics.md +368 -0
- package/dist/skills/doubao-plugin-app/references/frontend/examples/page-widget-recipes.md +65 -0
- package/dist/skills/doubao-plugin-app/references/frontend/framework/core.md +283 -0
- package/dist/skills/doubao-plugin-app/references/frontend/guides/auth.md +277 -0
- package/dist/skills/doubao-plugin-app/references/frontend/guides/best-practices.md +574 -0
- package/dist/skills/doubao-plugin-app/references/frontend/guides/client-tools-and-events.md +209 -0
- package/dist/skills/doubao-plugin-app/references/frontend/guides/component-development.md +832 -0
- package/dist/skills/doubao-plugin-app/references/frontend/guides/debugger-mcp.md +537 -0
- package/dist/skills/doubao-plugin-app/references/frontend/guides/expired-widget.md +101 -0
- package/dist/skills/doubao-plugin-app/references/frontend/guides/performance-optimization.md +417 -0
- package/dist/skills/doubao-plugin-app/references/frontend/guides/troubleshooting.md +278 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/best-practices.md +720 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/css/pseudo-classes.md +149 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/css/selectors.md +224 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/css/supported-properties.md +269 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/css/values-and-units.md +314 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/layout/flex-layout.md +334 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/layout/grid-layout.md +408 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/layout/linear-layout.md +355 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/layout/relative-layout.md +189 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/lynx-vs-web/css-differences.md +737 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/lynx-vs-web/migration-guide.md +669 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/lynx-vs-web/unsupported-features.md +290 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/overview.md +60 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/patterns/animation.md +243 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/patterns/responsive.md +194 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/patterns/theming.md +143 -0
- package/dist/skills/doubao-plugin-app/references/frontend/lynx/quick-reference.md +229 -0
- package/dist/skills/doubao-plugin-app/references/frontend/rules/dos-and-donts.md +479 -0
- package/dist/skills/doubao-plugin-app/references/frontend/widget-templates/design-boundary.md +46 -0
- package/dist/skills/doubao-plugin-app/references/frontend/widget-templates/overview.md +118 -0
- package/dist/skills/doubao-plugin-app/references/frontend/widget-templates/props.md +301 -0
- package/dist/skills/doubao-plugin-app/references/mcp-and-cards.md +38 -0
- package/dist/templates/README.md.template +11 -0
- package/dist/templates/com.doubao/app/manifest.yaml +41 -0
- package/dist/templates/com.doubao/app/package.json.template +13 -0
- package/dist/templates/com.doubao/app/src/app.config.ts.template +5 -0
- package/dist/templates/com.doubao/app/src/app.ts.template +3 -0
- package/dist/templates/com.doubao/app/src/pages/home/index.css +14 -0
- package/dist/templates/com.doubao/app/src/pages/home/index.tsx.template +14 -0
- package/dist/templates/com.doubao/app/src/widgets/example/index.tsx.template +29 -0
- package/dist/templates/com.doubao/app/tsconfig.json +19 -0
- package/dist/templates/mcp.json +9 -0
- package/dist/templates/plugin.json.template +29 -0
- package/dist/templates/skills/example/SKILL.md.template +14 -0
- package/dist/upload.d.ts +44 -0
- package/dist/upload.js +38 -0
- package/package.json +60 -3
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
# 组件开发指南
|
|
2
|
+
|
|
3
|
+
> 项目配置:下文 `defineAppConfig` 和 `appId` 示例适用于独立应用项目,沿用其配置要求。Plugin CLI 项目以根目录的 `plugin.json` 为标识,`src/` 位于 `com.doubao/app/` 下;`app.config.ts` 默认导出普通对象,不填写 `appId`,由 CLI 查询注入。配置示例以 `doubao-plugin-app` Skill 的应用开发指南为准,本文的组件、Hooks 和入口 metadata 用法共用。
|
|
4
|
+
|
|
5
|
+
Page(页面)和 Widget(卡片)组件的完整开发指南。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🎯 Page 开发指南
|
|
10
|
+
|
|
11
|
+
### App 配置、可选入口与多级目录
|
|
12
|
+
|
|
13
|
+
独立应用项目的 `src/app.config.ts` 是应用配置文件,需要配置 `appId` 和 `name`,`appId` 使用开放平台 `db_xxxxxx` 风格。`pages` / `widgets` 是可选的显式入口配置。
|
|
14
|
+
一级页面目录会自动发现,例如 `src/pages/home/index.tsx` 会产出页面路径 `/pages/home/index`;不配置到 `pages` 时仍可构建,
|
|
15
|
+
但 `title`、`description` 等页面 metadata 为空。
|
|
16
|
+
|
|
17
|
+
需要补 metadata、固定首页顺序或声明多级目录时,再配置 `pages` 数组。数组第一项会作为应用首页
|
|
18
|
+
(最终 manifest 的 `appletEntry`)。显式声明入口时,entry 写到 `index`,例如 `'pages/home/index'`,
|
|
19
|
+
不要省略入口文件名。
|
|
20
|
+
|
|
21
|
+
如果不配置 `pages` / `widgets`,一级 Page / Widget 仍会按目录产出;Page 的跳转路径按入口推导,Widget 的默认 ID 取入口最后一级目录名,
|
|
22
|
+
页面/卡片 metadata 字段为空,Widget 的 `boxType` 默认为 `inbox`。没有显式 `pages` 顺序时,默认首页入口为
|
|
23
|
+
`index`;首页不是 `index` 时建议配置 `pages` 第一项。
|
|
24
|
+
|
|
25
|
+
多级目录不会被一级扫描自动发现,例如 `src/pages/account/demo/index.tsx` 需要显式写
|
|
26
|
+
`entry: 'pages/account/demo/index'`。页面跳转使用 entry 对应的路径,例如 `/pages/account/demo/index`。
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { defineAppConfig } from '@doubao-dev/framework/config';
|
|
30
|
+
|
|
31
|
+
export default defineAppConfig({
|
|
32
|
+
appId: 'db_xxxxxx', // 独立应用项目填写;Plugin CLI 项目不填写
|
|
33
|
+
name: '我的豆包应用',
|
|
34
|
+
pages: [
|
|
35
|
+
'pages/home/index',
|
|
36
|
+
'pages/account/demo/index',
|
|
37
|
+
{
|
|
38
|
+
entry: 'pages/profile/index',
|
|
39
|
+
title: '资料页',
|
|
40
|
+
description: '补充 metadata 的页面示例'
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 基本结构
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { useDestroy, useHide, useShow, useViewData } from '@doubao-dev/framework';
|
|
50
|
+
import './index.scss';
|
|
51
|
+
|
|
52
|
+
interface PageViewData {
|
|
53
|
+
title: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function Page() {
|
|
57
|
+
useShow(() => console.log('页面显示'));
|
|
58
|
+
useHide(() => console.log('页面隐藏'));
|
|
59
|
+
useDestroy(() => console.log('页面销毁'));
|
|
60
|
+
|
|
61
|
+
const viewData = useViewData<PageViewData>();
|
|
62
|
+
return <MyPageComponent {...viewData} />;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 页面布局示例
|
|
67
|
+
|
|
68
|
+
**1. 默认页面布局**
|
|
69
|
+
普通 Page 默认采用可滚动安全区布局:根节点使用垂直 `scroll-view`,内容区预留顶部安全距离,避免状态栏和右上角胶囊按钮遮挡。
|
|
70
|
+
完整规则和样式写法见 [开发规则](../rules/dos-and-donts.md)。
|
|
71
|
+
```tsx
|
|
72
|
+
export default function Page() {
|
|
73
|
+
return (
|
|
74
|
+
<scroll-view className="full-page" scroll-orientation="vertical" enable-scroll={true}>
|
|
75
|
+
<view className="full-page__content">
|
|
76
|
+
<view className="full-page__header">头部</view>
|
|
77
|
+
<view className="full-page__body">内容区域</view>
|
|
78
|
+
<view className="full-page__footer">底部</view>
|
|
79
|
+
</view>
|
|
80
|
+
</scroll-view>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**2. 九宫格布局示例**
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
export default function GridPage() {
|
|
89
|
+
const items = [
|
|
90
|
+
{ id: '1', title: '应用1', icon: '🎯' },
|
|
91
|
+
{ id: '2', title: '应用2', icon: '📱' },
|
|
92
|
+
{ id: '3', title: '应用3', icon: '⚡' }
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<view className="grid-container">
|
|
97
|
+
{items.map(item => (
|
|
98
|
+
<view key={item.id} className="grid-item">
|
|
99
|
+
<text className="icon">{item.icon}</text>
|
|
100
|
+
<text className="title">{item.title}</text>
|
|
101
|
+
</view>
|
|
102
|
+
))}
|
|
103
|
+
</view>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 页面参数接收
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { useViewData } from '@doubao-dev/framework';
|
|
112
|
+
|
|
113
|
+
interface PageViewData {
|
|
114
|
+
userId: string;
|
|
115
|
+
tab?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default function ProfilePage() {
|
|
119
|
+
const viewData = useViewData<PageViewData>();
|
|
120
|
+
const { userId, tab = 'profile' } = viewData;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<view>
|
|
124
|
+
<text>用户 ID: {userId}</text>
|
|
125
|
+
<text>当前标签: {tab}</text>
|
|
126
|
+
</view>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 页面导航
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { navigateTo, redirectTo, reLaunch } from '@doubao-dev/framework/api';
|
|
135
|
+
|
|
136
|
+
// 保留当前页面,打开新页面
|
|
137
|
+
navigateTo({
|
|
138
|
+
url: '/pages/user-detail/index?userId=123&tab=profile'
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 替换当前页面
|
|
142
|
+
redirectTo({
|
|
143
|
+
url: '/pages/settings/index'
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 关闭所有页面并回到首页
|
|
147
|
+
reLaunch({
|
|
148
|
+
url: '/pages/home/index'
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`url` 在常规页面跳转里填写目标 Page 路径,通常和 `src/pages/<page-name>/index.tsx` 对应,例如
|
|
153
|
+
`/pages/detail/index`。
|
|
154
|
+
|
|
155
|
+
页面内通过 `useViewData<PageViewData>()` 读取 URL 中传入的参数。
|
|
156
|
+
|
|
157
|
+
### 常用模式
|
|
158
|
+
|
|
159
|
+
**1. 数据加载**
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { useEffect, useState } from '@doubao-dev/framework';
|
|
163
|
+
import { request } from '@doubao-dev/framework/api';
|
|
164
|
+
|
|
165
|
+
interface UserData {
|
|
166
|
+
name?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function MyPage({ userId }: { userId: string }) {
|
|
170
|
+
const [data, setData] = useState<UserData | null>(null);
|
|
171
|
+
const [loading, setLoading] = useState(true);
|
|
172
|
+
const [error, setError] = useState('');
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const fetchData = async () => {
|
|
176
|
+
try {
|
|
177
|
+
setLoading(true);
|
|
178
|
+
setError('');
|
|
179
|
+
const result = await request({
|
|
180
|
+
url: `https://api.example.com/users/${userId}`,
|
|
181
|
+
method: 'GET'
|
|
182
|
+
});
|
|
183
|
+
setData(result.data as UserData);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
setError(err instanceof Error ? err.message : '加载失败');
|
|
186
|
+
} finally {
|
|
187
|
+
setLoading(false);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
void fetchData();
|
|
192
|
+
}, [userId]);
|
|
193
|
+
|
|
194
|
+
if (loading) return <text>加载中...</text>;
|
|
195
|
+
if (error) return <text>错误: {error}</text>;
|
|
196
|
+
if (!data) return <text>无数据</text>;
|
|
197
|
+
|
|
198
|
+
return <view>{/* 渲染数据 */}</view>;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**2. 表单提交**
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
import { useState } from '@doubao-dev/framework';
|
|
206
|
+
import { navigateBack, request, showToast } from '@doubao-dev/framework/api';
|
|
207
|
+
|
|
208
|
+
function FormPage() {
|
|
209
|
+
const [formData, setFormData] = useState({
|
|
210
|
+
name: '',
|
|
211
|
+
email: ''
|
|
212
|
+
});
|
|
213
|
+
const [submitting, setSubmitting] = useState(false);
|
|
214
|
+
|
|
215
|
+
const handleSubmit = async () => {
|
|
216
|
+
try {
|
|
217
|
+
setSubmitting(true);
|
|
218
|
+
await request({
|
|
219
|
+
url: 'https://api.example.com/submit',
|
|
220
|
+
method: 'POST',
|
|
221
|
+
data: formData
|
|
222
|
+
});
|
|
223
|
+
showToast({ message: '提交成功' });
|
|
224
|
+
navigateBack(); // 返回上一页
|
|
225
|
+
} catch (err) {
|
|
226
|
+
showToast({ message: '提交失败' });
|
|
227
|
+
} finally {
|
|
228
|
+
setSubmitting(false);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<view>
|
|
234
|
+
<input
|
|
235
|
+
value={formData.name}
|
|
236
|
+
onInput={e => setFormData({ ...formData, name: e.detail.value })}
|
|
237
|
+
placeholder="姓名"
|
|
238
|
+
/>
|
|
239
|
+
<input
|
|
240
|
+
value={formData.email}
|
|
241
|
+
onInput={e => setFormData({ ...formData, email: e.detail.value })}
|
|
242
|
+
placeholder="邮箱"
|
|
243
|
+
/>
|
|
244
|
+
<button onClick={handleSubmit} disabled={submitting}>
|
|
245
|
+
{submitting ? '提交中...' : '提交'}
|
|
246
|
+
</button>
|
|
247
|
+
</view>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 🎴 Widget 开发指南
|
|
255
|
+
|
|
256
|
+
### 可选 Widgets 配置
|
|
257
|
+
|
|
258
|
+
一级 Widget 目录会自动发现,例如 `src/widgets/my-widget/index.tsx` 会产出 `widgetId: 'my-widget'`。不配置
|
|
259
|
+
`widgets` 时仍可构建,但 `name`、`description` 等 metadata 为空,`boxType` 默认是 `inbox`。
|
|
260
|
+
`titleType` 需要显式传值;默认无标题写 `titleType: 'none'`。
|
|
261
|
+
|
|
262
|
+
Widget 的卡片内容和布局必须使用 [Widget 模板库](../widget-templates/overview.md) 中的 `@doubao-dev/template` 模板完成。创建
|
|
263
|
+
Widget 时先读模板库的选型指南和 props 参考,选中模板后再回到本指南处理入口组件、`useViewData<T>()`、生命周期、
|
|
264
|
+
`boxType`、`titleType` 和端能力调用。
|
|
265
|
+
|
|
266
|
+
不要在 Widget 中手写 `view` / `text` / `image` 拼卡片布局。现有 Props 无法覆盖的内容,按模板能力或新设计需求确认。
|
|
267
|
+
|
|
268
|
+
需要补 metadata 或声明多级目录时,再配置 `widgets` 数组。显式声明入口时,entry 写到 `index`,例如
|
|
269
|
+
`'widgets/my-widget/index'`,不要省略入口文件名。
|
|
270
|
+
多级目录如 `src/widgets/order/detail/index.tsx` 需要显式写 `'widgets/order/detail/index'`。
|
|
271
|
+
如果不配置 `id`,默认 `widgetId` 是最后一级目录名 `detail`。Widget ID 必须在应用内全局唯一;如果多个入口得到
|
|
272
|
+
相同 ID,构建会失败,需要在 `app.config.ts` 中为它们配置不同的 `id`。
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { defineAppConfig } from '@doubao-dev/framework/config';
|
|
276
|
+
|
|
277
|
+
export default defineAppConfig({
|
|
278
|
+
appId: 'db_xxxxxx', // 独立应用项目填写;Plugin CLI 项目不填写
|
|
279
|
+
name: '我的豆包应用',
|
|
280
|
+
widgets: [
|
|
281
|
+
{
|
|
282
|
+
entry: 'widgets/simple-card/index',
|
|
283
|
+
titleType: 'none'
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
entry: 'widgets/order/detail/index',
|
|
287
|
+
titleType: 'none'
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
entry: 'widgets/my-widget/index',
|
|
291
|
+
id: 'my-widget', // 卡片唯一标识
|
|
292
|
+
name: '我的卡片', // 卡片名称
|
|
293
|
+
description: '卡片功能描述', // 卡片描述
|
|
294
|
+
boxType: 'inbox', // 卡片类型: inbox | full_box | custom_box
|
|
295
|
+
border: true,
|
|
296
|
+
keywords: ['demo', 'widget'],
|
|
297
|
+
titleType: 'none' // 不展示标题栏
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 基本结构
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import {
|
|
307
|
+
useBackground,
|
|
308
|
+
useDestroy,
|
|
309
|
+
useForeground,
|
|
310
|
+
useHide,
|
|
311
|
+
useMounted,
|
|
312
|
+
useShow,
|
|
313
|
+
useViewData
|
|
314
|
+
} from '@doubao-dev/framework';
|
|
315
|
+
import { ContentCard, type ContentCardItem } from '@doubao-dev/template';
|
|
316
|
+
|
|
317
|
+
interface MyWidgetData {
|
|
318
|
+
items: ContentCardItem[];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export default function Widget() {
|
|
322
|
+
useShow(() => console.log('卡片显示'));
|
|
323
|
+
useHide(() => console.log('卡片隐藏'));
|
|
324
|
+
useForeground(() => console.log('应用回到前台'));
|
|
325
|
+
useBackground(() => console.log('应用进入后台'));
|
|
326
|
+
useMounted(() => console.log('卡片挂载'));
|
|
327
|
+
useDestroy(() => console.log('卡片销毁'));
|
|
328
|
+
|
|
329
|
+
const viewData = useViewData<MyWidgetData>();
|
|
330
|
+
return <ContentCard items={viewData.items} />;
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 卡片类型 (boxType)
|
|
335
|
+
|
|
336
|
+
**1. inbox - 普通卡片**
|
|
337
|
+
|
|
338
|
+
默认卡片类型,适用于绝大多数场景(信息展示、结果回显、轻量交互)。
|
|
339
|
+
|
|
340
|
+
推荐使用场景:
|
|
341
|
+
- 内容相对简短:标题/摘要/列表/状态提示等
|
|
342
|
+
- 不需要占用整行宽度,希望保持对话流的紧凑阅读体验
|
|
343
|
+
- 交互以按钮、简单表单项为主
|
|
344
|
+
|
|
345
|
+
在 `src/app.config.ts` 的对应 Widget entry 上配置 `boxType: 'inbox'`。入口和 metadata 写法以
|
|
346
|
+
[开发规则](../rules/dos-and-donts.md) 为准。卡片内容仍从 [Widget 模板库](../widget-templates/overview.md) 选模板,不因 `boxType`
|
|
347
|
+
变化而手写布局。
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// src/app.config.ts
|
|
351
|
+
import { defineAppConfig } from '@doubao-dev/framework/config';
|
|
352
|
+
|
|
353
|
+
export default defineAppConfig({
|
|
354
|
+
appId: 'db_xxxxxx', // 独立应用项目填写;Plugin CLI 项目不填写
|
|
355
|
+
name: '我的豆包应用',
|
|
356
|
+
widgets: [
|
|
357
|
+
{
|
|
358
|
+
entry: 'widgets/info-card/index',
|
|
359
|
+
id: 'info-card',
|
|
360
|
+
name: '信息卡片',
|
|
361
|
+
boxType: 'inbox',
|
|
362
|
+
titleType: 'none'
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**2. full_box - 全宽卡片**
|
|
369
|
+
|
|
370
|
+
适用于需要更大展示空间的卡片(例如图表、图片墙、复杂表单等)。
|
|
371
|
+
|
|
372
|
+
在 `src/app.config.ts` 的对应 Widget entry 上配置 `boxType: 'full_box'`。入口和 metadata 写法以
|
|
373
|
+
[开发规则](../rules/dos-and-donts.md) 为准。需要全宽展示时,优先选择模板库中的列表、订单、票务或媒体布局模板;
|
|
374
|
+
匹配不上时按模板能力或新设计需求确认。
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
// src/app.config.ts
|
|
378
|
+
import { defineAppConfig } from '@doubao-dev/framework/config';
|
|
379
|
+
|
|
380
|
+
export default defineAppConfig({
|
|
381
|
+
appId: 'db_xxxxxx', // 独立应用项目填写;Plugin CLI 项目不填写
|
|
382
|
+
name: '我的豆包应用',
|
|
383
|
+
widgets: [
|
|
384
|
+
{
|
|
385
|
+
entry: 'widgets/user-action-card/index',
|
|
386
|
+
id: 'user-action-card',
|
|
387
|
+
name: '全宽卡片示例',
|
|
388
|
+
boxType: 'full_box',
|
|
389
|
+
titleType: 'none'
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**3. custom_box - 自定义容器样式**
|
|
396
|
+
|
|
397
|
+
在 `src/app.config.ts` 的 Widget entry 上配置 `boxType: 'custom_box'`,构建后 Manifest 对应
|
|
398
|
+
`boxType: 4`。容器不添加默认背景、圆角和描边,由业务小程序的内容样式设置;标题栏仍由
|
|
399
|
+
`titleType` 控制。未配置时仍默认为 `inbox`。
|
|
400
|
+
|
|
401
|
+
### ViewData 类型
|
|
402
|
+
|
|
403
|
+
Widget viewData 通过 `useViewData<T>()` 的泛型参数和 TypeScript 接口表达。数据结构应尽量贴近所选模板的 props
|
|
404
|
+
或列表项类型。
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
import { useViewData } from '@doubao-dev/framework';
|
|
408
|
+
import { ContentCard, type ContentCardItem } from '@doubao-dev/template';
|
|
409
|
+
|
|
410
|
+
interface ProductCardData {
|
|
411
|
+
title: string;
|
|
412
|
+
imageSrc?: string;
|
|
413
|
+
price?: string;
|
|
414
|
+
quantity?: string;
|
|
415
|
+
infoRows?: ContentCardItem[];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export default function ProductCard() {
|
|
419
|
+
const viewData = useViewData<ProductCardData>();
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<ContentCard
|
|
423
|
+
items={[
|
|
424
|
+
{
|
|
425
|
+
title: viewData.title,
|
|
426
|
+
subtitle: [viewData.price, viewData.quantity].filter(Boolean).join(' | '),
|
|
427
|
+
thumbnailSrc: viewData.imageSrc
|
|
428
|
+
},
|
|
429
|
+
...(viewData.infoRows ?? [])
|
|
430
|
+
]}
|
|
431
|
+
/>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 常用模式
|
|
437
|
+
|
|
438
|
+
**1. 数据获取和展示**
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
import { useEffect, useState, useViewData } from '@doubao-dev/framework';
|
|
442
|
+
import { request } from '@doubao-dev/framework/api';
|
|
443
|
+
import { ContentCard, type ContentCardItem } from '@doubao-dev/template';
|
|
444
|
+
|
|
445
|
+
interface WeatherInfo {
|
|
446
|
+
city: string;
|
|
447
|
+
temperature: number;
|
|
448
|
+
condition: string;
|
|
449
|
+
windSpeed: number;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export default function WeatherWidget() {
|
|
453
|
+
const { city } = useViewData<{ city: string }>();
|
|
454
|
+
const [weather, setWeather] = useState<WeatherInfo | null>(null);
|
|
455
|
+
const [loading, setLoading] = useState(true);
|
|
456
|
+
const [error, setError] = useState('');
|
|
457
|
+
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
const fetchWeather = async () => {
|
|
460
|
+
try {
|
|
461
|
+
setError('');
|
|
462
|
+
const response = await request({
|
|
463
|
+
url: `https://api.example.com/weather?city=${city}`,
|
|
464
|
+
method: 'GET'
|
|
465
|
+
});
|
|
466
|
+
setWeather(response.data as WeatherInfo);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
setError(err instanceof Error ? err.message : '获取天气失败');
|
|
469
|
+
} finally {
|
|
470
|
+
setLoading(false);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
void fetchWeather();
|
|
475
|
+
}, [city]);
|
|
476
|
+
|
|
477
|
+
if (loading) return <ContentCard items={[{ title: '加载中...' }]} />;
|
|
478
|
+
if (error) return <ContentCard items={[{ title: error }]} />;
|
|
479
|
+
if (!weather) return <ContentCard items={[{ title: '暂无数据' }]} />;
|
|
480
|
+
|
|
481
|
+
const items: ContentCardItem[] = [
|
|
482
|
+
{ key: 'condition', title: weather.city, subtitle: weather.condition },
|
|
483
|
+
{ key: 'wind', title: '风速', subtitle: weather.windSpeed },
|
|
484
|
+
{ key: 'temperature', title: '温度', subtitle: `${weather.temperature}℃` }
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
return <ContentCard items={items} />;
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**2. 用户交互**
|
|
492
|
+
|
|
493
|
+
```tsx
|
|
494
|
+
import { useState } from '@doubao-dev/framework';
|
|
495
|
+
import { sendFollowUpMessage } from '@doubao-dev/framework/api';
|
|
496
|
+
import { AskHumanCard } from '@doubao-dev/template';
|
|
497
|
+
|
|
498
|
+
function InteractiveWidget({ initialCount }: { initialCount: number }) {
|
|
499
|
+
const [count, setCount] = useState(initialCount);
|
|
500
|
+
|
|
501
|
+
const handleIncrement = () => {
|
|
502
|
+
const nextCount = count + 1;
|
|
503
|
+
setCount(nextCount);
|
|
504
|
+
// 以用户身份发送后续消息,触发新一轮对话
|
|
505
|
+
void sendFollowUpMessage({
|
|
506
|
+
content: [{ type: 'text', text: `用户将计数增加到 ${nextCount},请基于新的计数继续回复` }]
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<AskHumanCard
|
|
512
|
+
variant="jump"
|
|
513
|
+
items={[
|
|
514
|
+
{
|
|
515
|
+
key: 'increment',
|
|
516
|
+
text: `增加到 ${count + 1}`,
|
|
517
|
+
showArrow: 'right',
|
|
518
|
+
onClick: handleIncrement
|
|
519
|
+
}
|
|
520
|
+
]}
|
|
521
|
+
/>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**3. 打开页面**
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
import { navigateTo } from '@doubao-dev/framework/api';
|
|
530
|
+
import { AskHumanCard } from '@doubao-dev/template';
|
|
531
|
+
|
|
532
|
+
function ActionWidget({ itemId }: { itemId: string }) {
|
|
533
|
+
const handleViewDetail = () => {
|
|
534
|
+
navigateTo({
|
|
535
|
+
url: `/pages/item-detail/index?id=${itemId}`
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<AskHumanCard
|
|
541
|
+
variant="jump"
|
|
542
|
+
items={[
|
|
543
|
+
{
|
|
544
|
+
key: 'view-detail',
|
|
545
|
+
text: '查看详情',
|
|
546
|
+
showArrow: 'right',
|
|
547
|
+
onClick: handleViewDetail
|
|
548
|
+
}
|
|
549
|
+
]}
|
|
550
|
+
/>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## 🎨 样式开发
|
|
558
|
+
|
|
559
|
+
### SCSS 文件组织
|
|
560
|
+
|
|
561
|
+
```scss
|
|
562
|
+
// 样式变量
|
|
563
|
+
$primary-color: #1890ff;
|
|
564
|
+
$text-color: #333;
|
|
565
|
+
$bg-color: #f5f5f5;
|
|
566
|
+
$border-radius: 8px;
|
|
567
|
+
$spacing: 16px;
|
|
568
|
+
|
|
569
|
+
// 容器样式
|
|
570
|
+
.container {
|
|
571
|
+
padding: $spacing;
|
|
572
|
+
background-color: $bg-color;
|
|
573
|
+
|
|
574
|
+
.header {
|
|
575
|
+
font-size: 36px;
|
|
576
|
+
color: $text-color;
|
|
577
|
+
margin-bottom: $spacing;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.content {
|
|
581
|
+
background-color: #fff;
|
|
582
|
+
border-radius: $border-radius;
|
|
583
|
+
padding: $spacing;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// 响应式布局
|
|
588
|
+
.responsive-layout {
|
|
589
|
+
display: flex;
|
|
590
|
+
flex-wrap: wrap;
|
|
591
|
+
gap: $spacing;
|
|
592
|
+
|
|
593
|
+
.item {
|
|
594
|
+
flex: 1 1 calc(50% - #{$spacing});
|
|
595
|
+
min-width: 200px;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### 常用布局模式
|
|
601
|
+
|
|
602
|
+
本节布局示例面向 Page 的普通内容区。Widget 卡片整体布局不要手写,先使用
|
|
603
|
+
[Widget 模板库](../widget-templates/overview.md) 中的模板。
|
|
604
|
+
|
|
605
|
+
承载 `button`、`input`、`switch` 等内置组件时,还要按 [开发规则](../rules/dos-and-donts.md) 检查组件最小宽度、换行和收缩。
|
|
606
|
+
|
|
607
|
+
**1. Flex 布局**
|
|
608
|
+
|
|
609
|
+
```scss
|
|
610
|
+
.flex-container {
|
|
611
|
+
display: flex;
|
|
612
|
+
flex-direction: row; // row | column
|
|
613
|
+
justify-content: center; // flex-start | center | flex-end | space-between
|
|
614
|
+
align-items: center; // flex-start | center | flex-end | stretch
|
|
615
|
+
gap: 16px;
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**2. Grid 布局**
|
|
620
|
+
|
|
621
|
+
```scss
|
|
622
|
+
.grid-container {
|
|
623
|
+
display: grid;
|
|
624
|
+
grid-template-columns: repeat(3, 1fr);
|
|
625
|
+
gap: 16px;
|
|
626
|
+
padding: 16px;
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**3. Page 内容块布局**
|
|
631
|
+
|
|
632
|
+
```scss
|
|
633
|
+
.content-block {
|
|
634
|
+
background-color: #fff;
|
|
635
|
+
border-radius: 8px;
|
|
636
|
+
padding: 24px;
|
|
637
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
638
|
+
margin-bottom: 16px;
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 🔌 View Data APIs
|
|
645
|
+
|
|
646
|
+
在 Page 和 Widget 开发中,通过 `useViewData<T>()` 获取当前视图数据。
|
|
647
|
+
|
|
648
|
+
获取当前视图的数据,支持泛型类型定义,确保类型安全。
|
|
649
|
+
|
|
650
|
+
**类型定义**:
|
|
651
|
+
```typescript
|
|
652
|
+
import { useViewData } from '@doubao-dev/framework';
|
|
653
|
+
|
|
654
|
+
function useViewData<T extends Record<string, any> = Record<string, any>>(): T
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**使用场景**:
|
|
658
|
+
|
|
659
|
+
1. **在入口组件和生命周期 Hook 中获取数据**:
|
|
660
|
+
```tsx
|
|
661
|
+
import { useMounted, useViewData } from '@doubao-dev/framework';
|
|
662
|
+
import { AskHumanCard } from '@doubao-dev/template';
|
|
663
|
+
|
|
664
|
+
interface TodoData {
|
|
665
|
+
tasks: string[];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export default function TodoWidget() {
|
|
669
|
+
const viewData = useViewData<TodoData>();
|
|
670
|
+
useMounted(() => console.log('任务列表:', viewData.tasks));
|
|
671
|
+
|
|
672
|
+
return (
|
|
673
|
+
<AskHumanCard
|
|
674
|
+
variant="jump"
|
|
675
|
+
items={[
|
|
676
|
+
{
|
|
677
|
+
key: 'task-count',
|
|
678
|
+
text: `${viewData.tasks.length} 个任务`,
|
|
679
|
+
showArrow: 'right'
|
|
680
|
+
}
|
|
681
|
+
]}
|
|
682
|
+
/>
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
2. **在自定义函数中访问数据**:
|
|
688
|
+
```tsx
|
|
689
|
+
import { useViewData } from '@doubao-dev/framework';
|
|
690
|
+
import { ContentCard } from '@doubao-dev/template';
|
|
691
|
+
|
|
692
|
+
interface WeatherData {
|
|
693
|
+
city: string;
|
|
694
|
+
temperature: number;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function WeatherWidget() {
|
|
698
|
+
const viewData = useViewData<WeatherData>();
|
|
699
|
+
|
|
700
|
+
const loadWeatherIcon = () => {
|
|
701
|
+
return viewData.temperature > 25 ? '☀️' : '❄️';
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
return <ContentCard items={[{ title: loadWeatherIcon() }]} />;
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
**注意事项**:
|
|
709
|
+
- ✅ **推荐**:在入口组件函数中使用 `useViewData<T>()` 获取数据
|
|
710
|
+
- ✅ **推荐**:生命周期 Hook 可以闭包访问入口组件读取的数据
|
|
711
|
+
- 🔒 **类型安全**:始终定义明确的 TypeScript 接口并传递给泛型参数
|
|
712
|
+
|
|
713
|
+
## 🔧 开发最佳实践
|
|
714
|
+
|
|
715
|
+
### 1. TypeScript 类型定义
|
|
716
|
+
|
|
717
|
+
```tsx
|
|
718
|
+
// 定义 props 类型
|
|
719
|
+
interface ComponentProps {
|
|
720
|
+
title: string;
|
|
721
|
+
count?: number;
|
|
722
|
+
onAction: (value: string) => void;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// 定义 state 类型
|
|
726
|
+
interface ComponentState {
|
|
727
|
+
loading: boolean;
|
|
728
|
+
data: DataType | null;
|
|
729
|
+
error: string | null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function MyComponent({ title, count = 0, onAction }: ComponentProps) {
|
|
733
|
+
const [state, setState] = useState<ComponentState>({
|
|
734
|
+
loading: false,
|
|
735
|
+
data: null,
|
|
736
|
+
error: null
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
return <view>{/* 组件内容 */}</view>;
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### 2. 错误处理
|
|
744
|
+
|
|
745
|
+
```tsx
|
|
746
|
+
try {
|
|
747
|
+
// 异步操作
|
|
748
|
+
const result = await someAsyncOperation();
|
|
749
|
+
setData(result);
|
|
750
|
+
} catch (err) {
|
|
751
|
+
const message = err instanceof Error ? err.message : '未知错误';
|
|
752
|
+
// 记录错误
|
|
753
|
+
console.error('操作失败:', message);
|
|
754
|
+
|
|
755
|
+
// 显示用户友好的错误消息
|
|
756
|
+
setError('操作失败,请重试');
|
|
757
|
+
|
|
758
|
+
// 可选:上报错误
|
|
759
|
+
reportAppLog({
|
|
760
|
+
level: 'error',
|
|
761
|
+
message,
|
|
762
|
+
context: { operation: 'someAsyncOperation' }
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### 3. 性能优化
|
|
768
|
+
|
|
769
|
+
```tsx
|
|
770
|
+
import { useMemo, useCallback } from '@doubao-dev/framework';
|
|
771
|
+
|
|
772
|
+
function OptimizedComponent({ data, onSelect }) {
|
|
773
|
+
// 缓存计算结果
|
|
774
|
+
const processedData = useMemo(() => {
|
|
775
|
+
return data.map(item => ({
|
|
776
|
+
...item,
|
|
777
|
+
computed: expensiveComputation(item)
|
|
778
|
+
}));
|
|
779
|
+
}, [data]);
|
|
780
|
+
|
|
781
|
+
// 缓存回调函数
|
|
782
|
+
const handleClick = useCallback((id: string) => {
|
|
783
|
+
onSelect(id);
|
|
784
|
+
}, [onSelect]);
|
|
785
|
+
|
|
786
|
+
return (
|
|
787
|
+
<view>
|
|
788
|
+
{processedData.map(item => (
|
|
789
|
+
<view key={item.id} onClick={() => handleClick(item.id)}>
|
|
790
|
+
{item.computed}
|
|
791
|
+
</view>
|
|
792
|
+
))}
|
|
793
|
+
</view>
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### 4. 生命周期管理
|
|
799
|
+
|
|
800
|
+
```tsx
|
|
801
|
+
useEffect(() => {
|
|
802
|
+
// 订阅
|
|
803
|
+
const subscription = subscribeEvent('data-update', handleDataUpdate);
|
|
804
|
+
|
|
805
|
+
// 定时器
|
|
806
|
+
const timer = setInterval(() => {
|
|
807
|
+
fetchData();
|
|
808
|
+
}, 5000);
|
|
809
|
+
|
|
810
|
+
// 清理函数
|
|
811
|
+
return () => {
|
|
812
|
+
unsubscribeEvent(subscription);
|
|
813
|
+
clearInterval(timer);
|
|
814
|
+
};
|
|
815
|
+
}, []);
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## 📚 延伸阅读
|
|
821
|
+
|
|
822
|
+
- **Page/Widget 基础示例** → [../examples/page-widget-basics.md](../examples/page-widget-basics.md)
|
|
823
|
+
- **常用模式** → [../examples/common-patterns.md](../examples/common-patterns.md)
|
|
824
|
+
- **登录与授权约定入口** → [auth.md](./auth.md)
|
|
825
|
+
- **失效卡片** → [expired-widget.md](./expired-widget.md)
|
|
826
|
+
- **豆包应用插件的端能力 API 使用配方** → [../examples/doubao-agentic-service-api-recipes.md](../examples/doubao-agentic-service-api-recipes.md)
|
|
827
|
+
- **Page 组件配方** → [../examples/page-widget-recipes.md](../examples/page-widget-recipes.md)
|
|
828
|
+
- **最佳实践** → [best-practices.md](./best-practices.md)
|
|
829
|
+
- **性能优化** → [performance-optimization.md](./performance-optimization.md)
|
|
830
|
+
- **故障排查** → [troubleshooting.md](./troubleshooting.md)
|
|
831
|
+
- **Framework 核心入口、生命周期和 Hooks** → [../framework/core.md](../framework/core.md)
|
|
832
|
+
- **豆包应用插件的端能力 API 速查** → [../doubao-agentic-service-api/quick-reference.md](../doubao-agentic-service-api/quick-reference.md)
|