ArkTS组件编写实战练习:从基础到高阶
ArkTS组件编写实战练习:从基础到高阶
一句话概括:本文通过六个循序渐进的项目练习——评分组件、下拉刷新列表、图片轮播、多选标签组、展开收起面板、可拖拽排序列表——系统训练 ArkTS 组件的拆分、组合、数据流和性能优化能力。
一、背景与意义
掌握语言语法和框架概念只是开始,真正写出高质量的 ArkTS 组件需要大量的刻意练习。组件化的核心能力包括:
- 拆解能力:将设计稿拆分为合理的组件层级
- 抽象能力:提取可复用的通用组件
- 数据流设计:选择正确的通信模式
- 性能意识:减少不必要的重渲染
本文设计的六个练习由浅入深,覆盖了组件开发的常见场景。
二、练习一:星级评分组件(StarRating)
难度:★☆☆☆☆
需求描述
实现一个可交互的星级评分组件,支持:
- 1-5 星评分
- 点击选择分数
- 半星/全星显示
- 支持自定义星数(3-10颗)
组件设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
interface StarRatingOptions {
totalStars: number; // 总星数(3-10)
rating: number; // 当前评分
allowHalf: boolean; // 是否允许半星
size: number; // 星星大小(px)
activeColor: Color; // 选中颜色
inactiveColor: Color; // 未选中颜色
}
@Component
struct StarRating {
private totalStars: number = 5;
@Link rating: number;
private allowHalf: boolean = false;
private size: number = 32;
private activeColor: Color = Color.Yellow;
private inactiveColor: Color = Color.Gray;
build() {
Row({ space: 4 }) {
ForEach(this.generateStars(), (star: { index: number, filled: number }) => {
// 使用 Canvas 绘制星形
Stack() {
// 背景星(灰色)
Canvas((context: CanvasRenderingContext2D) => {
this.drawStar(context, 0, 0, this.size / 2, star.filled > 0
? this.activeColor : this.inactiveColor);
})
.width(this.size)
.height(this.size)
.onClick(() => {
this.handleStarClick(star.index);
})
}
}, (star: { index: number }) => star.index.toString())
}
}
private generateStars(): { index: number, filled: number }[] {
const result: { index: number, filled: number }[] = [];
for (let i = 1; i <= this.totalStars; i++) {
let filled = 0;
if (this.rating >= i) {
filled = 1;
} else if (this.rating >= i - 0.5 && this.allowHalf) {
filled = 0.5;
}
result.push({ index: i, filled });
}
return result;
}
private handleStarClick(starIndex: number) {
if (this.allowHalf) {
// 如果点击已选中的星,切换到半星
if (this.rating === starIndex) {
this.rating = starIndex - 0.5;
} else if (this.rating === starIndex - 0.5) {
this.rating = starIndex;
} else {
this.rating = starIndex;
}
} else {
this.rating = starIndex;
}
}
private drawStar(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, color: Color) {
// 绘制五角星
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 72 - 36) * Math.PI / 180;
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
const innerAngle = (i * 72) * Math.PI / 180;
const innerX = cx + r * 0.382 * Math.cos(innerAngle);
const innerY = cy + r * 0.382 * Math.sin(innerAngle);
ctx.lineTo(innerX, innerY);
}
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
}
// 使用示例
@Entry
@Component
struct ProductReview {
@State productRating: number = 3.5;
@State showRatingText: string = '';
private ratingTexts: string[] = ['很差', '较差', '一般', '较好', '非常好'];
build() {
Column({ space: 16 }) {
Text('商品评价').fontSize(22).fontWeight(FontWeight.Bold)
Row({ space: 12 }) {
StarRating({
rating: $productRating,
totalStars: 5,
allowHalf: true,
size: 36,
activeColor: '#FFD700' as unknown as Color
})
Text(`${this.productRating} 分`)
.fontSize(18)
}
Text('你对该商品的评分是:' +
this.ratingTexts[Math.min(Math.round(this.productRating) - 1, 4)])
.fontSize(16)
.fontColor(Color.Gray)
Text('点击星星评价')
.fontSize(14)
// 自定义星数版本
Divider()
Text('自定义版本(8星)')
StarRating({
rating: $productRating,
totalStars: 8,
allowHalf: false,
size: 24
})
}
.padding(24)
.width('100%')
}
}
练习要点
- @Link 的使用:评分值需要双向绑定
- Canvas 绘图:ArkTS 支持 Canvas API,底层渲染更高效
- 属性封装:通过组件参数暴露自定义选项
- 响应式计算:
generateStars()随rating变化自动触发
三、练习二:下拉刷新列表(PullToRefreshList)
难度:★★☆☆☆
需求描述
实现一个支持下拉刷新的列表组件,展示模拟的新闻列表数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
interface NewsItem {
id: string;
title: string;
summary: string;
timestamp: string;
category: string;
}
@Component
struct PullToRefreshList {
@State items: NewsItem[] = [];
@State isRefreshing: boolean = false;
@State pullDistance: number = 0;
@State listOffset: number = 0;
private threshold: number = 80;
aboutToAppear() {
this.loadInitialData();
}
private loadInitialData() {
this.items = [];
for (let i = 1; i <= 20; i++) {
this.items.push({
id: `news_${i}`,
title: `新闻标题 ${i}:鸿蒙生态持续发展`,
summary: `这是新闻${i}的摘要内容,展示了鸿蒙生态的最新动态和发展方向...`,
timestamp: `2026-05-${String(i).padStart(2, '0')}`,
category: i % 3 === 0 ? '科技' : i % 3 === 1 ? '财经' : '社会'
});
}
}
private async handleRefresh() {
this.isRefreshing = true;
// 模拟网络请求
await new Promise<void>((resolve) => {
setTimeout(() => {
this.loadInitialData();
resolve();
}, 1500);
});
this.isRefreshing = false;
this.pullDistance = 0;
}
build() {
Column() {
// 刷新指示器
if (this.pullDistance > 0 || this.isRefreshing) {
Row() {
if (this.isRefreshing) {
LoadingProgress()
.width(20)
.height(20)
.margin({ right: 8 })
Text('正在刷新...')
} else {
Text(this.pullDistance >= this.threshold
? '松开立即刷新' : '下拉刷新')
}
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height(50)
}
List() {
ForEach(this.items, (item: NewsItem) => {
ListItem() {
NewsCard({ news: item })
}
}, (item: NewsItem) => item.id)
}
.width('100%')
.layoutWeight(1)
.edgeEffect(EdgeEffect.None)
.onScrollIndex((firstIndex: number) => {
if (firstIndex <= 0 && !this.isRefreshing) {
this.handleRefresh();
}
})
}
.width('100%')
.padding(16)
}
}
@Component
struct NewsCard {
@Prop news: NewsItem;
build() {
Column() {
Row() {
Text(this.news.category)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor(this.getCategoryColor(this.news.category))
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(4)
Text(this.news.timestamp)
.fontSize(12)
.fontColor(Color.Gray)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Text(this.news.title)
.fontSize(17)
.fontWeight(FontWeight.Medium)
.margin({ top: 6 })
Text(this.news.summary)
.fontSize(14)
.fontColor(Color.Gray)
.margin({ top: 4 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 3, color: '#15000000' })
.margin({ bottom: 8 })
}
private getCategoryColor(category: string): Color {
switch (category) {
case '科技': return '#007AFF';
case '财经': return '#34C759';
case '社会': return '#FF9500';
default: return '#8E8E93';
}
}
}
练习要点
- List + ForEach 组合:理解列表性能优化
- 异步状态管理:
isRefreshing和async/await - LoadingProgress 组件:原生加载指示器
- State 驱动的 UI 条件渲染:下拉距离与刷新状态
四、练习三:图片轮播组件(ImageCarousel)
难度:★★☆☆☆
结合定时器和手势,实现自动轮播和手动滑动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Component
struct ImageCarousel {
private images: Resource[] = [
$r('app.media.banner_1'),
$r('app.media.banner_2'),
$r('app.media.banner_3'),
$r('app.media.banner_4'),
$r('app.media.banner_5'),
];
@State currentIndex: number = 0;
@State autoPlay: boolean = true;
private intervalId: number = -1;
private swipeX: number = 0;
aboutToAppear() {
this.startAutoPlay();
}
aboutToDisappear() {
this.stopAutoPlay();
}
private startAutoPlay() {
if (!this.autoPlay) return;
this.intervalId = setInterval(() => {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
}, 3000);
}
private stopAutoPlay() {
if (this.intervalId !== -1) {
clearInterval(this.intervalId);
this.intervalId = -1;
}
}
build() {
Column() {
// 轮播主体
Swiper() {
ForEach(this.images, (img: Resource) => {
Image(img)
.width('100%')
.height(200)
.objectFit(ImageFit.Cover)
.borderRadius(12)
}, (_img: Resource, index: number) => index.toString())
}
.width('100%')
.height(200)
.autoPlay(false) // 手动控制
.indicator(false) // 自定义指示器
.onChange((index: number) => {
// 手动滑动时重置自动轮播
this.currentIndex = index;
this.stopAutoPlay();
this.startAutoPlay();
})
// 自定义指示器
Row({ space: 6 }) {
ForEach(this.images, (_img: Resource, index: number) => {
Row()
.width(this.currentIndex === index ? 24 : 8)
.height(8)
.borderRadius(4)
.backgroundColor(
this.currentIndex === index ?
'#007AFF' : '#40000000'
)
.animation({ duration: 300 })
.onClick(() => {
this.currentIndex = index;
this.stopAutoPlay();
this.startAutoPlay();
})
}, (_img: Resource, index: number) => index.toString())
}
.margin({ top: 8 })
}
.width('100%')
}
}
练习要点
- Swiper 组件:ArkTS 内置轮播容器
- 生命周期管理:
aboutToAppear启动,aboutToDisappear清理 - 自定义指示器动画:
animation()API 的使用 - 手势覆盖:手动滑动时重置自动播放
五、练习四:多选标签组(TagSelector)
难度:★★★☆☆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@Observed
class TagOption {
label: string;
selected: boolean;
constructor(label: string) {
this.label = label;
this.selected = false;
}
}
@Component
struct TagGroup {
@Link selectedTags: TagOption[];
private maxSelect: number = 0; // 0 表示不限
private columns: number = 3;
build() {
Grid() {
ForEach(this.selectedTags, (tag: TagOption, index: number) => {
GridItem() {
TagChip({
tag: tag,
onToggle: () => this.handleToggle(index),
disabled: this.allowSelect(index)
})
}
}, (_tag: TagOption, index: number) => index.toString())
}
.columnsTemplate(`1fr 1fr 1fr`)
.rowsGap(8)
.columnsGap(8)
}
private allowSelect(index: number): boolean {
if (this.maxSelect === 0) return false;
if (this.selectedTags[index].selected) return false;
const selectedCount = this.selectedTags
.filter(t => t.selected).length;
return selectedCount >= this.maxSelect;
}
private handleToggle(index: number) {
if (this.allowSelect(index)) return;
this.selectedTags[index].selected =
!this.selectedTags[index].selected;
// 触发响应式更新
this.selectedTags = [...this.selectedTags];
}
}
@Component
struct TagChip {
@ObjectLink tag: TagOption;
private onToggle: () => void;
private disabled: boolean = false;
build() {
Row() {
Text(this.tag.label)
.fontSize(14)
.fontColor(this.tag.selected ? Color.White : Color.Black)
}
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor(this.tag.selected ? '#007AFF' :
this.disabled ? '#F0F0F0' : '#FFFFFF')
.borderRadius(20)
.border({
width: this.tag.selected ? 0 : 1,
color: '#D0D0D0'
})
.onClick(() => {
if (!this.disabled) {
this.onToggle();
}
})
.animation({ duration: 200 })
}
}
@Entry
@Component
struct PreferencePage {
@State hobbies: TagOption[] = [
new TagOption('阅读'),
new TagOption('运动'),
new TagOption('音乐'),
new TagOption('摄影'),
new TagOption('烹饪'),
new TagOption('旅行'),
new TagOption('编程'),
new TagOption('绘画'),
new TagOption('游戏'),
];
build() {
Column({ space: 16 }) {
Text('兴趣标签(最多选5个)').fontSize(18).fontWeight(FontWeight.Bold)
Text(`已选: ${this.hobbies.filter(t => t.selected).length}/5`)
TagGroup({
selectedTags: $hobbies,
maxSelect: 5,
columns: 3
})
Button('保存偏好')
.enabled(this.hobbies.some(t => t.selected))
.width('100%')
.onClick(() => {
const selected = this.hobbies
.filter(t => t.selected)
.map(t => t.label)
.join(', ');
console.info(`用户选择了: ${selected}`);
})
}
.padding(24)
.width('100%')
}
}
练习要点
- @Observed + @ObjectLink:对象数组的响应式处理
- Grid 布局:网格布局与标签对齐
- 条件禁用:maxSelect 逻辑与 disabled 状态
- 动画过渡:选中/未选中的切换动画
六、练习五:展开收起面板(ExpandablePanel)
难度:★★★☆☆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
@Component
struct ExpandableSection {
@Prop title: string;
@State isExpanded: boolean = false;
@BuilderParam content: () => void;
private animationDuration: number = 300;
build() {
Column() {
// 标题栏(始终可见)
Row() {
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
Image($r('app.media.arrow_down'))
.width(16)
.height(16)
.rotate({ angle: this.isExpanded ? 180 : 0 })
.animation({
duration: this.animationDuration,
curve: Curve.EaseOut
})
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius({
topLeft: 12,
topRight: 12,
bottomLeft: this.isExpanded ? 0 : 12,
bottomRight: this.isExpanded ? 0 : 12
})
.onClick(() => {
this.isExpanded = !this.isExpanded;
})
// 内容区域(展开/收起)
if (this.isExpanded) {
Column() {
this.content()
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius({
bottomLeft: 12,
bottomRight: 12
})
.transition({
type: TransitionType.Insert,
opacity: 0,
translate: { y: -16 }
})
.transition({
type: TransitionType.Delete,
opacity: 0,
translate: { y: -16 }
})
}
}
.margin({ bottom: 8 })
}
}
// 使用示例
@Entry
@Component
struct FAQPage {
build() {
Column({ space: 8 }) {
Text('常见问题').fontSize(22).fontWeight(FontWeight.Bold)
ExpandableSection({
title: '鸿蒙 App 如何申请上架?',
content: () => {
Column() {
Text('通过 DevEco Studio 打包后,在 AppGallery Connect 中...')
.fontSize(14)
.fontColor('#666666')
.lineHeight(22)
Text('\n1. 注册开发者账号')
Text('2. 创建应用信息并配置签名')
Text('3. 提交审核资料')
Text('4. 等待审核通过后发布')
}
}
})
ExpandableSection({
title: 'ArkTS 和 eTS 有什么区别?',
content: () => {
Column() {
Text('ArkTS 是鸿蒙原生应用开发语言,基于 TypeScript 扩展...')
.fontSize(14)
.fontColor('#666666')
.lineHeight(22)
Text('\n主要差异:类型系统增强、装饰器支持、运行时优化')
}
}
})
}
.padding(24)
.width('100%')
}
}
练习要点
- @BuilderParam:插槽模式的内容自定义
- transition 动画:插入/删除过渡效果
- 条件渲染的可访问性:内容展开/收起
- borderRadius 动态控制:根据状态切换圆角
七、练习六:可拖拽排序列表(DraggableList)
难度:★★★★☆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
@Observed
class SortableItem {
id: string;
label: string;
order: number;
constructor(id: string, label: string, order: number) {
this.id = id;
this.label = label;
this.order = order;
}
}
@Component
struct DraggableListItem {
@ObjectLink item: SortableItem;
@State isDragging: boolean = false;
private onDragStart?: () => void;
private onDrop?: (targetId: string) => void;
build() {
Row() {
Image($r('app.media.drag_handle'))
.width(24)
.height(24)
.margin({ right: 12 })
Text(this.item.label)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
Text(`#${this.item.order}`)
.fontSize(12)
.fontColor(Color.Gray)
}
.padding(16)
.backgroundColor(this.isDragging ?
'#E8F0FE' : Color.White)
.borderRadius(12)
.shadow({
radius: this.isDragging ? 8 : 2,
color: this.isDragging ? '#30000000' : '#10000000'
})
.margin({ bottom: 8 })
.animation({ duration: 150 })
.gesture(
PanGesture({ direction: PanDirection.Vertical })
.onActionStart(() => {
this.isDragging = true;
this.onDragStart?.();
})
.onActionUpdate((event: GestureEvent) => {
// 处理拖动偏移
})
.onActionEnd(() => {
this.isDragging = false;
})
)
}
}
@Entry
@Component
struct SortableListPage {
@State items: SortableItem[] = [];
@State dragIndex: number = -1;
aboutToAppear() {
const labels = ['前端开发', '后端开发', '移动开发',
'数据分析', 'UI设计', '产品经理', '测试工程'];
this.items = labels.map((label, i) =>
new SortableItem(`item_${i}`, label, i + 1)
);
}
private moveItem(fromIndex: number, toIndex: number) {
if (fromIndex === toIndex) return;
const item = this.items[fromIndex];
this.items.splice(fromIndex, 1);
this.items.splice(toIndex, 0, item);
// 更新排序号
this.items.forEach((it, i) => {
it.order = i + 1;
});
this.items = [...this.items]; // 触发响应式
}
build() {
Column({ space: 16 }) {
Text('职务排序(长按拖动)')
.fontSize(18).fontWeight(FontWeight.Bold)
Text('拖动调整您关注的职务领域优先级')
.fontSize(14).fontColor(Color.Gray)
Column() {
ForEach(this.items, (item: SortableItem, index: number) => {
DraggableListItem({
item: item,
onDragStart: () => {
this.dragIndex = index;
},
onDrop: (targetId: string) => {
const targetIndex = this.items
.findIndex(it => it.id === targetId);
if (targetIndex !== -1) {
this.moveItem(this.dragIndex, targetIndex);
}
this.dragIndex = -1;
}
})
}, (item: SortableItem) => item.id)
}
Divider()
Text('当前排序:')
Text(this.items.map((it, i) => `${i + 1}.${it.label}`).join(' → '))
.fontSize(14)
.fontColor(this.items.length > 0 ? '#007AFF' : Color.Gray)
}
.padding(24)
.width('100%')
}
}
练习要点
- PanGesture:手势识别与响应
- splice 数据重排:数组操作触发响应式更新
- 状态驱动的样式变化:
isDragging实时视觉反馈 - shadow 动画:拖起时加深阴影
八、高频面试题解析
Q1:如何判断一个组件应该拆分为独立组件?
答: 三个标准:1)代码能否独立测试?2)是否有自己的状态?3)是否被多处复用?符合任意两条就值得拆分。
Q2:组件太多会降低性能吗?
答: 组件实例化本身有成本,但通常可以忽略。真正影响性能的是:每个组件的 build() 中做了过多计算,以及状态变更引发了不必要的全量重建。关键优化点:使用 @ObjectLink 替代 @Prop 减少拷贝,使用 keyGenerator 优化 ForEach 复用。
Q3:如何实现组件级别的代码复用(不含 UI)?
答: 使用 ArkTS 的普通类和函数。将纯逻辑(数据转换、API 调用、验证)提取到独立的 .ts 文件中,在组件中 import 使用。ArkTS 不支持 mixin 模式,但支持组合优于继承——通过 @BuilderParam 实现 UI 插槽复用。
九、总结与扩展
这六个练习覆盖了 ArkTS 组件开发的常见模式:
| 练习 | 核心技能 | 技术点 |
|---|---|---|
| 评分组件 | 状态双向绑定 | @Link, Canvas, 属性暴露 |
| 下拉刷新 | 列表与异步 | List, ForEach, async/await |
| 图片轮播 | 定时器+手势 | Swiper, interval, 动画 |
| 标签选择 | 响应式对象 | @Observed, @ObjectLink, Grid |
| 展开收起 | 插槽+过渡 | @BuilderParam, transition |
| 拖拽排序 | 手势+数组操作 | PanGesture, splice, array |
进阶方向: 掌握基础组件后,可以挑战更复杂的场景:
- 虚拟滚动列表(处理 10 万级数据)
- 可编辑表格组件
- 表单生成器(JSON 驱动的动态表单)
- 可视化图表组件(Canvas 高级绘图)
扩展阅读:
- HarmonyOS 自定义组件官方指南
- ArkTS 手势处理详解
- @Builder 和 @BuilderParam 的使用规范
- 高性能列表优化(LazyForEach)
本文由作者按照 CC BY 4.0 进行授权