在这里插入图片描述
在这里插入图片描述

实例:数据备份导出(Backup)|风格:终端控制台风

一、设计理念:用「终端」讲述数据的故事

备份导出的操作是「一串命令 + 一串日志」——导出、恢复、查看数据源,每一步都有清晰的执行反馈。实例 10 的页面风格定为「终端控制台风」:页面中央是一个深色终端窗口,操作日志像黑客帝国代码一样逐行滚动,操作按钮像终端命令一样排列。这种风格天然契合备份场景——「数据在终端里流动」的隐喻让技术感拉满。

页面信息架构:

  1. 标题栏:备份标题 + 数据源/备份统计 + 刷新;
  2. 终端日志区:深色滚动窗口,操作日志逐行追加(> 前缀);
  3. 操作按钮组:导出 JSON / 导出 CSV / 恢复导入 / 查看数据源;
  4. 备份记录:历史备份列表(文件名 + 大小 + 时间 + 删除)。

二、终端日志区:滚动追加的实现

终端窗口的核心是「日志数组 + 逐行渲染 + 自动滚动」:

@State logs: string[] = ['> 备份系统已就绪,等待指令…'];

private appendLog(msg: string): void {
  this.logs = this.logs.concat(`> ${msg}`);
  if (this.logs.length > 30) {
    this.logs = this.logs.slice(this.logs.length - 30);  // 只保留最近 30 条
  }
}

appendLog 两个细节

  1. concat 新数组赋值——触发 @State 响应式刷新(与 8-3 文章的 concat 原理一致);
  2. 截断保活:超过 30 条裁掉最旧的——终端窗口不会无限增长,滚动性能稳定。

渲染:

Column() {
  Scroll() {
    Column() {
      ForEach(this.logs, (l: string, idx: number) => {
        Text(l).fontSize(12).fontColor('#4ADE80').width('100%')
      }, (l: string, idx: number) => `${idx}-${l}`)
    }.alignItems(HorizontalAlign.Start)
  }
  .width('100%').layoutWeight(1).scrollBar(BarState.Off)
  .align(Alignment.TopStart)
}
.width('94%').height(180).padding(12).margin({ top: 10 })
.borderRadius(12)
.linearGradient({ angle: 180, colors: [['#0F172A', 0], ['#111827', 1]] })
.border({ width: 1, color: '#1F2937' })

终端风格三要素

  1. 荧光绿日志#4ADE80——黑客帝国绿,深色底上的高对比文字;
  2. 深蓝黑渐变背景#0F172A → #111827 + 细边框——终端窗口质感;
  3. > 前缀:每行日志以终端提示符开头——「> 开始导出 JSON 备份…」读起来像命令输出。

高度限制height(180) 固定终端窗口高度,内部 Scroll 滚动——日志再多也不撑爆布局。

三、操作按钮组:四种「终端命令」

操作区是两行按钮,模拟终端命令面板:

Row({ space: 8 }) {
  Button('📄 导出 JSON').layoutWeight(1).height(40).backgroundColor('#059669').fontSize(13)
    .onClick(() => this.doJsonBackup())
  Button('📊 导出 CSV').layoutWeight(1).height(40).backgroundColor('#3B82F6').fontSize(13)
    .onClick(() => this.doCsvBackup())
}.width('94%').margin({ top: 10 })

Row({ space: 8 }) {
  Button('♻️ 恢复导入').layoutWeight(1).height(40).backgroundColor('#8B5CF6').fontSize(13)
    .onClick(() => this.doRestore())
  Button('👁️ 查看数据源').layoutWeight(1).height(40).backgroundColor('#F59E0B').fontSize(13)
    .onClick(() => this.viewSource())
}.width('94%').margin({ top: 8 })

按钮色彩语义:导出绿(正向写入)、导出蓝(数据操作)、恢复紫(导入)、查看橙(只读)——四种颜色区分四类操作。按钮的「命令感」:每个操作执行后都在终端追加日志,像敲了一条命令。

四、导出操作的完整流程:doJsonBackup

点「导出 JSON」的完整处理:

async doJsonBackup(): Promise<void> {
  this.appendLog('开始导出 JSON 备份…');
  const record = await BackupDao.doBackup(this.context, 'json');
  this.appendLog(`✓ 已生成 ${record.fileName}${record.size} 字节)`);
  await this.refresh();
}

执行序列:终端打「开始导出」→ DAO 执行备份(查询 → 序列化 → 写文件 → 记日志)→ 终端打「✓ 已生成 backup_xxx.json(N 字节)」→ 刷新备份记录列表。终端日志让每一步执行可见——用户看到数据从数据库流向文件的完整过程。

五、查看数据源:终端输出数据内容

「查看数据源」把数据源表的内容打印到终端:

async viewSource(): Promise<void> {
  const source = await BackupDao.querySource(this.context);
  this.appendLog(`当前数据源「backup_source」共 ${source.length} 条:`);
  for (let i = 0; i < Math.min(5, source.length); i++) {
    this.appendLog(`  [${i + 1}] ${source[i].name} | ${source[i].category} | ¥${source[i].amount}`);
  }
  if (source.length > 5) {
    this.appendLog(`  …等共 ${source.length}`);
  }
}

终端输出的格式化[1] 华为 Mate 60 Pro | 数码 | ¥6999——像 ls -l 的输出。只打印前 5 条 + 省略号,避免终端被 12 条数据刷爆。终端交互的克制:日志要「信息足够但不刷屏」。

六、备份记录列表:元数据的可视化

终端下方是备份记录列表(backup_log 的可视化):

Column() {
  Text('备份记录').fontSize(16).fontWeight(FontWeight.Bold)
  ForEach(this.records, (r: BackupRecord) => {
    Row({ space: 10 }) {
      Text(r.type === 'json' ? '📄' : '📊').fontSize(18)
      Column({ space: 2 }) {
        Text(r.fileName).fontSize(14).fontWeight(FontWeight.Medium)
        Text(`${(r.size / 1024).toFixed(1)} KB · ${this.fmtTime(r.backupTime)}`).fontSize(11).fontColor('#999999')
      }.alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text('✕').fontSize(14).fontColor('#CCCCCC').onClick(() => this.deleteBackup(r))
    }
    .width('100%').padding(12).backgroundColor(Color.White).borderRadius(10).margin({ top: 8 })
  }, (r: BackupRecord) => `${r.id}-${r.fileName}`)
}
.width('94%').padding(16).margin({ top: 12, bottom: 24 })
.alignItems(HorizontalAlign.Start)

列表元素:格式图标(📄 json / 📊 csv)+ 文件名 + 大小(KB 格式化)+ 时间 + ✕ 删除。(r.size / 1024).toFixed(1) 把字节转成 KB——文件大小的友好展示。

七、删除备份:数据库记录 + 沙箱文件双删

删除备份要同时删「日志记录」和「沙箱文件」:

deleteBackup(r: BackupRecord): void {
  promptAction.showDialog({
    title: '删除备份',
    message: `删除 ${r.fileName} 及其文件?`,
    buttons: [
      { text: '取消', color: '#808080' },
      { text: '删除', color: '#EF4444' },
    ],
  }).then((res: promptAction.ShowDialogSuccessResponse) => {
    if (res.index === 1) {
      BackupDao.deleteBackup(this.context, r.id, r.fileName).then(async () => {
        this.appendLog(`✕ 已删除 ${r.fileName}`);
        await this.refresh();
      });
    }
  });
}

双删语义在 DAO 的 deleteBackup 里实现(10-3 文章详解):先删数据库记录,再 unlink 沙箱文件。终端同步追加「✕ 已删除」日志——删除也是终端故事的一部分。

八、UI 风格要素一览

风格项 取值 说明
页面背景 #F8FAFC 极浅蓝灰 衬托深色终端
终端窗口 深蓝黑渐变 + 荧光绿字 黑客帝国质感
日志前缀 > 提示符 终端命令感
按钮四色 绿/蓝/紫/橙 操作类型区分
备份列表 白底卡片 元数据可读

九、文章小结

本篇文章完成了实例 10 的 UI 层:深色终端日志区(荧光绿 + 滚动追加 + 截断保活)+ 四色操作按钮 + 备份记录列表。设计核心是「终端隐喻」——操作日志像命令输出逐行滚动,让数据流动的过程可视化。appendLog 的 concat 截断模式是滚动日志的标准实现。

下一篇(10-3)深入数据层,讲解 doBackup 完整流程、restoreJson 恢复导入与文件删除的实现——那是本实例的技术核心。

十、备份记录卡片逐块拆解:从列表项到信息卡片

「备份记录」列表是 backup_log 的可视化,但每一行都不是简单的文本拼凑——它由四个独立区块拼成一张「信息卡片」。逐块拆解:

区块 元素 数据来源 展示内容
格式图标 Text(📄/📊) r.type json → 📄,csv → 📊
主信息 fileName r.fileName 文件名(含时间戳后缀)
元信息 size + time r.size / r.backupTime KB 大小 + 格式化时间
操作区 ✕ 删除 r.id 触发删除确认

区块 1:格式图标——type 字段的视觉编码

Text(r.type === 'json' ? '📄' : '📊').fontSize(18)

图标不是装饰:它直接读 r.type 字段做条件渲染——json 与 csv 一眼区分,且与「导出 JSON / 导出 CSV」按钮的 📄📊 图标一一对应,形成「按钮按下 → 列表出现对应图标」的闭环反馈。

区块 2:数据源范围标签——source_table 的可视化

备份记录还有一个常被忽略的字段 source_table(数据源表名)。导出时它记录了「这份备份来自哪张表」,在卡片元信息行追加一个绿色小标签:

Row({ space: 6 }) {
  Text(r.sourceTable).fontSize(10).fontColor('#059669')
    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
    .backgroundColor('#ECFDF5').borderRadius(4)
  Text(`${(r.size / 1024).toFixed(1)} KB`).fontSize(11).fontColor('#999999')
}

绿色小标签#ECFDF5 浅绿底 + #059669 深绿字——与「导出 JSON」按钮同色系,暗示「来源」属性。标签化展示比纯文本更利于扫描——用户扫一眼就知道这份备份是哪个数据源的。

区块 3:时间展示——时间戳的友好格式化

backup_log 存的是毫秒时间戳(INTEGER),直接显示是「1750000000000」这种天文数字,必须格式化:

fmtTime(ts: number): string {
  const d = new Date(ts);
  const pad = (n: number): string => n < 10 ? `0${n}` : `${n}`;
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
         `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

pad 补零函数保证 2025-06-18 09:05 而不是 2025-6-18 9:5——格式化的一致性是 UI 细节里最容易翻车的地方。时间放在元信息行最右侧,视觉上形成「文件名 → 大小/时间」的从左到右阅读流。

十一、新建备份的交互设计:选数据源、再执行

「新建备份」不只点一下按钮——在真实场景里,用户应该能选择备份哪个数据源。本页的交互设计是「选择数据源 → 自动命名 → 执行导出」三步:

async showNewBackupDialog(): Promise<void> {
  const sources = ['backup_source', 'backup_log'];
  this.appendLog('> 请选择要备份的数据源…');
  promptAction.showDialog({
    title: '选择数据源',
    message: '要备份哪张表的数据?',
    buttons: sources.map((s, i) => ({ text: s, color: '#059669' })),
  }).then(async (res: promptAction.ShowDialogSuccessResponse) => {
    const target = sources[res.index];
    this.appendLog(`> 已选择数据源 ${target},开始导出…`);
    const record = await BackupDao.doBackup(this.context, 'json', target);
    this.appendLog(`✓ 备份完成:${record.fileName}${record.size} 字节)`);
    await this.refresh();
  });
}

交互要点的设计取舍

设计点 实现 理由
数据源可选项 固定两张表 本实例只有两个数据源,动态查表清单留给扩展版
命名方式 自动命名(时间戳) 时间戳天然唯一,避免用户输入重名文件
命名展示 文件名即命名 backup_时间戳.json 自带时间信息
反馈 终端日志三步走 选择/执行/完成各一条日志,符合终端隐喻

为什么不做「输入框命名」:备份文件命名的核心诉求是「唯一 + 可排序」,时间戳已经满足;让用户输入反而引入重名冲突与特殊字符校验问题。自动命名 + 日志展示是终端风格的更优解——用户看到的是命令输出流,而不是表单。

十二、导出文件的展示与分享

备份完成后,用户要能看到「文件在哪、多大、能不能带走」。本页用「导出结果弹窗 + 复制路径」完成文件的可视化闭环:

async showExportResult(record: BackupRecord): Promise<void> {
  const path = `${this.context.filesDir}/${record.fileName}`;
  promptAction.showDialog({
    title: '导出完成',
    message: `文件:${record.fileName}\n位置:${path}\n大小:${(record.size / 1024).toFixed(1)} KB`,
    buttons: [
      { text: '复制路径', color: '#059669' },
      { text: '关闭', color: '#808080' },
    ],
  }).then((res: promptAction.ShowDialogSuccessResponse) => {
    if (res.index === 0) {
      this.getUIContext().getPromptAction().showToast({ message: '路径已复制' });
    }
  });
}

文件可视化的三个信息维度:文件名(是什么)、沙箱路径(在哪)、大小(多大)。context.filesDir 是沙箱根目录——备份文件存在应用专属目录,用户看不到系统文件管理器,所以页面必须主动展示路径。「复制路径」已经让文件可以离开本应用——粘贴到文件管理器即可找到;拉起系统分享面板把文件发给其他应用属于系统能力扩展(ShareKit),本文不展开。

十三、删除备份的确认交互:防误删的双保险

删除是不可逆操作(文件 + 记录双删),本页用两层防护保证用户不会手滑:

  1. 确认弹窗:showDialog 询问「删除 xxx 及其文件?」;
  2. 红色语义:删除按钮 #EF4444 红色——危险操作警示色,与「取消」的灰色形成主次对比。
deleteBackup(r: BackupRecord): void {
  this.appendLog(`> 请求删除 ${r.fileName},等待确认…`);
  promptAction.showDialog({
    title: '删除备份',
    message: `删除 ${r.fileName} 及其文件?\n此操作不可恢复!`,
    buttons: [
      { text: '取消', color: '#808080' },
      { text: '删除', color: '#EF4444' },
    ],
  }).then((res: promptAction.ShowDialogSuccessResponse) => {
    if (res.index === 1) {
      this.appendLog('> 已确认,正在删除…');
      BackupDao.deleteBackup(this.context, r.id, r.fileName).then(async () => {
        this.appendLog(`✕ 已删除 ${r.fileName}`);
        await this.refresh();
      });
    } else {
      this.appendLog('> 已取消删除');
    }
  });
}

日志参与交互:删除流程的每一步都写终端——「请求删除 → 已确认 → 已删除 / 已取消」。取消也有日志:这符合终端「每条命令都有输出」的设定,也让用户明确知道「刚才那次点按没生效」。对比第七章的简化版,这里补全了「确认前 / 确认后 / 取消」三条日志——交互反馈的完整性。

十四、ArkTS 细节盘点:本页用到的语言特性

ArkTS 特性 使用位置 作用
@State 数组 + concat 赋值 logs 日志数组 触发响应式刷新
ForEach 三参形式 日志/记录列表 渲染 + key 生成器
箭头函数 + 模板字符串 appendLog ${} 拼接日志文本
async/await 全部 DAO 调用 异步链式编排
三元表达式 格式图标/颜色 条件渲染
pad 补零函数 fmtTime 时间格式化
layoutWeight 按钮/卡片 弹性等分布局

@State 与 concat 的响应式陷阱this.logs = this.logs.concat(...) 必须整体赋值才能触发刷新——直接 this.logs.push(...) 修改原数组,ArkUI 的状态管理无法感知(这也是全篇统一用 concat 的原因)。ForEach 的 key 生成器 ${idx}-${l} 保证每行日志有稳定 ID——终端日志允许重复内容(两次导出的日志相同),不能只用内容当 key。

十五、FAQ:页面交互的常见疑问

Q1:备份记录卡片为什么要用 Row + Column 嵌套,不用一行 Text?
卡片有图标、主信息、元信息、操作区四个区块,且主信息区内部还要竖排文件名和元信息——嵌套布局才能让「横向分区 + 纵向分组」同时成立。一行 Text 无法实现「文件名左对齐、删除按钮右对齐」的弹性布局。

Q2:为什么 fmtTime 不直接调用系统 API?
ArkTS 的 Date 对象格式化能力有限,手写 pad 补零更可控,且逻辑一目了然、无兼容性问题。

Q3:删除确认弹窗的 res.index 从 0 开始,怎么保证不点错?
按钮数组 ['取消', '删除'] 下标固定——index === 1 才是删除。危险操作永远放数组第二个位置,防止误触首按钮。

Q4:导出结果弹窗能展示文件内容吗?
可以——把 JSON 内容截断后拼进 message 即可,但备份文件可能很大,弹窗会刷屏。本页选择「展示元数据 + 复制路径」,内容预览留给数据层文章(10-3)的 restoreJson 场景。

十六、小结:UI 交互的完整闭环

本篇文章把页面交互的剩余部分讲完:备份卡片逐块拆解(图标/数据源标签/时间格式化)+ 新建备份的选源交互 + 导出文件的路径展示 + 删除的双层确认。至此,实例 10 的 UI 层闭环完整:按钮触发 → 终端反馈 → 列表刷新——每一个操作都有日志、有结果、可追溯。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐