HarmonyOS7 ArkUI 社区邻里应用 - 公告、互助、二手市场实战:把案例真正写懂
前言
如果你想练 HarmonyOS7 页面开发,我反而建议先从这种中等复杂度的页面下手,信息量够,但不至于把人劝退。
这个案例的主线是 社区邻里应用,后面的 公告、互助、二手市场 则把页面做得更像真实业务页。读代码时,建议把交互、状态和列表这三条线一起看。
它解决了什么事
NeighborhoodCommunityPage 对应的是 社区邻里应用 - 公告、互助、二手市场 这个业务场景。别把它只当成一个 UI 练习页,它真正有价值的地方在于:同一个页面里同时出现了状态切换、列表渲染、条件展示和用户反馈。
| 观察点 | 在这个案例里的表现 |
|---|---|
| 页面主题 | 社区邻里应用 |
| 主要文案 | 【重要】电梯维修停用通知, 业委会换届选举公告, 停车位租赁信息, 小区绿化改造施工通知, 求助:有没有五月份的驾照考试题库, 最近在准备科目二,想要找最新的模拟题练习 |
| 常用组件 | Column, Divider, ForEach, Row, Scroll, Text |
| 适合练习 | 状态驱动 UI、局部刷新、事件回调、页面结构拆分 |

使用方法
把完整代码放进 ArkTS 页面文件后,就可以直接在预览器里运行。实际使用时,建议先手动点一遍页面里的主要交互,看状态有没有跟着变化;然后再去对照代码,理解每个 @State 字段到底控制了哪一块区域。
如果你想把这个案例改成自己的版本,我建议先做三件事:换掉模拟数据、调整筛选规则、再把重复结构抽成 Builder 或子组件。顺序别反,先改结构往往最容易把自己绕进去。
交互入口怎么拆
ArkUI 页面写顺手之后,你会发现很多问题其实都不是样式问题,而是状态没放对位置。这个案例里能直接看到哪些数据是输入、哪些是选择、哪些是列表源,读起来会比较顺。
| 状态字段 | 类型 | 说明 |
|---|---|---|
activeTab |
number |
当前选中项,决定内容切换、高亮或过滤结果 |
announcements |
AnnouncementItem[] |
记录当前展示状态或页面选择 |
helpItems |
HelpRequest[] |
列表数据源,通常会交给 ForEach 或卡片区域渲染 |
secondHandItems |
SecondHandItem[] |
列表数据源,通常会交给 ForEach 或卡片区域渲染 |
我自己看这类示例时,会先把
@State和事件函数圈出来,再去看布局。这样不会被大段Column、Row搞乱节奏。
关键代码说明
片段 1:toggleWanted(id: number) {
这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。

toggleWanted(id: number) {
this.secondHandItems = this.secondHandItems.map((item) => { if (item.id === id) { item.isWanted = !item.isWanted } return item })
}
必读小结
适合拿来练手的能力:页面拆分、状态联动、条件渲染、交互反馈。
推荐阅读顺序:先看前言 -> 再跑页面 -> 再看状态表 -> 最后啃完整代码
如果只想快速上手,优先改模拟数据和交互函数,收益最高
我会重点检查的几个地方
- 点击或输入之后,界面有没有立即更新
- 列表渲染是否依赖了稳定的数据结构
- 筛选、统计、派生数据是不是单独放进函数里
- 是否还有重复布局可以继续抽出去
- 文案、颜色、间距是不是同一套风格
完整代码
// NeighborhoodCommunityPage - 社区邻里应用 - 公告、互助、二手市场
interface AnnouncementItem {
id: number
title: string
content: string
type: string
time: string
important: boolean
}
interface HelpRequest {
id: number
title: string
desc: string
author: string
building: string
time: string
status: string
category: string
replies: number
}
interface SecondHandItem {
id: number
title: string
price: number
originalPrice: number
condition: string
author: string
building: string
time: string
color: string
isWanted: boolean
}
@Entry
@Component
struct NeighborhoodCommunityPage {
@State activeTab: number = 0
@State announcements: AnnouncementItem[] = [
{ id: 1, title: '【重要】电梯维修停用通知', content: '1号楼A栋电梯将于6月8日8:00-18:00进行年度维修,期间请使用1号楼B栋电梯,不便之处敬请谅解。', type: '维修', time: '1小时前', important: true },
{ id: 2, title: '业委会换届选举公告', content: '根据小区章程,业委会任期届满,现启动新一届换届选举工作,欢迎业主踊跃参与。', type: '通知', time: '昨天', important: false },
{ id: 3, title: '停车位租赁信息', content: '地下车库B区有5个月租车位出租,月租500元,有意者请联系物业前台。', type: '租赁', time: '2天前', important: false },
{ id: 4, title: '小区绿化改造施工通知', content: '小区中心花园将于本月15日开始绿化改造,为期10天,施工期间请注意避让。', type: '施工', time: '3天前', important: false },
]
@State helpItems: HelpRequest[] = [
{ id: 1, title: '求助:有没有五月份的驾照考试题库', desc: '最近在准备科目二,想要找最新的模拟题练习', author: '李明', building: '3栋601', time: '30分钟前', status: 'open', category: '求助', replies: 5 },
{ id: 2, title: '求换:有没有人有充气泵可以借用?', desc: '自行车轮胎跑气了,需要充气泵借用一下', author: '王芳', building: '2栋301', time: '2小时前', status: 'resolved', category: '借用', replies: 3 },
{ id: 3, title: '拼单:一起拼买有机蔬菜', desc: '本周六上午去有机农场采购,有意愿一起拼单的邻居可以联系我', author: '张华', building: '1栋201', time: '昨天', status: 'open', category: '拼单', replies: 12 },
{ id: 4, title: '提醒:快递代收箱位置变更', desc: '快递代收箱已搬到南门旁边,之前放在北门的邻居注意一下', author: '赵强', building: '4栋501', time: '昨天', status: 'open', category: '提醒', replies: 0 },
]
@State secondHandItems: SecondHandItem[] = [
{ id: 1, title: '9成新IKEA实木书桌', price: 350, originalPrice: 899, condition: '9成新', author: '陈家', building: '1栋802', time: '1小时前', color: '#74b9ff', isWanted: false },
{ id: 2, title: '婴儿推车 用了8个月', price: 200, originalPrice: 1500, condition: '8成新', author: '刘家', building: '2栋402', time: '3小时前', color: '#fd79a8', isWanted: true },
{ id: 3, title: '跑步机 欢迎试用', price: 800, originalPrice: 3000, condition: '7成新', author: '周先生', building: '3栋102', time: '昨天', color: '#00b894', isWanted: false },
{ id: 4, title: '全套厨具 搬家出售', price: 150, originalPrice: 400, condition: '6成新', author: '吴家', building: '5栋701', time: '2天前', color: '#a29bfe', isWanted: false },
{ id: 5, title: '儿童自行车 16寸', price: 120, originalPrice: 380, condition: '8成新', author: '孙家', building: '2栋301', time: '2天前', color: '#fdcb6e', isWanted: false },
]
private tabs: string[] = ['公告', '互助', '二手市场']
private typeColors: Record<string, string> = {
'维修': '#ff4d4f',
'通知': '#1890ff',
'租赁': '#52c41a',
'施工': '#fa8c16',
}
private categoryColors: Record<string, string> = {
'求助': '#1890ff',
'借用': '#52c41a',
'拼单': '#722ed1',
'提醒': '#fa8c16',
}
toggleWanted(id: number) {
this.secondHandItems = this.secondHandItems.map((item) => { if (item.id === id) { item.isWanted = !item.isWanted } return item })
}
build() {
Column({ space: 0 }) {
// 顶部
Row() {
Column({ space: 2 }) {
Text('幸福小区').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')
Text('3栋 · 1001室').fontSize(12).fontColor('#aaaaaa')
}
.alignItems(HorizontalAlign.Start)
Blank()
Text('🔔').fontSize(24)
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 12 })
.backgroundColor('#ffffff')
// Tab
Row() {
ForEach(this.tabs, (tab: string, idx: number) => {
Column() {
Text(tab)
.fontSize(15)
.fontWeight(this.activeTab === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.activeTab === idx ? '#1890ff' : '#888888')
.padding({ top: 10, bottom: 8 })
Divider().strokeWidth(2.5).color(this.activeTab === idx ? '#1890ff' : 'transparent')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.activeTab = idx })
})
}
.backgroundColor('#ffffff')
.border({ width: { bottom: 0.5 }, color: '#f0f0f0' })
Scroll() {
Column({ space: 12 }) {
if (this.activeTab === 0) {
// 公告列表
ForEach(this.announcements, (item: AnnouncementItem) => {
Column({ space: 8 }) {
Row({ space: 8 }) {
Text(item.type)
.fontSize(11).fontColor('#ffffff')
.backgroundColor(this.typeColors[item.type] ?? '#aaaaaa')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(4)
if (item.important) {
Text('重要').fontSize(11).fontColor('#ff4d4f').fontWeight(FontWeight.Bold)
}
Blank()
Text(item.time).fontSize(12).fontColor('#aaaaaa')
}
.width('100%')
Text(item.title)
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1a1a1a').width('100%')
Text(item.content)
.fontSize(13).fontColor('#666666').lineHeight(20).width('100%').maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.padding(16)
.backgroundColor('#ffffff')
.borderRadius(12)
.border({ width: item.important ? 2 : 0, color: '#ff4d4f20' })
})
} else if (this.activeTab === 1) {
// 互助列表
ForEach(this.helpItems, (item: HelpRequest) => {
Column({ space: 8 }) {
Row({ space: 8 }) {
Text(item.category)
.fontSize(11).fontColor('#ffffff')
.backgroundColor(this.categoryColors[item.category] ?? '#aaaaaa')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(4)
Text(item.status === 'resolved' ? '✓ 已解决' : '待回应')
.fontSize(11)
.fontColor(item.status === 'resolved' ? '#52c41a' : '#fa8c16')
.fontWeight(FontWeight.Medium)
Blank()
Text(item.time).fontSize(12).fontColor('#aaaaaa')
}
.width('100%')
Text(item.title)
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1a1a1a').width('100%')
Text(item.desc)
.fontSize(13).fontColor('#666666').width('100%').maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(`👤 ${item.author} · ${item.building}`)
.fontSize(12).fontColor('#999999')
Blank()
Text(`💬 ${item.replies}条回复`)
.fontSize(12).fontColor('#1890ff')
}
.width('100%')
}
.padding(16)
.backgroundColor('#ffffff')
.borderRadius(12)
})
} else {
// 二手市场
ForEach(this.secondHandItems, (item: SecondHandItem) => {
Row({ space: 12 }) {
Column()
.width(80).height(80)
.backgroundColor(item.color + '40')
.borderRadius(10)
Column({ space: 6 }) {
Text(item.title)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 6 }) {
Text(`¥${item.price}`)
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#ff4d4f')
Text(`原¥${item.originalPrice}`)
.fontSize(12).fontColor('#cccccc')
.decoration({ type: TextDecorationType.LineThrough })
}
Row({ space: 6 }) {
Text(item.condition)
.fontSize(11).fontColor('#52c41a').backgroundColor('#f0fff4')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8)
Text(`📍 ${item.building}`)
.fontSize(11).fontColor('#aaaaaa')
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column({ space: 8 }) {
Text(item.isWanted ? '❤️' : '🤍')
.fontSize(22)
.onClick(() => this.toggleWanted(item.id))
Text(item.time).fontSize(10).fontColor('#cccccc')
}
.alignItems(HorizontalAlign.Center)
}
.padding(16)
.backgroundColor('#ffffff')
.borderRadius(12)
})
}
Column().height(24)
}
.padding({ left: 16, right: 16, top: 12 })
}
.layoutWeight(1)
.backgroundColor('#f5f7fa')
}
.width('100%')
.height('100%')
.backgroundColor('#f5f7fa')
}
}

落地建议
这类案例最值得学的,其实不是某一个控件怎么写,而是页面组织方式。把状态放对、把交互函数收好、把布局压简单,后面你接正式项目时会轻松很多。
更多推荐


所有评论(0)