HarmonyOS ArkTS 嵌入式弹窗(Embedded Dialog)实战:书籍管理小业务(可直接跑)
HarmonyOS ArkTS 嵌入式弹窗(Embedded Dialog)实战:书籍管理小业务(可直接跑)
目标很明确:做一个“书籍列表 + 新增/编辑 + 删除确认”的小应用。
删除时弹出嵌入式对话框确认;表单校验失败也用嵌入式对话框提示。
整个弹窗都绑定当前页面,不会出现那种“切页面了弹窗还挂在上面”的尴尬问题。
0. 先把 Embedded Dialog 的“人话逻辑”讲清楚
很多人第一次用弹窗,会用全局 show,那种弹窗很“强势”:
- 页面都切走了,它还可能在上层挡着
- 生命周期不跟页面走,你要自己记得 close
- 页面内的状态联动也不太自然(尤其多个页面跳转)
嵌入式弹窗的好处就是:
它更像“页面里的一块 UI”,跟着页面一起生、一起灭。
我在这套 demo 里用的策略很简单:
- 用
@State visible控制弹窗是否显示 - 需要弹窗 →
visible = true - 弹窗按钮点击/取消 →
visible = false - 弹窗配置里用
levelMode: LevelMode.EMBEDDED(关键点)
你会发现:代码读起来很顺,和页面状态是一套思路。
1. 真实业务场景设定(为什么选“书籍管理”)
这个场景其实非常接近你以后做的业务页:
- 首页列表:展示数据(书名、作者、价格)
- 详情/编辑页:输入框校验 + 保存
- 删除这种操作:必须二次确认(否则用户误删会骂人)
所以我们用它来演示嵌入式弹窗,是非常合理的。
2. 可运行工程目录结构(你照着建就行)
我建议你直接在 DevEco Studio 新建一个 Stage 工程,然后按下面目录补齐文件。
BookShelfEmbeddedDialogDemo/
└── entry/
└── src/
└── main/
├── module.json5
├── ets/
│ ├── entryability/
│ │ └── EntryAbility.ets
│ ├── pages/
│ │ ├── Index.ets // 书籍列表页(删除确认弹窗)
│ │ └── EditBook.ets // 新增/编辑页(校验失败提示弹窗)
│ ├── components/
│ │ └── EmbeddedConfirmDialog.ets // 可复用嵌入式弹窗组件
│ ├── model/
│ │ └── Book.ets // 数据模型
│ └── store/
│ └── BookStore.ets // 简单数据仓库(内存版)
└── resources/
└── base/
└── element/
└── string.json
这套结构是典型“业务分层”:
- model:数据结构
- store:数据管理(以后可换成 RDB/网络请求)
- pages:页面
- components:可复用组件
3. 先写数据模型(Book)
entry/src/main/ets/model/Book.ets
export type Book = {
id: string
title: string
author: string
price: number
updatedAt: number
}
export function now(): number {
return Date.now()
}
export function uuid(): string {
// demo 用的轻量 id:够用、好理解
return `${Date.now()}_${Math.floor(Math.random() * 100000)}`
}
这里我加了 updatedAt,主要是为了列表页可以按更新时间排序,让它更像真实应用。
4. 写一个“可替换”的数据仓库(BookStore)
注意:这里用“内存数组”做存储,目的是让你能立刻跑起来。
之后如果你要换成 RDB 或网络接口,只需要改 store 层,页面不用大动。
entry/src/main/ets/store/BookStore.ets
import { Book, now, uuid } from '../model/Book'
export class BookStore {
private static _instance: BookStore | null = null
static get I(): BookStore {
if (!this._instance) this._instance = new BookStore()
return this._instance
}
private books: Book[] = [
{ id: uuid(), title: '鸿蒙开发入门', author: '程奕红', price: 59.9, updatedAt: now() },
{ id: uuid(), title: 'ArkTS 实战', author: 'Cheng Yihong', price: 79.0, updatedAt: now() },
{ id: uuid(), title: 'UI 组件设计笔记', author: 'Harmony Dev', price: 45.5, updatedAt: now() },
]
list(): Book[] {
return this.books.slice().sort((a, b) => b.updatedAt - a.updatedAt)
}
getById(id: string): Book | undefined {
return this.books.find(b => b.id === id)
}
upsert(input: Omit<Book, 'updatedAt'>): void {
const idx = this.books.findIndex(b => b.id === input.id)
const item: Book = { ...input, updatedAt: now() }
if (idx >= 0) this.books[idx] = item
else this.books.unshift(item)
}
remove(id: string): void {
this.books = this.books.filter(b => b.id !== id)
}
}
5. 核心:写一个“嵌入式确认弹窗”组件(可复用)
这个组件我故意做得“业务化”一点:
传 title、message、按钮文案、确认/取消回调,很多页面都能直接用。
entry/src/main/ets/components/EmbeddedConfirmDialog.ets
@Component
export struct EmbeddedConfirmDialog {
@Link visible: boolean
title: string = '提示'
message: string = ''
cancelText: string = '取消'
confirmText: string = '确认'
onCancel?: () => void
onConfirm?: () => void
private closeOnly() {
this.visible = false
}
build() {
if (!this.visible) {
return
}
// ✅ 关键:levelMode: LevelMode.EMBEDDED
AlertDialog.show({
title: this.title,
message: this.message,
autoCancel: false,
alignment: DialogAlignment.Center,
levelMode: LevelMode.EMBEDDED,
primaryButton: {
value: this.confirmText,
action: () => {
this.closeOnly()
this.onConfirm?.()
}
},
secondaryButton: {
value: this.cancelText,
action: () => {
this.closeOnly()
this.onCancel?.()
}
},
onCancel: () => {
this.closeOnly()
this.onCancel?.()
}
})
}
}
为什么我这里要 autoCancel: false?
因为删除确认这种弹窗,最怕用户误触外部就直接消失(用户以为没点上)。
真实项目里我一般会让用户明确点按钮。
6. 首页:书籍列表(删除确认弹窗就在这里)
交互流程(很真实)
- 用户点“删除”
- 我先把要删的书
id/title存起来 confirmVisible = true- 弹窗显示“确认删除《xxx》吗”
- 用户点“删除” → 执行 remove → reload 刷新列表
entry/src/main/ets/pages/Index.ets
import { router } from '@kit.ArkUI'
import { Book } from '../model/Book'
import { BookStore } from '../store/BookStore'
import { EmbeddedConfirmDialog } from '../components/EmbeddedConfirmDialog'
@Entry
@Component
struct Index {
@State books: Book[] = []
// 删除确认弹窗
@State confirmVisible: boolean = false
@State pendingDeleteId: string = ''
@State pendingDeleteTitle: string = ''
aboutToAppear() {
this.reload()
}
private reload() {
this.books = BookStore.I.list()
}
private goAdd() {
router.pushUrl({ url: 'pages/EditBook', params: { mode: 'add' } })
}
private goEdit(book: Book) {
router.pushUrl({ url: 'pages/EditBook', params: { mode: 'edit', id: book.id } })
}
private askDelete(book: Book) {
this.pendingDeleteId = book.id
this.pendingDeleteTitle = book.title
this.confirmVisible = true
}
private doDelete() {
BookStore.I.remove(this.pendingDeleteId)
this.pendingDeleteId = ''
this.pendingDeleteTitle = ''
this.reload()
}
build() {
Column() {
Row() {
Text('📚 书架(Embedded Dialog Demo)')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Blank()
Button('新增')
.onClick(() => this.goAdd())
}
.width('100%')
.padding(12)
Divider()
List() {
ForEach(this.books, (b: Book) => {
ListItem() {
Row() {
Column() {
Text(b.title).fontSize(16).fontWeight(FontWeight.Medium)
Text(`作者:${b.author} · ¥${b.price.toFixed(2)}`)
.fontSize(12)
.fontColor('#666')
}
.layoutWeight(1)
Button('编辑')
.margin({ right: 8 })
.onClick(() => this.goEdit(b))
Button('删除')
.backgroundColor('#E5484D')
.fontColor('#FFF')
.onClick(() => this.askDelete(b))
}
.padding(12)
}
}, (b: Book) => b.id)
}
.layoutWeight(1)
// ✅ 嵌入式确认弹窗:归属当前页面
EmbeddedConfirmDialog({
visible: $confirmVisible,
title: '确认删除',
message: `确定要删除《${this.pendingDeleteTitle}》吗?删除后无法恢复。`,
cancelText: '我再想想',
confirmText: '删除',
onConfirm: () => this.doDelete()
})
}
.height('100%')
}
}
7. 新增/编辑页:做校验失败提示(也是嵌入式弹窗)
真实业务里表单最常见的坑就是:
- 用户输入空
- 用户价格输入“abc”
- 用户输入负数
- 用户输入特别离谱的数(比如 999999)
所以我给你写了一套比较“像产品要求”的校验逻辑。
entry/src/main/ets/pages/EditBook.ets
import { router } from '@kit.ArkUI'
import { BookStore } from '../store/BookStore'
import { Book, uuid } from '../model/Book'
import { EmbeddedConfirmDialog } from '../components/EmbeddedConfirmDialog'
@Entry
@Component
struct EditBook {
@State mode: 'add' | 'edit' = 'add'
@State id: string = ''
@State title: string = ''
@State author: string = ''
@State priceText: string = ''
// 校验提示弹窗
@State errorVisible: boolean = false
@State errorMsg: string = ''
aboutToAppear() {
const params = router.getParams() as Record<string, any> | undefined
const mode = (params?.mode as string) ?? 'add'
this.mode = (mode === 'edit') ? 'edit' : 'add'
if (this.mode === 'edit') {
const id = String(params?.id ?? '')
this.id = id
const book = BookStore.I.getById(id)
if (book) {
this.title = book.title
this.author = book.author
this.priceText = String(book.price)
} else {
this.showError('没有找到这本书,可能已经被删除了。')
}
} else {
this.id = uuid()
this.title = ''
this.author = ''
this.priceText = ''
}
}
private showError(msg: string) {
this.errorMsg = msg
this.errorVisible = true
}
private validate(): number | null {
const t = this.title.trim()
const a = this.author.trim()
const p = this.priceText.trim()
if (!t) { this.showError('书名不能为空。'); return null }
if (t.length > 30) { this.showError('书名太长了(建议 30 字以内)。'); return null }
if (!a) { this.showError('作者不能为空。'); return null }
const price = Number(p)
if (!p || Number.isNaN(price)) { this.showError('价格请输入数字,例如 59.9'); return null }
if (price <= 0) { this.showError('价格要大于 0'); return null }
if (price > 9999) { this.showError('价格太离谱了(>9999),检查一下输入。'); return null }
return price
}
private save() {
const price = this.validate()
if (price === null) return
const book: Omit<Book, 'updatedAt'> = {
id: this.id,
title: this.title.trim(),
author: this.author.trim(),
price
}
BookStore.I.upsert(book)
router.back()
}
build() {
Column() {
Row() {
Text(this.mode === 'add' ? '新增书籍' : '编辑书籍')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Blank()
Button('保存')
.onClick(() => this.save())
}
.width('100%')
.padding(12)
Divider()
Column() {
Text('书名').fontSize(12).fontColor('#666')
TextInput({ text: this.title, placeholder: '例如:ArkTS 实战' })
.onChange(v => this.title = v)
.margin({ bottom: 12 })
Text('作者').fontSize(12).fontColor('#666')
TextInput({ text: this.author, placeholder: '例如:程奕红' })
.onChange(v => this.author = v)
.margin({ bottom: 12 })
Text('价格').fontSize(12).fontColor('#666')
TextInput({ text: this.priceText, placeholder: '例如:59.9' })
.type(InputType.Number)
.onChange(v => this.priceText = v)
}
.padding(12)
// ✅ 校验提示:嵌入式弹窗
EmbeddedConfirmDialog({
visible: $errorVisible,
title: '填写有问题',
message: this.errorMsg,
cancelText: '知道了',
confirmText: '去修改'
})
}
.height('100%')
}
}
8. 入口 Ability(保证能加载首页)
entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { }
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('loadContent failed: ' + JSON.stringify(err))
}
})
}
}
9. module.json5(常见工程默认有,确认 pages 指向 Index)
entry/src/main/module.json5
{
"module": {
"name": "entry",
"type": "entry",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true
}
],
"pages": "pages/Index"
}
}
10. 你跑起来之后,应该看到什么(对照检查)
首页
- 顶部:📚 书架(Embedded Dialog Demo) + 新增按钮
- 列表 3 条数据(标题/作者/价格)
- 每条右侧:编辑、删除
删除
- 点删除 → 出现“确认删除《xxx》吗?”
- 点“删除” → 列表立刻少一条
- 点“我再想想” → 关闭,不删
新增/编辑
- 新增:输入书名/作者/价格
- 不填/填错:弹出“填写有问题”的嵌入式提示
- 保存成功:返回首页,新增记录在顶部
11. 我给你补一句“真实项目里最容易踩的坑”
- 弹窗显示条件要可控
别在 build 里每次都 show,不然你会遇到“状态更新导致重复弹出”的问题。
我这套用visible+ show,确保只在你想弹的时候弹。 - 跨页面不要复用同一个弹窗实例
嵌入式弹窗是“页面私有”的,页面 A 的弹窗不要拿去页面 B 用(很容易乱)。 - 删除确认按钮建议危险色
我已经给“删除”按钮上了红色背景,这个是产品常规操作,用户更不容易误点。
浙公网安备 33010602011771号