HarmonyOS APP<玩转React>开源教程三十一:示例项目下载功能

第31次:示例项目下载功能

本文讲解 DemoDownloadPage.etsDemoProjectData.ets。这是项目里很有"作品集"气质的一部分:它把 11 个示例项目做成了可浏览、可筛选、可下载、可分享的内容中心。


这个页面到底在做什么

很多人第一次看到"成品下载"以为只是一个文件列表,但当前页面其实做了完整的展示与操作闭环:

  • 项目分类筛选
  • 项目预览图展示
  • 权限标签提示
  • 下载 ZIP 包到本地
  • 分享项目压缩包
  • 视频教程弹层

#mermaid-svg-PSOhEfFxw4U17j6Q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PSOhEfFxw4U17j6Q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PSOhEfFxw4U17j6Q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PSOhEfFxw4U17j6Q .error-icon{fill:#552222;}#mermaid-svg-PSOhEfFxw4U17j6Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PSOhEfFxw4U17j6Q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PSOhEfFxw4U17j6Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PSOhEfFxw4U17j6Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PSOhEfFxw4U17j6Q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PSOhEfFxw4U17j6Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PSOhEfFxw4U17j6Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PSOhEfFxw4U17j6Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PSOhEfFxw4U17j6Q .marker.cross{stroke:#333333;}#mermaid-svg-PSOhEfFxw4U17j6Q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PSOhEfFxw4U17j6Q p{margin:0;}#mermaid-svg-PSOhEfFxw4U17j6Q .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PSOhEfFxw4U17j6Q .cluster-label text{fill:#333;}#mermaid-svg-PSOhEfFxw4U17j6Q .cluster-label span{color:#333;}#mermaid-svg-PSOhEfFxw4U17j6Q .cluster-label span p{background-color:transparent;}#mermaid-svg-PSOhEfFxw4U17j6Q .label text,#mermaid-svg-PSOhEfFxw4U17j6Q span{fill:#333;color:#333;}#mermaid-svg-PSOhEfFxw4U17j6Q .node rect,#mermaid-svg-PSOhEfFxw4U17j6Q .node circle,#mermaid-svg-PSOhEfFxw4U17j6Q .node ellipse,#mermaid-svg-PSOhEfFxw4U17j6Q .node polygon,#mermaid-svg-PSOhEfFxw4U17j6Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PSOhEfFxw4U17j6Q .rough-node .label text,#mermaid-svg-PSOhEfFxw4U17j6Q .node .label text,#mermaid-svg-PSOhEfFxw4U17j6Q .image-shape .label,#mermaid-svg-PSOhEfFxw4U17j6Q .icon-shape .label{text-anchor:middle;}#mermaid-svg-PSOhEfFxw4U17j6Q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PSOhEfFxw4U17j6Q .rough-node .label,#mermaid-svg-PSOhEfFxw4U17j6Q .node .label,#mermaid-svg-PSOhEfFxw4U17j6Q .image-shape .label,#mermaid-svg-PSOhEfFxw4U17j6Q .icon-shape .label{text-align:center;}#mermaid-svg-PSOhEfFxw4U17j6Q .node.clickable{cursor:pointer;}#mermaid-svg-PSOhEfFxw4U17j6Q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PSOhEfFxw4U17j6Q .arrowheadPath{fill:#333333;}#mermaid-svg-PSOhEfFxw4U17j6Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PSOhEfFxw4U17j6Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PSOhEfFxw4U17j6Q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PSOhEfFxw4U17j6Q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PSOhEfFxw4U17j6Q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PSOhEfFxw4U17j6Q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PSOhEfFxw4U17j6Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PSOhEfFxw4U17j6Q .cluster text{fill:#333;}#mermaid-svg-PSOhEfFxw4U17j6Q .cluster span{color:#333;}#mermaid-svg-PSOhEfFxw4U17j6Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PSOhEfFxw4U17j6Q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PSOhEfFxw4U17j6Q rect.text{fill:none;stroke-width:0;}#mermaid-svg-PSOhEfFxw4U17j6Q .icon-shape,#mermaid-svg-PSOhEfFxw4U17j6Q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PSOhEfFxw4U17j6Q .icon-shape p,#mermaid-svg-PSOhEfFxw4U17j6Q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PSOhEfFxw4U17j6Q .icon-shape .label rect,#mermaid-svg-PSOhEfFxw4U17j6Q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PSOhEfFxw4U17j6Q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PSOhEfFxw4U17j6Q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PSOhEfFxw4U17j6Q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DemoDownloadPage
DemoProjectData 提供项目数据
分类筛选
项目卡片列表
downloadProject
shareProject
TutorialSheet 视频教程


一、为什么下载页一定要先把项目数据结构化

DemoProjectData.ets 中每个示例项目都带有完整信息:

  • id
  • name
  • description
  • icon
  • folderName
  • zipName
  • imageName
  • permissions
  • category

这说明下载页不是"从文件夹临时扫一遍就展示",而是有明确的数据层。这样做的好处很多:

  • UI 展示字段稳定
  • 分类筛选更简单
  • 权限提示可以直接从数据里读取
  • 未来增加新项目只需要补数据项

这就是典型的"页面不猜资源,数据层提前组织好资源信息"。


二、图文结合看一下页面里真实用到的示例图

当前下载页会直接读取 rawfile 中的预览图。你可以先看几个真实素材:

这些图片的存在,让下载页不再只是冷冰冰的文件列表,而是真正能让用户"看到项目长什么样"。这就是图文结合带来的价值。


三、分类筛选为什么做成横向标签条

示例项目一共有多个分类,例如:

  • 社交
  • 媒体
  • 金融
  • 健康
  • 生活
  • 效率
  • 购物
  • 教育
  • 出行
  • 设计

这类分类数量不少,但每个分类标签都比较短,所以最适合用横向滚动标签条来承载。这样既不占太多垂直空间,也方便用户快速切换。

这和源码学习、开源项目模块的分类条设计是一脉相承的,说明整个项目在"分类导航"这一模式上保持了统一。


四、项目卡片为什么必须包含预览图和权限标签

当前 ProjectItem(project) 卡片里,除了名称和描述,还展示了:

  • 预览图
  • 权限标签
  • 下载按钮
  • 分享按钮

预览图

帮助用户快速判断项目风格和适用方向。

权限标签

告诉用户这个项目可能会涉及哪些系统能力,比如:

  • 相机
  • 麦克风
  • 定位

这一步尤其重要,因为它能帮助用户在下载前建立心理预期。比如看到"相机"权限,就知道这可能是一个拍照类项目;看到"定位"权限,就知道它可能和地图、运动或生活服务有关。


五、下载功能到底做了哪些步骤

downloadProject(project) 的核心流程非常清楚:

  1. 通过 getContext(this) 获取 UIAbilityContext
  2. resourceManager 读取 rawfile 中的 ZIP 内容
  3. 计算本地 filesDir 下载路径
  4. 使用 fileIo 写入本地文件
  5. 成功后弹出提示框展示保存路径

fileIo resourceManager DemoDownloadPage 用户 fileIo resourceManager DemoDownloadPage 用户 #mermaid-svg-qscYunaeq68OkdaD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qscYunaeq68OkdaD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qscYunaeq68OkdaD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qscYunaeq68OkdaD .error-icon{fill:#552222;}#mermaid-svg-qscYunaeq68OkdaD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qscYunaeq68OkdaD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qscYunaeq68OkdaD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qscYunaeq68OkdaD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qscYunaeq68OkdaD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qscYunaeq68OkdaD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qscYunaeq68OkdaD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qscYunaeq68OkdaD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qscYunaeq68OkdaD .marker.cross{stroke:#333333;}#mermaid-svg-qscYunaeq68OkdaD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qscYunaeq68OkdaD p{margin:0;}#mermaid-svg-qscYunaeq68OkdaD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qscYunaeq68OkdaD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qscYunaeq68OkdaD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qscYunaeq68OkdaD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qscYunaeq68OkdaD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qscYunaeq68OkdaD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qscYunaeq68OkdaD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qscYunaeq68OkdaD .sequenceNumber{fill:white;}#mermaid-svg-qscYunaeq68OkdaD #sequencenumber{fill:#333;}#mermaid-svg-qscYunaeq68OkdaD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qscYunaeq68OkdaD .messageText{fill:#333;stroke:none;}#mermaid-svg-qscYunaeq68OkdaD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qscYunaeq68OkdaD .labelText,#mermaid-svg-qscYunaeq68OkdaD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qscYunaeq68OkdaD .loopText,#mermaid-svg-qscYunaeq68OkdaD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qscYunaeq68OkdaD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qscYunaeq68OkdaD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qscYunaeq68OkdaD .noteText,#mermaid-svg-qscYunaeq68OkdaD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qscYunaeq68OkdaD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qscYunaeq68OkdaD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qscYunaeq68OkdaD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qscYunaeq68OkdaD .actorPopupMenu{position:absolute;}#mermaid-svg-qscYunaeq68OkdaD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qscYunaeq68OkdaD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qscYunaeq68OkdaD .actor-man circle,#mermaid-svg-qscYunaeq68OkdaD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qscYunaeq68OkdaD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 点击下载getRawFileContent(zipName)openSync / writeSync / closeSyncAlertDialog 提示下载成功

这是一条非常典型的鸿蒙本地资源导出链路。教程里一定要按这个真实流程讲,不然读者只会记住"有个下载按钮",却不知道背后做了什么。

项目里真正执行下载的代码如下:

ts 复制代码
const context = getContext(this) as common.UIAbilityContext;
const resourceManager = context.resourceManager;

const zipData = await resourceManager.getRawFileContent(project.zipName);

const filesDir = context.filesDir;
const downloadPath = `${filesDir}/${project.zipName}`;

const file = fileIo.openSync(downloadPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
fileIo.writeSync(file.fd, zipData.buffer);
fileIo.closeSync(file);

六、分享功能为什么要先保存本地 ZIP

shareProject(project) 并不是直接把 rawfile 资源拿去分享,而是先把 ZIP 从 rawfile 拷到本地,再通过 ShareService 分享本地文件路径。

这一步说明一个很重要的工程实践:

  • 应用内资源不一定能直接拿去给外部系统分享
  • 更稳妥的做法是先转存到可访问的本地路径

如果分享失败,页面还会降级为文字提示方案。这种"主方案 + 兜底方案"的设计非常值得学习。


七、视频教程弹层为什么也是这页的一部分

下载页底部还有一个"使用教程"入口,点击后会通过 bindSheet 弹出 TutorialSheet(),里面用 Video 组件播放 使用教程.mp4

这说明作者不希望用户只把项目下载下来,还希望用户知道怎么使用这些 DEMO。对于学习型资源中心来说,这是非常合理的补充:

  • 下载解决"拿到资源"
  • 视频解决"知道怎么用"

这就是很典型的图文 + 视频结合式说明页面。

视频教程弹层用到的真实代码如下:

ts 复制代码
Video({
  src: $rawfile('使用教程.mp4'),
  controller: this.videoController
})
  .width('100%')
  .height(220)
  .controls(true)
  .autoPlay(false)
  .onStart(() => {
    this.isPlaying = true;
  })

八、实操步骤

  1. 打开下载页,切换几个分类标签。
  2. 对比不同卡片的预览图、描述和权限标签。
  3. 点击下载,确认是否能成功保存 ZIP。
  4. 点击分享,观察正常路径和异常兜底提示。
  5. 打开视频教程弹层,测试播放、暂停、重播。

只要这几个动作都自己走过一遍,你对这一页的实现思路就会很清楚。


九、本篇常见坑

1. 只有文件名,没有视觉预览

用户很难判断项目是否值得下载。

2. 下载前不提示项目所需权限

会降低用户对项目内容的预期清晰度。

3. 分享时直接拿 rawfile 资源去发

更稳定的方式是先导出到本地。

4. 只有下载,没有教程

这样资源拿到了,但上手成本仍然很高。


九、本篇常见坑

1. 只有文件名,没有视觉预览

用户很难判断项目是否值得下载。

2. 下载前不提示项目所需权限

会降低用户对项目内容的预期清晰度。

3. 分享时直接拿 rawfile 资源去发

更稳定的方式是先导出到本地。

4. 只有下载,没有教程

这样资源拿到了,但上手成本仍然很高。


本篇小结

示例项目下载功能的价值,不只是"导出几个 ZIP 文件",而是把项目资源做成了一个有预览、有分类、有说明、有下载、有分享、有教程的完整内容中心。

这一页最值得学习的有两点:

  • 资源型页面如何做得有吸引力
  • 鸿蒙本地资源如何转换成可下载、可分享的文件

课后练习

  1. 观察 DemoProjectData,思考如果要增加"推荐指数"字段,最适合在下载页的什么位置展示。
  2. 想一想为什么视频教程适合放在下载页,而不是首页。
  3. 设计一个"已下载"状态标记方案,说明你会把它存在哪里。
相关推荐
大鱼前端2 小时前
Veaury:让Vue和React组件在同一应用中共存的神器
前端·vue.js·react.js
五月君_2 小时前
继 React、Vue 之后,Three.js 也有 Skills 了!AI 写 3D 终于不“晕”了
javascript·vue.js·人工智能·react.js·3d
小崽崽12 小时前
如何实现React 19+Vite+TypeScript技术栈告别高薪主播!从零打造 24 小时“AI 销冠”:星云数字人直播间全链路实战
人工智能·react.js·typescript
想你依然心痛2 小时前
HarmonyOS 6(API 23)智能体驱动的沉浸式AR量子计算实验室
ar·harmonyos·量子计算·智能体
技术路线图2 小时前
鸿蒙系统小红书应用分身设置教程(2026详细版)
华为·harmonyos
MemoriKu2 小时前
【端侧 AI 部署】MobileCLIP 导出 ONNX/TFLite 并发布到 Hugging Face 的完整实践
大数据·人工智能·elasticsearch·搜索引擎·重构·开源
科技与数码2 小时前
鸿蒙智能体框架HMAF与智能化升级全解析
华为·harmonyos
不羁的木木3 小时前
HarmonyOS文件基础服务(Core File Kit)实战演练01-核心概念与架构设计
华为·harmonyos
大雷神3 小时前
第28篇|相机失败态:没有权限、没有设备、会话失败时如何提示
harmonyos