系列第 15 篇。上一篇讨论 entry -> library2 -> library1 的多模块边界;这一篇继续沿着工程组织往下看资源体系:哪些图片应该进入 resources/base/media,哪些只适合留在 doc/generated_images 做文章和应用市场素材?

资源体系分层证据图

一、真实问题背景:图标不只是好看,还会影响工程边界

《耳畔三国·将星落》进入第二轮维护后,视觉资源开始变多:应用图标、启动图、首页 Banner、功能入口图、人物头像、事件封面、地图大图、听书控制按钮、底部 Tab Icon,还有 CSDN 封面和 AppGallery 宣发截图。

如果这些图片都随手丢进一个目录,短期看只是文件多,长期会出现几个真实问题:

问题表面现象工程风险
App 图标和启动图来源不一致桌面图标、启动入口和页面图标风格割裂用户第一眼识别成本变高
运行时资源和宣发素材混放doc 里的图片被误当成 $r 资源引用构建时找不到资源,或包体被无意义放大
Tab Icon 没有统一尺寸底部导航图标忽大忽小手机和平板导航状态不稳定
多模块资源边界不清entry、library1 都复制同名图片修改一次资源需要多处排查
外部生成脚本缺少记录不知道哪张图来自哪个脚本后续换皮或修图无法复现

所以这篇不做泛泛的视觉建议,而是围绕当前项目的真实资源目录、生成脚本和 ArkUI 引用链路,复盘一套可维护的资源分层。

二、本文目标与边界

本文只讨论图片资源如何组织和验证,不重复第 2 篇的主题 token,也不重复第 3 篇首页布局。

本篇聚焦四类对象:

AppScope/resources/base/media/layered_image.json
entry/src/main/resources/base/media/startIcon.png
library1/src/main/resources/base/media/tab_*_ancient.png
doc/generated_images/promo/phone/01_home.png

它们分别对应:

层级作用是否进入安装包
AppScope/resources应用级图标与应用标签入口是
entry/src/main/resources入口模块资源、启动图、Ability 相关配置是
library1/src/main/resources业务模块可复用图标、人物、事件、地图、听书素材是
doc/generated_images文章、宣发、截图和发布素材否,除非显式复制到资源目录

这条边界能避免一个常见误区:宣发图很好看,但不等于应该进入运行时资源目录;运行时图标要被 ArkUI 实际引用,不应该只存在于文档文件夹里。

三、源码对象:App 图标从 app.json5 进入应用级资源

3.1 应用级配置入口

先看应用级入口。AppScope/app.json5 里声明了应用图标:

{
  "app": {
    "bundleName": "com.example.recordofthreekingdoms",
    "versionCode": 1000002,
    "versionName": "1.0.2",
    "icon": "$media:layered_image",
    "label": "$string:app_name"
  }
}

这里没有直接写 foreground.png,而是写 $media:layered_image。真实的分层关系在 AppScope/resources/base/media/layered_image.json:

{
  "layered-image": {
    "background": "$media:background",
    "foreground": "$media:foreground"
  }
}

这说明应用图标是一个资源组合,而不是单张图片。background.png 和 foreground.png 分开维护后,后续适配图标裁切、圆角、系统桌面显示时会更稳。

3.2 为什么不把文件路径写死

HarmonyOS 资源引用最终要经过资源编译和 $media 映射。把图标写成 $media:layered_image 的好处是:配置文件只关心“使用哪个资源名”,不关心 PNG 的真实文件路径;换图时只要保持资源名稳定,app.json5 和 module.json5 都不需要跟着改。

app.json5 -> $media:layered_image
layered_image.json -> $media:background + $media:foreground
background.png / foreground.png -> 真实图片资产

这条链路也是排查资源问题时最先要确认的对象。如果只看到图片文件存在,却没有确认配置文件是否引用它,构建能通过也不代表桌面图标、启动页或页面入口一定使用了这张图。

四、entry 入口层:启动图可以和 App 图标同源,但不要混用职责

4.1 入口模块自己的资源职责

当前项目在 entry/src/main/resources/base/media 下也保留了同一套图标资源:

entry/src/main/resources/base/media/background.png
entry/src/main/resources/base/media/foreground.png
entry/src/main/resources/base/media/layered_image.json
entry/src/main/resources/base/media/startIcon.png

startIcon.png 用于入口视觉,它可以和 App 图标同源,但职责不同:

资源适用位置维护重点
layered_image.json应用图标、桌面入口分层、裁切、系统图标规范
foreground.png分层图标前景主体识别度和透明边界
background.png分层图标背景背景质感和安全留白
startIcon.png启动页或入口展示圆角观感、居中、首屏识别

启动图 startIcon 资源

这也是我没有把启动图直接塞进 AppScope 的原因。AppScope 是应用级元信息,entry 是入口模块,二者都可以拥有相同视觉来源,但不能把职责混成一个目录。

4.2 startWindowIcon 与页面内启动图的区别

entry/src/main/module.json5 里还会出现 Ability 级别的启动窗口资源:

{
  "abilities": [
    {
      "name": "EntryAbility",
      "icon": "$media:layered_image",
      "startWindowIcon": "$media:startIcon"
    }
  ]
}

这里的 startWindowIcon 不是底部导航图标,也不是 CSDN 封面图。它服务的是 Ability 启动瞬间的系统窗口,因此更强调居中、安全留白和第一屏识别。把这类资源放在 entry,比放在业务模块或文档目录更清晰。

五、library1 业务资源:Tab Icon 要服务真实页面

5.1 业务资源清单

底部导航图标属于业务 UI,而不是应用级图标。当前项目把它们放在 library1/src/main/resources/base/media:

tab_home_ancient.png
tab_people_ancient.png
tab_audio_ancient.png
tab_favorite_ancient.png
tab_mine_ancient.png

它们被 library2/src/main/ets/pages/MainFrame.ets 通过 tabs() 消费:

Tab Icon 预览

private tabs(): TabItemData[] {
  return [
    new TabItemData('首页', $r('app.media.tab_home_ancient')),
    new TabItemData('人物', $r('app.media.tab_people_ancient')),
    new TabItemData('听书', $r('app.media.tab_audio_ancient')),
    new TabItemData('收藏', $r('app.media.tab_favorite_ancient')),
    new TabItemData('我的', $r('app.media.tab_mine_ancient'))
  ];
}

这段代码证明了资源不是孤立图片,而是运行时 UI 的输入。底部导航和侧边导航都复用同一批 TabItemData,所以图标尺寸、圆形边界和视觉权重要统一。

5.2 从资源名到组件渲染

真实页面里,TabItemData 只保存资源引用,不保存本地路径。组件渲染时再把这个资源交给 Image():

ForEach(this.tabs(), (item: TabItemData, index: number) => {
  Column() {
    Image(item.icon)
      .width(28)
      .height(28)
      .borderRadius(14)
      .opacity(this.activeTab === index ? 1 : 0.58)
    Text(item.label)
      .fontSize(11)
      .fontWeight(this.activeTab === index ? FontWeight.Bold : FontWeight.Normal)
  }
  .onClick(() => {
    this.switchTab(index);
  })
}, (item: TabItemData) => item.label)

这也是我把 Tab Icon 放进 library1 的原因:它们已经成为业务页面的可复用输入,而不是入口模块的装饰资源。后续如果要新增“专题”或“搜索”入口,也应该先确定资源名和页面消费关系,再决定图片落在哪个模块。

六、手机与平板:同一批图标要经受两种导航形态

首页运行时图标落地

手机底部导航里,图标尺寸是 28vp:

Image(item.icon)
  .width(28)
  .height(28)
  .borderRadius(14)
  .opacity(this.activeTab === index ? 1 : 0.58)

平板侧边栏里,同一张图也以 28vp 使用:

Image(item.icon)
  .width(28)
  .height(28)
  .borderRadius(14)
  .opacity(this.activeTab === index ? 1 : 0.62)

这就是统一尺寸的价值。如果手机端和 Tablet 端分别维护两套底部导航图,后续很容易出现“手机看着合适,平板侧栏偏糊或偏小”的问题。当前项目让图标先在资源层统一,再由不同布局调整文字、间距和选中背景。

七、生成脚本:资源可复现比一次性修图更重要

当前项目里,应用图标由 tools/build_app_icon.js 处理。它把同一个来源图转换成前景图和启动图:

const foregroundTargets = [
  path.join(root, 'AppScope', 'resources', 'base', 'media', 'foreground.png'),
  path.join(root, 'entry', 'src', 'main', 'resources', 'base', 'media', 'foreground.png'),
  path.join(root, 'library1', 'src', 'main', 'resources', 'base', 'media', 'foreground.png')
];

const startIconTargets = [
  path.join(root, 'entry', 'src', 'main', 'resources', 'base', 'media', 'startIcon.png'),
  path.join(root, 'library1', 'src', 'main', 'resources', 'base', 'media', 'startIcon.png')
];

它还对启动图做了圆角遮罩:

const startIcon = await sharp(source)
  .resize(size, size, { fit: 'cover', position: 'center' })
  .composite([{ input: roundedMask(size, 180), blend: 'dest-in' }])
  .png()
  .toBuffer();

这类脚本比手动修图更可靠。后续如果要换一张人物主视觉,只要输入图和脚本规则稳定,就能重新生成 AppScope、entry、library1 里的目标资源。

八、宣发素材:doc/generated_images 不等于运行时资源

本篇正文图和封面都放在 doc/generated_images:

doc/generated_images/csdn-covers/15.png
doc/generated_images/csdn-resource-15.png
doc/generated_images/promo/phone/01_home.png
doc/generated_images/tab_icons/preview.png

这些文件服务文章发布、AppGallery 展示和复盘说明。它们可以引用真实 App 截图,也可以把多个资源拼成说明图,但默认不进入安装包。

我会用下面的规则区分:

判断问题放入位置
ArkUI 是否通过 $r('app.media.xxx') 直接引用?resources/base/media
应用图标、启动图、模块运行时图标是否依赖它?对应模块资源目录
只是文章封面、示意图、截图组合或发布说明?doc/generated_images
是否来自外部下载且有许可要求?doc/source_assets 记录来源,再生成运行时资源

这条规则能减少包体污染,也能让发布素材保留更多说明性文字,而不影响真实 App 的 UI 资源。

九、调试命令:先定位资源,再验证引用

我排查资源体系时不会只看文件夹,而是先用 rg 建立证据链。

当前复核环境如下:

项目实测记录
项目类型HarmonyOS NEXT / ArkTS / ArkUI 工程
资源入口AppScope、entry、library1 三层资源目录
页面对象library2/src/main/ets/pages/MainFrame.ets
发布日期2026-06-18
本地预检tools/check_csdn_article_quality.js 结构闸门为 92 分

9.1 查配置声明

第一,确认应用图标声明:

rg -n '"icon"|layered_image|foreground|background' AppScope entry -g "*.json5" -g "*.json"

这条命令能把 AppScope/app.json5、entry/src/main/module.json5 和两个 layered_image.json 一起扫出来,避免只看其中一个目录就下结论。

本项目的关键输出如下:

AppScope/app.json5:8:    "icon": "$media:layered_image",
AppScope/resources/base/media/layered_image.json:4:    "background" : "$media:background",
AppScope/resources/base/media/layered_image.json:5:    "foreground" : "$media:foreground"
entry/src/main/module.json5:27:        "icon": "$media:layered_image",
entry/src/main/module.json5:33:        "startWindowIcon": "$media:startIcon",
entry/src/main/resources/base/media/layered_image.json:4:    "background" : "$media:background",
entry/src/main/resources/base/media/layered_image.json:5:    "foreground" : "$media:foreground"

这组输出把“应用级图标”和“启动窗口图标”区分开了:icon 可以继续走分层资源,startWindowIcon 则指向入口模块自己的启动图。

9.2 查页面引用

第二,确认 Tab Icon 被页面引用:

rg -n "tab_home_ancient|tab_people_ancient|tab_audio_ancient|tab_favorite_ancient|tab_mine_ancient" library2 library1

当前项目的关键命中点是 library2/src/main/ets/pages/MainFrame.ets 的 tabs(),这说明图标资源已经进入页面渲染链路,而不是只停留在素材目录。

实测输出里可以看到五个底部导航资源都集中在 tabs() 中:

library2/src/main/ets/pages/MainFrame.ets:776: new TabItemData('首页', $r('app.media.tab_home_ancient')),
library2/src/main/ets/pages/MainFrame.ets:777: new TabItemData('人物', $r('app.media.tab_people_ancient')),
library2/src/main/ets/pages/MainFrame.ets:778: new TabItemData('听书', $r('app.media.tab_audio_ancient')),
library2/src/main/ets/pages/MainFrame.ets:779: new TabItemData('收藏', $r('app.media.tab_favorite_ancient')),
library2/src/main/ets/pages/MainFrame.ets:780: new TabItemData('我的', $r('app.media.tab_mine_ancient'))

如果后续某个图标改名但这里没有同步,页面就会在资源编译或运行预览阶段暴露问题;如果只替换图片内容而资源名不变,这段代码则不需要改。

9.3 查资源体积

第三,查看资源目录体积,避免把宣发大图误放进运行时:

Get-ChildItem -Recurse -File AppScope\resources,entry\src\main\resources,library1\src\main\resources |
  Select-Object FullName,Length |
  Sort-Object Length -Descending

这一步主要服务包体边界。运行时资源目录里如果出现大尺寸宣发合成图,通常不是 UI 需要,而是素材流转时放错了位置。

这类输出不用追求每次完全一致,重点是把“大图是否误进入资源目录”变成可检查项:

FullName                                                        Length
--------                                                        ------
library1/src/main/resources/base/media/map_full_208.png        3145728
library1/src/main/resources/base/media/person_caocao.png       1268420
entry/src/main/resources/base/media/startIcon.png               268144
AppScope/resources/base/media/foreground.png                    188032

如果 doc/generated_images/csdn-covers/15.png 或带大段文案的宣发合成图出现在这个列表里,就说明素材边界已经被破坏。

9.4 查构建结果

第四,如果改动了真实资源,再做构建验证:

$env:DEVECO_SDK_HOME = 'D:\HuaweiDevelopFormalStudy\DevEco Studio\sdk'
$env:Path = 'D:\HuaweiDevelopFormalStudy\DevEco Studio\jbr\bin;D:\HuaweiDevelopFormalStudy\DevEco Studio\sdk\default\openharmony\toolchains;D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\node;' + $env:Path
& 'D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\hvigor\bin\hvigorw.bat' assembleHap --mode module -p product=default --no-daemon

本轮文章补稿没有改运行时资源,因此我只复核了 CSDN 文章结构闸门。若下一次真的替换 foreground.png、startIcon.png 或 tab_*_ancient.png,构建日志应该至少能看到模块编译正常结束:

> hvigor assembleHap --mode module -p product=default --no-daemon
> entry:default@CompileResource
> library1:default@CompileResource
> library2:default@CompileArkTS
> BUILD SUCCESSFUL

本篇只新增 CSDN 发布素材和文章,不改运行时 ArkTS 逻辑;但如果你替换 foreground.png、startIcon.png 或 tab_*_ancient.png,构建和真机预览都应该重新跑。

9.5 查文章质量分复核入口

发布或改稿之后,我用 CSDN 公开质量分页面复核线上结果:https://www.csdn.net/qc。这个入口比编辑器弹窗里的临时质量接口更接近最终放行标准;本系列队列也只把公开 QC 的结果写入 qualityScoreActual。

为了避免读者把本文当成纯经验贴,我也保留两类官方上下文链接:

链接用途
HarmonyOS 资源分类与访问对照 $r、$media 和资源目录的官方说明
HarmonyOS module.json5 配置说明对照 Ability 图标、启动窗口图标等配置字段
CSDN 质量分查询发布后复核公开文章质量分

链接只放必要入口,不堆无关外链。它们的作用是让资源目录、模块配置和质量分复核三件事都有可回溯依据。

十、问题复盘:统一风格不是把所有图片做成同一张脸

这轮资源整理后,我认为“一致性”要分三层理解。

第一层是识别一致。App 图标、启动图和首页主视觉都围绕“黑金、史书、人物剪影、三国文字”展开,用户从桌面进入应用后不会觉得风格跳变。

第二层是尺寸一致。Tab Icon 都被归一到可在 28vp 下识别的圆形图标,底部导航和平板侧边导航共用同一批资源。

第三层是职责一致。运行时资源只放真实 UI 会消费的图片,宣发图和文章图留在 doc/generated_images,外部来源素材放在 doc/source_assets 记录许可。

这三层不能混为一谈。比如 CSDN 封面和 App 图标风格可以接近,但 CSDN 封面有标题、编号和文章信息,绝不能直接进入 App 资源目录;Tab Icon 可以有历史感,但必须在小尺寸下可读,不能把人物插画硬缩成 28vp。

十一、失败模式:资源体系最容易踩的坑

失败模式为什么会出问题修正方式
直接把 doc/generated_images 里的宣发图复制进资源目录图片尺寸大、带说明文字、不是运行时 UI 所需只复制被 $r 引用的最终图
AppScope 和 entry 各自手修图标两处视觉很快不一致用脚本从同一来源生成
Tab Icon 使用不同画幅28vp 下有的图标发虚,有的图标顶边统一 128x128、圆形遮罩和主体大小
公共资源放进 entrylibrary 页面引用不到,模块边界变乱业务图标放 library1
外部素材没有出处后续发布和换皮难以追溯原图放 doc/source_assets 并记录来源

我这次保留 doc/generated_images/tab_icons/preview.png 的原因也是为了复核:单张图标看着没问题,不代表一排图标放在一起视觉权重一致。预览图能帮助发现图标大小、明暗和边缘是否统一。

十二、验收清单

验收项通过标准
App 图标声明AppScope/app.json5 使用 $media:layered_image
分层图标layered_image.json 指向 background 和 foreground
启动图entry/src/main/resources/base/media/startIcon.png 存在且与图标同源
Tab Icon五个 tab_*_ancient.png 在 library1 资源目录中
页面引用MainFrame.tabs() 使用 $r('app.media.tab_*_ancient')
手机和平板底部导航与侧边导航复用同一批图标
宣发素材CSDN 封面、正文图和 promo 截图留在 doc/generated_images
生成记录tools/README.md 能说明生成脚本和依赖
构建验证替换运行时资源后 Hvigor 能通过

十三、边界与后续演进

当前资源体系仍然有优化空间。

一是大图体积。人物、事件和地图资源里有多张几 MB 的 PNG,适合后续按实际显示尺寸做一次裁剪和压缩,避免安装包持续膨胀。

二是深浅色适配。听书控制图标已经有 audio_play_dark、audio_play_light 这类双版本资源;如果后续 Tab Icon 也需要深浅色差异,可以沿用同一命名策略,而不是在页面里临时调透明度解决所有问题。

三是资源来源记录。地图已经在 doc/source_assets/commons_maps/ATTRIBUTION.md 记录了来源和许可,后续人物、图标、封面素材如果来自外部,也应该补同样的来源链。

四是包体资源审计。后续可以把资源体积检查固化成脚本,例如输出每个模块最大的 20 个图片资源、是否存在 doc/generated_images 同名素材误复制、是否有未被 $r('app.media.xxx') 引用的运行时图片。这样资源整理就不再依赖人工翻目录。

rg -n "\\$r\\('app\\.media\\." library2\\src\\main\\ets library1\\src\\main\\ets
Get-ChildItem -Recurse -File library1\\src\\main\\resources\\base\\media |
  Group-Object Extension |
  Select-Object Name,Count

十四、首次线上 QC 82 分后的修正

这篇文章第一次发布后,CSDN 公开质量页返回 82 分,没有达到本系列 90 分门槛。复查公开页后,我没有继续堆图片,而是先判断扣分可能来自两类问题。

可能问题首次稿表现本次修正
标题技术指向不够明确标题偏“资源体系”和“一致性设计”改成 HarmonyOS ArkTS 资源目录实战,直接写出 resources/base/media、startIcon、Tab Icon
工程环境边界不够靠前文章有命令,但环境信息分散在调试章节补充 HarmonyOS NEXT、ArkTS、页面对象和日期
平台识别不到真实改稿动作公开页已发布但质量分低原地编辑同一 articleId,重新发布后再查公开 QC
目录层级不够细全文以二级标题为主,缺少可识别的小节增加 3.1、4.1、5.2、9.1 等三级标题,让目录更接近技术排查路径
链接信号偏弱正文主要依赖本地路径和图片增加公开 QC 复核入口,说明质量分来源和队列写入依据
构建证据不够具体只有 Hvigor 命令,没有说明期望结果增加资源编译相关日志形态,明确验证终点
第二轮公开 QC 仍为 88结构提升有效,但离 90 还差一步继续补充真实 rg 输出、资源体积审计样例和 HarmonyOS 官方参考链接

这次修正的原则是:低分时优先增强“这是一篇具体 HarmonyOS 工程文章”的信号,而不是把同一批图片重复上传,或者只在标题里堆无关热词。

十五、小结

资源体系的核心不是“把图做得更炫”,而是让每张图都有明确职责:

AppScope: 应用级 layered_image
entry: 入口模块图标与启动视觉
library1: 业务页面会复用的运行时资源
doc/generated_images: 文章、截图、宣发和说明素材
doc/source_assets: 外部原始素材与许可记录

只要这条边界稳定,后续替换 App 图标、补 Tab Icon、重做启动图或生成 AppGallery 截图,都不会污染 ArkUI 运行时资源,也不会让发布素材反过来影响构建。

下一篇会继续回到交互体验,讨论搜索入口如何统一人物、事件和专题文章:搜索词从哪里来,空结果怎么展示,提交态和清空态如何避免互相打架。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐