图标行文件浏览器:ArkUI 让鸿蒙文件库像电脑资源管理器
实例文件资料库FileLib风格图标行文件浏览器一、设计理念文件库要「扫一眼认文件」文件资料库的 UI 目标是电脑资源管理器/手机文件管理器——每行一个文件图标类型 名称 元信息类型标签/大小/日期/标签。实例 16 的风格「图标行文件浏览器」搜索框 类型筛选条 图标行列表。页面信息架构标题栏文件库 文件总数/总大小 刷新搜索框按名称/标签模糊搜索LIKE类型筛选条全部/文档/图片/视频/音频/压缩包/其他动态——来自统计结果文件列表图标行类型 emoji 图标 文件名 类型标签/大小/日期 标签 删除。二、页面骨架与核心状态Statefiles:FileRecord[][];StatetypeFilter:string全部;Statekeyword:string;StatetotalCount:number0;StatetotalSize:number0;StatetypeStats:TypeStat[][];privatereadonlyicons:Recordstring,string{文档:,图片:️,视频:,音频:,压缩包:️,其他:,};icons 类型映射Recordstring, string——类型 → emoji 图标的映射表ArkTS 对象字面量需声明类型。列表渲染时this.icons[f.fileType] ?? 未知类型兜底「」。build 骨架Column(){// 标题栏// 搜索框// 类型筛选条动态// 文件列表List 图标行}三、搜索框实时模糊搜索TextInput({placeholder: 按名称 / 标签搜索…,text:this.keyword}).width(94%).height(40).margin({top:10}).backgroundColor(Color.White).borderRadius(20).onChange((v:string)this.onSearch(v))onSearch(v:string):void{this.keywordv;this.applyFilter();}asyncapplyFilter():Promisevoid{if(this.keyword.trim()){this.filesawaitFileLibDao.search(this.context,this.keyword.trim());}else{this.filesawaitFileLibDao.queryByType(this.context,this.typeFilter);}}搜索优先关键词非空 → search名称/标签 LIKE为空 → 类型筛选。「搜索与筛选互斥」搜索时不叠加类型过滤简化交互placeholder 说明搜索范围「按名称 / 标签搜索…」——用户知道搜什么。圆角 20 胶囊搜索框——常见搜索框样式。四、类型筛选条动态生成类型筛选条的特殊之处——选项来自数据库统计不是写死Scroll(){Row({space:8}){ForEach([全部].concat(this.typeStats.map((s:TypeStat)s.fileType)),(t:string){Text(${t}).fontSize(13).padding({left:12,right:12,top:6,bottom:6}).borderRadius(16).backgroundColor(this.typeFiltert?#111827:#FFFFFF).fontColor(this.typeFiltert?Color.White:#4B5563).onClick(()this.switchType(t))},(t:string)t)}.padding({left:16,right:16,top:10})}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width(100%)动态选项[全部].concat(typeStats.map(s s.fileType))——「全部」 数据库里实际存在的类型GROUP BY 结果。为什么动态数据库只有 6 类时显示 7 个 tab某类型文件全删了tab 自动消失——筛选条与数据同步不显示空类型。switchType 清空搜索asyncswitchType(t:string):Promisevoid{this.typeFiltert;this.keyword;awaitthis.applyFilter();}切类型清空关键词——搜索与筛选互斥的一致性维护从搜索状态切到类型筛选清掉搜索词避免残留过滤。五、文件列表图标行每行 类型图标 名称 元信息 删除List({space:8}){ForEach(this.files,(f:FileRecord){ListItem(){Row({space:12}){Text(this.icons[f.fileType]??).fontSize(28).width(48).height(48).borderRadius(10).backgroundColor(#F3F4F6).textAlign(TextAlign.Center)Column({space:3}){Text(f.name).fontSize(15).fontWeight(FontWeight.Medium).maxLines(1)Row({space:8}){Text(f.fileType).fontSize(11).padding({left:6,right:6,top:2,bottom:2}).borderRadius(8).backgroundColor(#DBEAFE).fontColor(#3B82F6)Text(this.fmtSize(f.size)).fontSize(11).fontColor(#9CA3AF)Text(this.fmtTime(f.createdTime)).fontSize(11).fontColor(#9CA3AF)}if(f.tags){Text(${f.tags}).fontSize(11).fontColor(#9CA3AF)}}.alignItems(HorizontalAlign.Start).layoutWeight(1)Text(✕).fontSize(14).fontColor(#CCCCCC).onClick(()this.deleteFile(f))}.width(100%).padding(12).backgroundColor(Color.White).borderRadius(10)}},(f:FileRecord)${f.id}-${f.name})}.width(94%).layoutWeight(1).margin({top:10}).scrollBar(BarState.Off)图标行五要素类型图标28fp emoji 放 48×48 浅灰圆角块#F3F4F6——像文件图标位文件名15fp 中等字重单行截断类型标签蓝色小胶囊#DBEAFE底#3B82F6字——类型可扫读大小 日期灰字fmtSize 换算 fmtTime 月日——文件元信息标签行 需求,产品有 tags 才显示——if 条件渲染。fmtSize 大小换算字节 → 人类可读privatefmtSize(size:number):string{if(size1073741824){return(size/1073741824).toFixed(2) GB;}if(size1048576){return(size/1048576).toFixed(1) MB;}if(size1024){return(size/1024).toFixed(0) KB;}returnsize B;}分级换算GB2 位小数/ MB1 位/ KB0 位/ B——按数量级选单位与精度。800MB → 「800.0 MB」、350MB → 「350.0 MB」、120MB → 「120.0 MB」2.3MB → 「2.3 MB」。六、UI 风格要素一览风格项取值说明页面背景#F8FAFC极浅灰蓝文件管理清爽搜索框白色胶囊圆角 20搜索入口筛选条动态选项 黑底选中与数据同步类型图标28fp emoji 浅灰底文件图标位类型标签蓝底胶囊类型可扫读元信息灰字大小/日期次级信息删除行尾灰色 ✕轻量删除七、文章小结本篇文章完成了实例 16 的 UI 层搜索框实时 LIKE 搜索 动态类型筛选条选项来自 GROUP BY 统计与数据同步 图标行文件列表类型 emoji 图标 名称 类型胶囊 大小/日期 标签 删除。设计核心是「资源管理器感」——图标行五要素扫一眼认文件技术亮点是动态筛选条typeStats 驱动选项无空类型残留与fmtSize 分级换算字节 → GB/MB/KB 按数量级选单位。下一篇16-3深入数据层讲解 LIKE 通配符搜索、GROUP BY 类型统计与批量导入去重。八、搜索框实时交互击键即查库的节奏「实时」的含义onChange在用户每敲一个字符时触发——无需回车、无需按钮。击键 →onSearch(v)→applyFilter()→ 数据库查询 →this.files更新 → List 差量重渲染整条链路毫秒级完成环节触发点职责onChange每敲一个字符更新 keyword 并触发查询applyFilteronSearch 内关键词非空走 search为空回落到类型筛选ForEachfiles 变化key 相同条目复用差异条目重渲染小数据量直接查库18 个文件的本地 SQLite 查询是微秒~毫秒级击键即查无压力不必防抖。数据量上千条后可加 300ms 防抖——只查最后一次击键后的结果privatetimer:number-1;onSearch(v:string):void{this.keywordv;if(this.timer!-1){clearTimeout(this.timer);}this.timersetTimeout((){this.applyFilter();},300);}防抖原理连续击键只重置定时器不查询停顿 300ms 后才真正查一次——把「每键一查」降为「停顿一查」。注意setTimeout在 ArkTS 中返回number定时器句柄必须显式声明类型。清空搜索词删除最后一个字符时 keyword 变空串applyFilter自动回落到类型筛选——搜索与筛选状态无缝切换无需额外分支。九、typeStats 驱动的筛选条统计结果长成 UI筛选条不是写死的 7 个 tab而是由数据库 GROUP BY 统计结果生成。数据流interfaceTypeStat{fileType:string;count:number;totalSize:number;}onPageShow():void{this.loadStats();this.applyFilter();}asyncloadStats():Promisevoid{this.typeStatsawaitFileLibDao.queryTypeStats(this.context);}渲染侧[全部].concat(this.typeStats.map((s: TypeStat) s.fileType))——「全部」永远第一其余 tab 与数据库实际存在的类型一一对应。动态 vs 静态场景静态写死typeStats 动态某类型文件全删tab 残留点开为空tab 自动消失新增类型需改代码自动出现tab 数量固定随数据变化typeStats 的刷新时机onPageShow每次进入页面 删除文件后重查——保证「tab 与数据库一致」绝不出现空类型 tab。十、图标行五要素逐块拆解图标行的「资源管理器感」来自五要素各司其职1. 类型 emoji图标位this.icons[f.fileType] ?? ——映射表命中返回对应 emoji未知类型兜底「」数据库出现新类型 UI 也不空白。48×48 浅灰圆角块 28fp emoji 居中模拟文件图标位。2. 名称主信息15fp Medium 字重 maxLines(1)单行截断——超长省略号收尾行高稳定列表不因文件名长短参差。3. 类型胶囊类型可扫读#DBEAFE底 #3B82F6蓝字小胶囊——与 emoji 双保险扫一眼既能从图标也能从文字认出类型。4. 大小 日期元信息fmtSize(f.size)换算可读大小、fmtTime(f.createdTime)输出月日时间。两个 11fp 灰字#9CA3AF弱化视觉——元信息是辅助不抢主信息权重。5. 标签行扩展信息if (f.tags) Text( f.tags)——有标签才渲染无标签的行不高出一行列表行高统一。要素代码要点视觉职责类型 emojiicons 映射 ?? 28fp 浅灰底图标识别名称maxLines(1)15fp Medium主信息类型胶囊蓝底蓝字11fp 胶囊类型识别大小/日期fmtSize fmtTime11fp 灰字元信息标签if 条件渲染11fp 灰字 扩展信息十一、fmtSize 分级换算数量级决定单位与精度privatefmtSize(size:number):string{if(size1073741824){return(size/1073741824).toFixed(2) GB;}if(size1048576){return(size/1048576).toFixed(1) MB;}if(size1024){return(size/1024).toFixed(0) KB;}returnsize B;}阈值即 1024 的幂1073741824 1024³GB、1048576 1024²MB、1024 1024¹KB——文件大小是二进制单位除数与阈值必须同源不能拿 1000 除却拿 1024 判断否则出现「1023.9 KB」这类怪异值。数量级阈值除数精度示例GB≥ 1024³10737418242 位小数1.25 GBMB≥ 1024²10485761 位小数800.0 MBKB≥ 102410240 位小数800 KBB 1024—整数512 B精度随数量级递减MB 显示 1 位小数已够精确KB 若也显示小数「800.5 KB」占位更宽且无信息量——单位越大需要的有效数字越少。边界值1024 字节落入 KB 分支输出「1 KB」0 字节输出「0 B」。fmtTime 同理分级1 小时内「x 分钟前」、当天「今天 HH:mm」、更早「M月d日」——时间也按距离分级。十二、删除交互确认 双通道刷新行尾灰色 ✕ 是轻量删除入口但直接删有误触风险用promptAction.showDialog确认asyncdeleteFile(f:FileRecord):Promisevoid{constresawaitpromptAction.showDialog({title:删除文件,message:确定删除「${f.name}」,buttons:[{text:取消,color:#9CA3AF},{text:删除,color:#EF4444},],});if(res.index1){awaitFileLibDao.deleteById(this.context,f.id);this.filesthis.files.filter((x:FileRecord)x.id!f.id);this.loadStats();}}删除后两条刷新链路① 列表——本地filter移除已删行比重新查库快② 统计——loadStats()重查 typeStats被删类型的 tab 计数归零后自动消失。res.index 1对应「删除」按钮buttons 从 0 编号index 0 是取消。十三、ArkTS 细节清单细节写法说明对象字面量readonly icons: Recordstring, stringArkTS 要求字面量带显式类型兜底取值this.icons[f.fileType] ?? 索引可能未命中??兜底ForEach key${f.id}-${f.name}key 唯一稳定差量更新回调参数(t: string) t必须显式标注类型无隐式 any组件方法private fmtSize等方法声明为私有setTimeout 句柄private timer: number -1定时器返回 numberForEach key 的坑key 必须全局唯一且稳定——只用f.name会因重名文件 key 冲突导致渲染错乱${f.id}-${f.name}用自增主键保证唯一。无 any 约束ForEach/map 回调、对象属性访问全部显式类型——编译期强约束杜绝隐式 any 的运行时风险。十四、FAQ 问答Q搜索时为什么不做类型叠加过滤A搜索LIKE 全库模糊与筛选类型等值是两种心智模型叠加会让结果语义混乱SQL 也要拼 ORAND 复合条件。本实例选择互斥搜索时忽略筛选切筛选时清空搜索词——交互简单、语义清晰。QtypeStats 会不会和列表不同步A三个刷新时机onPageShow、删除后重查、种子数据导入后。选项来自 GROUP BY 统计而非写死数据变了 tab 跟着变天然一致。QfmtSize 为什么 KB 不保留小数、GB 却保留两位A按数量级给精度KB 数值大整数足够表达GB 数值小1.25GB 与 1.3GB 差约 50MB两位小数才看得出差异。单位越大精度越高——这是「人类可读」的核心。Q删除为什么不直接删、非要弹确认框A元数据删除后不可恢复误触成本高。showDialog是一次性低成本防护——多一步点击换来删除操作的确定性。行尾 ✕ 保持轻量入口确认框承担安全兜底。