HarmonyOS7 ArkUI 代码片段管理 - 收藏、语言分类、复制功能实战:把案例真正写懂
文章目录
前言

这类页面我一般会拿来练业务感。它不是纯展示页,用户点一下之后,界面应该马上给反馈,代码组织也不能散。
这个案例的主线是 代码片段管理,后面的 收藏、语言分类、复制功能 则把页面做得更像真实业务页。读代码时,建议把交互、状态和列表这三条线一起看。
这个页面适合练什么
CodeSnippetManagerPage 对应的是 代码片段管理 - 收藏、语言分类、复制功能 这个业务场景。别把它只当成一个 UI 练习页,它真正有价值的地方在于:同一个页面里同时出现了状态切换、列表渲染、条件展示和用户反馈。
| 观察点 | 在这个案例里的表现 |
|---|---|
| 页面主题 | 代码片段管理 |
| 主要文案 | ArkTS 防抖函数, 防止按钮短时间内重复点击的通用工具, 工具, 防抖, 性能, Flex 居中布局 |
| 常用组件 | Column, Flex, ForEach, Image, Row, Scroll, Stack, Text, TextInput |
| 适合练习 | 状态驱动 UI、局部刷新、事件回调、页面结构拆分 |
使用方法
把完整代码放进 ArkTS 页面文件后,就可以直接在预览器里运行。实际使用时,建议先手动点一遍页面里的主要交互,看状态有没有跟着变化;然后再去对照代码,理解每个 @State 字段到底控制了哪一块区域。
如果你想把这个案例改成自己的版本,我建议先做三件事:换掉模拟数据、调整筛选规则、再把重复结构抽成 Builder 或子组件。顺序别反,先改结构往往最容易把自己绕进去。
状态设计别忽略
ArkUI 页面写顺手之后,你会发现很多问题其实都不是样式问题,而是状态没放对位置。这个案例里能直接看到哪些数据是输入、哪些是选择、哪些是列表源,读起来会比较顺。
| 状态字段 | 类型 | 说明 |
|---|---|---|
snippets |
CodeSnippet[] |
记录当前展示状态或页面选择 |
searchText |
string |
用户输入值,会直接参与计算、筛选或提交 |
selectedLang |
string |
当前选中项,决定内容切换、高亮或过滤结果 |
showFavoriteOnly |
boolean |
布尔状态,控制弹层、模式或显示隐藏 |
expandedId |
number |
记录当前展示状态或页面选择 |
我自己看这类示例时,会先把
@State和事件函数圈出来,再去看布局。这样不会被大段Column、Row搞乱节奏。
关键代码单独看
片段 1:getLangs(): string[] {

这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。
getLangs(): string[] {
const langs = ['全部']
this.snippets.forEach((s: CodeSnippet) => {
if (!langs.includes(s.language)) langs.push(s.language)
})
return langs
}
片段 2:getFiltered(): CodeSnippet[] {
这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。
getFiltered(): CodeSnippet[] {
return this.snippets.filter((s: CodeSnippet) => {
const matchSearch = s.title.includes(this.searchText) || s.description.includes(this.searchText)
const matchLang = this.selectedLang === '全部' || s.language === this.selectedLang
const matchFav = !this.showFavoriteOnly || s.isFavorite
return matchSearch && matchLang && matchFav
})
}

必读小结
适合拿来练手的能力:页面拆分、状态联动、条件渲染、交互反馈。
推荐阅读顺序:先看前言 -> 再跑页面 -> 再看状态表 -> 最后啃完整代码
如果只想快速上手,优先改模拟数据和交互函数,收益最高
我会重点检查的几个地方
- 点击或输入之后,界面有没有立即更新
- 列表渲染是否依赖了稳定的数据结构
- 筛选、统计、派生数据是不是单独放进函数里
- 是否还有重复布局可以继续抽出去
- 文案、颜色、间距是不是同一套风格
完整代码
// CodeSnippetManagerPage - 代码片段管理:收藏、语言分类、复制功能
interface CodeSnippet {
id: number
title: string
description: string
language: string
code: string
tags: string[]
isFavorite: boolean
createTime: string
viewCount: number
}
@Entry
@Component
struct CodeSnippetManagerPage {
@State snippets: CodeSnippet[] = [
{
id: 1, title: 'ArkTS 防抖函数', description: '防止按钮短时间内重复点击的通用工具',
language: 'ArkTS',
code: 'function debounce(fn: Function, delay: number) {\n let timer: number = -1\n return (...args: Object[]) => {\n clearTimeout(timer)\n timer = setTimeout(() => fn(...args), delay)\n }\n}',
tags: ['工具', '防抖', '性能'], isFavorite: true, createTime: '2024-01-15', viewCount: 128
},
{
id: 2, title: 'Flex 居中布局', description: '水平垂直完全居中的 Flex 模板',
language: 'ArkTS',
code: 'Flex({\n direction: FlexDirection.Row,\n justifyContent: FlexAlign.Center,\n alignItems: ItemAlign.Center\n}) {\n Text("居中内容")\n}',
tags: ['布局', 'Flex', 'UI'], isFavorite: false, createTime: '2024-02-03', viewCount: 89
},
{
id: 3, title: 'LazyForEach 数据源', description: '大列表懒加载标准 IDataSource 实现',
language: 'ArkTS',
code: 'class BasicDataSource implements IDataSource {\n private dataArray: string[] = []\n totalCount(): number { return this.dataArray.length }\n getData(index: number): string { return this.dataArray[index] }\n registerDataChangeListener() {}\n unregisterDataChangeListener() {}\n}',
tags: ['性能', '列表', '懒加载'], isFavorite: true, createTime: '2024-02-20', viewCount: 210
},
{
id: 4, title: 'animateTo 淡入', description: '通用组件淡入出现动画',
language: 'ArkTS',
code: 'animateTo(\n { duration: 300, curve: Curve.EaseOut },\n () => { this.opacity = 1 }\n)',
tags: ['动画', 'UI', '效果'], isFavorite: false, createTime: '2024-03-01', viewCount: 67
},
{
id: 5, title: 'HTTP 请求封装', description: '基于 @ohos.net.http 的 GET/POST 封装',
language: 'ArkTS',
code: 'import http from "@ohos.net.http"\nconst req = http.createHttp()\nreq.request(url, {\n method: http.RequestMethod.GET,\n header: { "Content-Type": "application/json" }\n}, (err, data) => {\n if (!err) { console.log(data.result) }\n})',
tags: ['网络', 'HTTP', '请求'], isFavorite: true, createTime: '2024-03-10', viewCount: 305
},
{
id: 6, title: 'Stack 浮层徽章', description: '右上角红点/数字徽章标准写法',
language: 'ArkTS',
code: 'Stack({ alignContent: Alignment.TopEnd }) {\n Image($r("app.media.icon")).width(40)\n Text("3")\n .fontSize(10).fontColor(Color.White)\n .width(16).height(16)\n .backgroundColor(Color.Red)\n .borderRadius(8)\n .textAlign(TextAlign.Center)\n}',
tags: ['UI', '徽章', 'Stack'], isFavorite: false, createTime: '2024-03-18', viewCount: 156
}
]
@State searchText: string = ''
@State selectedLang: string = '全部'
@State showFavoriteOnly: boolean = false
@State expandedId: number = -1
getLangs(): string[] {
const langs = ['全部']
this.snippets.forEach((s: CodeSnippet) => {
if (!langs.includes(s.language)) langs.push(s.language)
})
return langs
}
getFiltered(): CodeSnippet[] {
return this.snippets.filter((s: CodeSnippet) => {
const matchSearch = s.title.includes(this.searchText) || s.description.includes(this.searchText)
const matchLang = this.selectedLang === '全部' || s.language === this.selectedLang
const matchFav = !this.showFavoriteOnly || s.isFavorite
return matchSearch && matchLang && matchFav
})
}
toggleFavorite(id: number) {
this.snippets = this.snippets.map((s) => { if (s.id === id) { s.isFavorite = !s.isFavorite } return s })
}
build() {
Column() {
// 顶部
Row() {
Text('代码片段').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1A1A1A')
Blank()
Text(`${this.snippets.filter((s: CodeSnippet) => s.isFavorite).length} 收藏`)
.fontSize(13).fontColor('#FAAD14')
}.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })
// 搜索框
Row() {
Text('🔍').fontSize(16).margin({ right: 8 })
TextInput({ placeholder: '搜索标题、描述...', text: this.searchText })
.layoutWeight(1).fontSize(14).backgroundColor('transparent')
.onChange((val: string) => { this.searchText = val })
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor(Color.White)
.borderRadius(10)
.margin({ left: 16, right: 16, bottom: 10 })
// 语言筛选
Row() {
Scroll() {
Row({ space: 8 }) {
ForEach(this.getLangs(), (lang: string) => {
Text(lang)
.fontSize(13)
.fontColor(this.selectedLang === lang ? Color.White : '#595959')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.selectedLang === lang ? '#1890FF' : '#F0F0F0')
.borderRadius(16)
.onClick(() => { this.selectedLang = lang })
})
}.padding({ left: 16, right: 16 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
.layoutWeight(1)
Text(this.showFavoriteOnly ? '⭐' : '☆')
.fontSize(20).margin({ left: 8, right: 16 })
.onClick(() => { this.showFavoriteOnly = !this.showFavoriteOnly })
}.width('100%').margin({ bottom: 10 })
// 片段列表
Scroll() {
Column({ space: 10 }) {
ForEach(this.getFiltered(), (snippet: CodeSnippet) => {
Column() {
Row() {
Column() {
Row({ space: 6 }) {
Text(snippet.language)
.fontSize(11).fontColor('#1890FF')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor('#E6F7FF').borderRadius(10)
Text(snippet.title).fontSize(15).fontWeight(FontWeight.Medium).fontColor('#1A1A1A')
}
Text(snippet.description).fontSize(12).fontColor('#8C8C8C').margin({ top: 4 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text(snippet.isFavorite ? '⭐' : '☆').fontSize(20)
.onClick(() => this.toggleFavorite(snippet.id))
}.width('100%')
// 代码预览
if (this.expandedId === snippet.id) {
Text(snippet.code)
.fontSize(12).fontColor('#D4D4D4')
.fontFamily('monospace')
.backgroundColor('#1E1E1E')
.padding(12).borderRadius(8)
.width('100%').margin({ top: 10 })
.copyOption(CopyOptions.LocalDevice)
}
Row() {
ForEach(snippet.tags, (tag: string) => {
Text(`#${tag}`)
.fontSize(11).fontColor('#8C8C8C')
.margin({ right: 8 })
})
Blank()
Text(`${snippet.viewCount} 次查看`).fontSize(11).fontColor('#BFBFBF')
Text(this.expandedId === snippet.id ? '收起' : '展开')
.fontSize(12).fontColor('#1890FF').margin({ left: 12 })
.onClick(() => {
this.expandedId = this.expandedId === snippet.id ? -1 : snippet.id
})
}.width('100%').margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: '#0D000000', offsetX: 0, offsetY: 2 })
})
}.padding({ left: 16, right: 16, bottom: 24 })
}.layoutWeight(1)
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
}
写在最后
这类案例最值得学的,其实不是某一个控件怎么写,而是页面组织方式。把状态放对、把交互函数收好、把布局压简单,后面你接正式项目时会轻松很多。
更多推荐



所有评论(0)