HarmonyOS7电子名片交换页实战:一张名片,两个场景,三种状态切换
电子名片这种页面很容易写偏。很多示例会把重点放在渐变背景、圆角、头像和图标上,最后做出来像一张漂亮截图,但看不出业务结构。这个案例真正值得看的地方不在视觉,而在它把“我拿出去交换的名片”和“我收到后要管理的名片”放进了同一个页面里。
这两个场景看起来都叫名片,实际任务完全不同。我的名片更像身份资产,要完整、醒目、可分享;收到的名片更像关系记录,要方便浏览、能快速回忆当时交换的背景。代码如果没有先想清楚这一点,很容易写成两个互相复制的卡片区域,后面一改样式就两边都要补。

这里的处理方式比较稳:先定义统一的 BusinessCard 模型,再用 CardView(card, compact) 控制展示密度。compact 不是一个随手加的样式开关,它表达的是同一份数据在不同任务场景里的信息层级。
compact 为 false 时,卡片承担“我的身份页”角色,电话、邮箱、网站、NFC 标记都可以完整展示;compact 为 true 时,卡片收缩成列表摘要,只保留姓名、职位、公司这类快速识别信息。这个判断很像真实项目里的组件设计:复用不是把 UI 硬抽成一个函数,而是先找出稳定的数据协议,再把变化点变成明确参数。
页面状态也拆得比较干净。myCard 是当前用户自己的名片,属于页面的核心资产;receivedCards 是收到的名片列表,代表关系沉淀;activeTab 决定当前用户正在处理哪类任务;showNFC 只负责 NFC 弹层开关;selectedCard 虽然当前还没真正用起来,但明显是给详情或编辑链路预留的位置。
这几个状态的层级不一样。myCard 和 receivedCards 是内容状态,页面大部分区域都围着它们转;activeTab 是视图状态,只决定当前显示哪一面;showNFC 是瞬时交互状态,用完就收。把这些东西分清楚,页面维护起来会轻很多,因为你能判断每次修改到底会影响内容、结构,还是只影响一次弹窗交互。

@Builder CardView(card: BusinessCard, compact: boolean) 是这篇最值得反复看的地方。它不只是为了少写几行 ArkUI 代码,而是在定义“电子名片应该怎么展示”。头像、姓名、职位、公司是基础身份信息,两种模式都要保留;联系方式和网站只在完整态展示;高度、字号、间距会随着 compact 自动收缩;背景渐变和阴影又跟 themeColor 绑定,视觉也没有被写死。
这意味着 CardView() 更像一个展示协议。只要这个协议稳定,外层不管把它放在“我的名片”、收到列表,还是未来的详情页里,都不用重新解释一张名片长什么样。真实项目里很多组件复用失败,就是因为只抽了布局,没有抽出协议。
主题色切换也是一个很好的状态驱动示例。用户点某个颜色后,代码只改 this.myCard.themeColor = color,名片渐变、阴影和选中边框就跟着更新。这里没有额外维护一套“当前背景色”“当前阴影色”“当前选中样式”,视觉结果直接从业务状态投影出来。
这个习惯很重要。做 ArkUI 页面时,如果每个视觉变化都单独开状态,页面很快会变得乱。更好的方式是让状态表达业务含义,再让 UI 根据状态计算出视觉结果。这里的 themeColor 就是这种思路的缩小版,以后扩成深色模式、卡片皮肤、品牌主题,也能沿着这个方向走。
“我的名片”和“收到的名片”表面上只是两个 tab,但更准确地说,它们是两个任务上下文。前者关心展示自己、分享出去、快速交换;后者关心认识了谁、什么时候交换的、当时有什么合作备注。所以收到列表里没有继续塞满联系方式,而是把 exchangeDate 和 note 放到卡片下面,让它从展示对象变成关系记录。
这类取舍比组件写法更关键。页面不是信息越全越好,而是要看用户在当前场景里最需要什么。我的名片需要完整,因为它面向外部展示;收到的名片需要轻,因为它面向内部回看。代码里的 compact 参数,背后其实就是这层产品判断。
NFC 弹层的状态也很克制。主动作是 NFC碰一碰,点击后只把 showNFC 设为 true;遮罩和取消按钮再把它设回 false。这个布尔值虽然简单,但边界很清楚:主页面继续管理名片,弹层只管理一次交换动作,不把临时流程塞进主数据结构。
如果后面要把 NFC 换成二维码、蓝牙或者系统分享面板,这种组织方式也还能留住。因为它没有把交换过程和名片数据绑死,只是在页面上挂了一个局部交互层。
selectedCard: BusinessCard | null 是当前代码里一个挺真实的“未完成口子”。收到的名片列表里已经有“查看详情”的文案,但还没有把点击项写入 selectedCard。如果继续往正式项目做,我会从这里接下去:点击某张收到的名片,把它设为 selectedCard,再拉起详情面板,展示完整联系方式、交换记录和后续备注。
这样页面就不只是电子名片 demo 了,而会更接近一个轻量 CRM 入口。它已经有统一模型、有列表、有主题、有交换动作,只差把“收到之后怎么管理关系”这条线继续补完整。
所以这篇真正该学的不是某个 ArkUI 组件怎么写,而是这套组合:用 BusinessCard 收住数据,用 CardView() 固定展示协议,用 activeTab 分开任务场景,用 showNFC 承接局部动作,再让 themeColor 证明视觉可以由状态自然驱动。页面不复杂,但结构是能往真实业务里长的。
完整代码
下面保留案例完整代码,方便你直接对照学习、复制和重构。

// 电子名片交换页: 电子名片交换 - NFC模拟、名片设计、分享记录
interface BusinessCard {
id: number
name: string
title: string
company: string
phone: string
email: string
website: string
avatar: string
themeColor: string
exchangeDate: string
note: string
isMine: boolean
}
@Entry
@Component
struct DigitalBusinessCardExchange {
@State myCard: BusinessCard = {
id: 0,
name: '李明',
title: '高级产品经理',
company: '星云科技有限公司',
phone: '138-8888-8888',
email: 'liming@xingyun.com',
website: 'www.xingyun.com',
avatar: '李',
themeColor: '#007AFF',
exchangeDate: '',
note: '',
isMine: true,
}
@State receivedCards: BusinessCard[] = [
{ id: 1, name: '王芳', title: '技术总监', company: '腾讯科技', phone: '135-1234-5678', email: 'wangfang@tencent.com', website: 'tencent.com', avatar: '王', themeColor: '#5856D6', exchangeDate: '2026-06-05', note: '产品合作意向', isMine: false },
{ id: 2, name: '陈志强', title: '市场总监', company: '阿里巴巴', phone: '139-8765-4321', email: 'chenzhiqiang@alibaba.com', website: 'alibaba.com', avatar: '陈', themeColor: '#FF9500', exchangeDate: '2026-06-04', note: '推广合作', isMine: false },
{ id: 3, name: '刘雪', title: '设计师', company: '小米科技', phone: '132-5555-6666', email: 'liuxue@xiaomi.com', website: 'mi.com', avatar: '刘', themeColor: '#FF3B30', exchangeDate: '2026-06-02', note: 'UI设计合作', isMine: false },
{ id: 4, name: '赵博', title: 'CEO', company: '未来科技', phone: '136-7777-8888', email: 'zhaobo@future.com', website: 'future.com', avatar: '赵', themeColor: '#34C759', exchangeDate: '2026-05-30', note: '融资对接', isMine: false },
]
@State activeTab: number = 0
@State selectedCard: BusinessCard | null = null
@State showNFC: boolean = false
@Builder
CardView(card: BusinessCard, compact: boolean) {
Column() {
Row() {
Text(card.avatar)
.fontSize(compact ? 20 : 28).fontColor('#FFFFFF')
.width(compact ? 44 : 56).height(compact ? 44 : 56)
.backgroundColor('#FFFFFF30')
.borderRadius(compact ? 22 : 28)
.textAlign(TextAlign.Center)
.lineHeight(compact ? 44 : 56)
Column() {
Text(card.name)
.fontSize(compact ? 16 : 20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(card.title)
.fontSize(compact ? 11 : 13).fontColor('#FFFFFF90').margin({ top: 2 })
Text(card.company)
.fontSize(compact ? 11 : 12).fontColor('#FFFFFF80').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
.layoutWeight(1)
if (!compact) {
Text('NFC').fontSize(12).fontColor('#FFFFFF80')
.border({ width: 1, color: '#FFFFFF60' })
.borderRadius(6).padding({ left: 6, right: 6, top: 3, bottom: 3 })
}
}
.width('100%')
if (!compact) {
Divider().color('#FFFFFF30').margin({ top: 14, bottom: 14 })
Row({ space: 20 }) {
Column() {
Text('📞').fontSize(14)
Text(card.phone).fontSize(11).fontColor('#FFFFFFCC').margin({ top: 3 })
}
Column() {
Text('📧').fontSize(14)
Text(card.email).fontSize(10).fontColor('#FFFFFFCC').margin({ top: 3 })
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
Column() {
Text('🌐').fontSize(14)
Text(card.website).fontSize(10).fontColor('#FFFFFFCC').margin({ top: 3 })
}
}
.width('100%')
}
}
.width('100%')
.padding(compact ? 14 : 20)
.borderRadius(16)
.linearGradient({
angle: 135,
colors: [[card.themeColor, 0], [`${card.themeColor}CC`, 1]]
})
.shadow({ radius: 10, color: `${card.themeColor}40`, offsetX: 0, offsetY: 4 })
}
build() {
Column() {
// 顶部
Text('我的名片夹').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1A1A1A')
.padding({ left: 20, right: 20, top: 20, bottom: 12 })
.alignSelf(ItemAlign.Start)
// Tab
Row() {
ForEach(['我的名片', '收到的名片'], (tab: string, idx: number) => {
Text(tab)
.fontSize(14)
.fontColor(this.activeTab === idx ? '#007AFF' : '#888888')
.fontWeight(this.activeTab === idx ? FontWeight.Bold : FontWeight.Normal)
.layoutWeight(1).textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.border({ width: { bottom: this.activeTab === idx ? 2 : 0 }, color: { bottom: '#007AFF' } })
.onClick(() => { this.activeTab = idx })
})
}
.backgroundColor('#FFFFFF')
.margin({ bottom: 16 })
if (this.activeTab === 0) {
// 我的名片
Scroll() {
Column({ space: 16 }) {
this.CardView(this.myCard, false)
// 操作按钮
Row({ space: 12 }) {
Button('📤 分享名片')
.layoutWeight(1).height(46).fontSize(14)
.fontColor('#007AFF').backgroundColor('#EEF6FF').borderRadius(12)
Button('📱 NFC碰一碰')
.layoutWeight(1).height(46).fontSize(14)
.fontColor('#FFFFFF').backgroundColor('#007AFF').borderRadius(12)
.onClick(() => { this.showNFC = true })
}
// 名片颜色主题
Column({ space: 10 }) {
Text('选择主题').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1A1A1A').alignSelf(ItemAlign.Start)
Row({ space: 12 }) {
ForEach(['#007AFF', '#5856D6', '#FF3B30', '#34C759', '#FF9500', '#FF2D92'], (color: string) => {
Column()
.width(32).height(32)
.backgroundColor(color)
.borderRadius(16)
.border({ width: this.myCard.themeColor === color ? 3 : 0, color: '#333333' })
.onClick(() => {
this.myCard.themeColor = color
})
})
}
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
// 统计
Row() {
Column() {
Text(String(this.receivedCards.length)).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#007AFF')
Text('交换次数').fontSize(11).fontColor('#888888')
}
.layoutWeight(1)
Column() {
Text('32').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#34C759')
Text('被查看').fontSize(11).fontColor('#888888')
}
.layoutWeight(1)
Column() {
Text('8').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FF9500')
Text('本周新增').fontSize(11).fontColor('#888888')
}
.layoutWeight(1)
}
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
.padding({ left: 20, right: 20, bottom: 20 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
} else {
// 收到的名片
List({ space: 10 }) {
ForEach(this.receivedCards, (card: BusinessCard) => {
ListItem() {
Column() {
this.CardView(card, true)
Row() {
Text(`📅 ${card.exchangeDate}`).fontSize(11).fontColor('#AAAAAA')
Blank()
if (card.note) {
Text(`💬 ${card.note}`).fontSize(11).fontColor('#888888').maxLines(1)
}
Text('查看详情 >').fontSize(11).fontColor('#007AFF').margin({ left: 10 })
}
.width('100%')
.padding({ top: 8, left: 4, right: 4 })
}
}
})
}
.layoutWeight(1)
.padding({ left: 16, right: 16 })
.scrollBar(BarState.Off)
}
// NFC弹窗
if (this.showNFC) {
Stack() {
Column().width('100%').height('100%').backgroundColor('#00000060')
.onClick(() => { this.showNFC = false })
Column({ space: 16 }) {
Text('📱 NFC 碰一碰').fontSize(18).fontWeight(FontWeight.Bold)
Text('将两台设备背面靠近\n即可交换名片')
.fontSize(14).fontColor('#666666').textAlign(TextAlign.Center)
Text('📡')
.fontSize(60)
.animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
Button('取消').height(44).width(120)
.fontColor('#007AFF').backgroundColor('#F5F5F5').borderRadius(22)
.onClick(() => { this.showNFC = false })
}
.width('80%').padding(24).backgroundColor('#FFFFFF').borderRadius(20)
.alignItems(HorizontalAlign.Center)
}
.width('100%').height('100%').position({ x: 0, y: 0 })
}
}
.width('100%').height('100%')
.backgroundColor('#F2F2F7')
}
}
最后总结
电子名片这页最值得学的,不是把卡片做得多像真名片,而是它把“我的展示页”和“我的联系人记录”拆成了两个任务场景,再用一个可切换密度的 CardView() 把它们收进同一套组件协议里。这样的结构后续无论扩成详情页、编辑页还是轻量 CRM,都还有很好的延展空间。
更多推荐

所有评论(0)