一、技术前言:当软件下载站遇见 ArkWeb 双 URL 溯源能力

在这里插入图片描述

HarmonyOS 的 ArkUI 框架是华为面向全场景分布式设备打造的声明式 UI 开发范式,以 ArkTS 语言为根基,在 TypeScript 的类型安全之上引入了 @Entry@Component@State@Builder@Observed 等装饰器体系。开发者通过接近自然语言的方式描述界面结构,框架则采用数据驱动渲染机制——当状态变量发生变更时,自动精确刷新与该状态绑定的组件节点,无需手动调用 findViewByIdsetText。这种范式极大地简化了复杂界面的状态管理,特别适合需要频繁局部刷新的软件下载站类应用:下载进度条实时更新、下载记录动态增删、环形图呼吸动画联动重绘,都可以通过状态绑定自动完成。

在这里插入图片描述
ArkWeb 是 HarmonyOS 提供的 Web 组件能力套件,它让开发者可以在 ArkUI 应用中嵌入一个功能完整的浏览器内核。通过 Web 组件和 webview.WebviewController 控制器,应用可以加载任意网页、执行 JavaScript、拦截网络请求、管理 Cookie 和缓存。但 ArkWeb 真正的差异化价值在于其下载代理体系——WebDownloadDelegate。这个代理类提供了四个完整的下载生命周期回调:onBeforeDownload(下载开始前)、onDownloadUpdated(下载进行中)、onDownloadFailed(下载失败)、onDownloadFinish(下载完成)。开发者注册代理后,无论是网页内用户点击下载链接,还是应用侧通过 startDownload 主动发起下载,都会依次进入这四个回调,使应用获得了对下载全流程的精细控制权。

在这里插入图片描述
HarmonyOS 6.1.1 为 WebDownloadDelegateonDownloadFinish 回调带来了两个重磅新增接口——getOriginalUrl()getReferrerUrl()getOriginalUrl() 返回下载项的原始 URL 地址,即文件在网络上的真实直链位置,例如 https://dl.jisuxia.cn/client/JiSuXia_v6.1.1_20260825.apkgetReferrerUrl() 返回引用页 URL 地址,即用户是从哪个页面触发了这次下载,例如 https://www.jisuxia.cn/client/download?from=home_banner。这两个接口的组合实现了"双 URL 溯源"能力——让每一次下载都能完整追溯"从哪个页面、下载的哪个直链",来源信息不再是一团迷雾,而是清晰可审计的双链条记录。

在这里插入图片描述
Canvas 绘图能力是 ArkUI 框架的原生图形渲染基础。通过 Canvas 组件和 CanvasRenderingContext2D 上下文对象,开发者可以使用与 Web Canvas API 高度一致的接口进行 2D 图形绘制——路径绘制(beginPath/moveTo/arc/closePath)、填充(fill)、清除(clearRect)、文字渲染(fillText)、矩形绘制(fillRect)等。本应用在首页利用 Canvas 绘制下载分类占比环形图,通过扇区填充、中心镂空、中心文字叠加和右侧图例四个步骤,实现了一张信息密度高、视觉效果好的数据可视化图表。更巧妙的是,环形图与呼吸动画定时器联动,每秒根据 breath 状态调整半径倍率(1.04 或 0.96)实现脉动效果,让图表"活"起来。

在这里插入图片描述
软件下载站是互联网最古老的业务形态之一。从早期的 FTP 站点、到后来的第三方下载站、再到如今的厂商应用市场,软件分发的形式不断演进,但核心痛点始终存在:用户下载了一个安装包后,往往无法追溯它是从哪里来的。传统下载管理器只记录文件名和大小,丢失了"来源页面"这一关键信息。在安全审计场景下,如果一个安装包被举报存在风险,运营人员需要快速定位它是在哪个页面被推广的、真实的下载直链是什么,以判断是渠道被劫持还是源头被篡改。双 URL 溯源能力恰好填补了这一审计链条的空白。

在这里插入图片描述
本应用名为"极速下",定位为软件下载站平台,采用深空黑(#0A1220)+ 电光蓝(#3D8BFF)+ 荧光青(#35E0C8)的深色主题方案。深空黑作为背景色不是纯黑,而是带有微弱蓝调的深色,在 OLED 屏幕上具有显著的省电效果,同时降低了暗光环境下的视觉刺激。电光蓝作为品牌主色用于按钮、选中态、渐变起点,荧光青作为强调色用于高亮链接、趋势标记、环形图扇区,两者搭配创造了科技感十足的视觉张力。应用设计了四个功能完全独立的 Tab 页面:首页(今日精选渐变大卡 + 大编号热软榜 + Canvas 环形图)、网页(地址栏 + 快捷站点 + Web 组件 + 主动下载演示)、下载(进度条 + 双 URL 溯源列表 + 代码预览卡)、我的(用户等级渐变大卡 + 下载统计 + 功能清单),每个 Tab 的布局都经过差异化设计。同时应用内置了完整的弹窗系统,支持下载任务的新建、备注编辑、删除确认全流程操作。

在这里插入图片描述

二、整体架构流程图

极速下主页面

颜色系统 ColorPalette

常量定义 Tab/Cate/Rec/Site/Ring/Soft/Download/Stat

辅助函数 trendColor/trendIcon/siteHost

数据模型 @Observed

组件主体 状态管理 + 业务方法

Builder 函数群

SoftItem 软件榜单条目

DownloadRecord 下载记录双URL载体

UserStat 我的页功能清单

Tab状态 currentTab

弹窗状态 addModal/editModal/delModal

Canvas状态 canvasReady/ringCtx

ArkWeb状态 webController/downloadDelegate

业务方法 setupDownloadDelegate/loadUrl/triggerDownload/drawRingChart

弹窗业务 saveDownload/updateRecord/delRecord

生命周期 aboutToAppear/aboutToDisappear

headerMain 头部渐变Banner+搜索+chips

tabHome 首页Tab

tabWeb 网页Tab ArkWeb特性页

tabDownload 下载Tab 双URL溯源

tabMine 我的Tab

recBanner/rankCard/chartCard 首页子构建

tabBar 底部导航

modalOverlay/panelAdd/panelEdit/panelDel 弹窗系统

WebDownloadDelegate

onBeforeDownload 开始前

onDownloadUpdated 进行中

onDownloadFailed 失败

onDownloadFinish 完成

6.1.1新特性

getOriginalUrl 原始URL

getReferrerUrl 引用页URL

呼吸动画定时器 breath

三、模块导入与依赖引入

3.1 导入语句

import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';

这两行导入语句是整个 ArkWeb 下载溯源功能的基石。第一行从 @kit.ArkWeb 中导入了 webview 命名空间,它是 ArkWeb 组件体系的核心入口。通过 webview.WebviewController 可以创建 Web 控制器实例,用于加载网页、绑定下载代理、主动发起下载等操作。通过 webview.WebDownloadDelegate 可以创建下载代理实例,用于注册下载生命周期的四个回调。通过 webview.WebDownloadItem 可以在回调中接收下载项对象,从中获取文件名、进度、总字节数以及 6.1.1 新增的双 URL 信息。

第二行从 @kit.BasicServicesKit 中导入了 BusinessError 类型。这是 HarmonyOS 统一的错误处理接口,包含 code(错误码)和 message(错误描述)两个字段。在调用 setDownloadDelegate 绑定代理和 startDownload 发起下载时,如果操作失败会抛出 BusinessError 异常,通过 try-catch 捕获后可以精确读取错误码和错误消息,向用户展示友好的失败提示信息。

四、颜色系统设计

4.1 颜色接口定义

interface ColorPalette {
  bg: string;
  card: string;
  chip: string;
  title: string;
  sub: string;
  text3: string;
  blue: string;
  blueD: string;
  cyan: string;
  red: string;
  green: string;
  gold: string;
  line: string;
  tabOn: string;
  mask: string;
  codeBg: string;
}

ColorPalette 接口将页面用到的所有颜色字段集中声明为一个类型契约。这种设计的好处在于:当需要切换主题时,只需创建一个新的 ColorPalette 实现对象替换 COLORS 常量即可,无需在代码中逐一搜索替换散落的颜色值。接口中的字段涵盖了界面所需的全部色彩角色:bg 是页面背景色,card 是卡片背景色,chip 是标签芯片背景色,title 是主标题文字色,sub 是次要文字色,text3 是三级辅助文字色,blueblueD 是品牌主色及其深色变体,cyan 是荧光青强调色,red/green/gold 分别用于警示/正向/高亮场景,line 是分割线颜色,tabOn 是底部 Tab 选中色,mask 是弹窗遮罩色,codeBg 是代码预览卡背景色。

4.2 深色主题色板常量

const COLORS: ColorPalette = {
  bg: '#0A1220',
  card: '#111C2E',
  chip: '#1A2A44',
  title: '#EAF2FF',
  sub: '#9FB4D8',
  text3: '#5E729A',
  blue: '#3D8BFF',
  blueD: '#1E5FD0',
  cyan: '#35E0C8',
  red: '#FF6B7E',
  green: '#5ED491',
  gold: '#FFD36E',
  line: '#22334F',
  tabOn: '#3D8BFF',
  mask: 'rgba(4,10,20,0.66)',
  codeBg: '#0A1830'
};

深空黑 #0A1220 作为页面背景色,不是简单的纯黑,而是带有微弱蓝调的深色。这种蓝调暗色在 OLED 屏幕上具有显著的省电效果(纯黑像素不发光),同时比纯黑更有层次感,避免了生硬的视觉割裂。卡片背景 #111C2E 和芯片背景 #1A2A44 形成了从深到浅的三级递进,为界面层次感奠定了基础。

电光蓝 #3D8BFF 是品牌主色,用于按钮背景、选中态高亮、渐变中段和底部 Tab 选中色。其深色变体 #1E5FD0 用于渐变起点,形成从深蓝到亮蓝再到荧光青的色彩过渡。荧光青 #35E0C8 作为强调色,用于高亮链接、趋势上升标记、环形图扇区和代码高亮。蓝色与青色的冷暖对比创造了科技感十足的视觉张力,非常契合软件下载站的极客气质。

值得注意的是 codeBg: '#0A1830' 这个字段,它是专门为代码预览卡设计的背景色。比页面背景 #0A1220 略深一些,但又比纯黑更有层次,使代码内容在深色环境中具有独立的视觉分区感。这种对细节的色彩把控体现了成熟的设计思维。

五、常量定义与 Mock 数据

5.1 Tab 元数据与分类标签

interface TabMeta {
  icon: string;
  label: string;
}

const TAB_LIST: TabMeta[] = [
  { icon: '💻', label: '首页' },
  { icon: '🌐', label: '网页' },
  { icon: '⬇', label: '下载' },
  { icon: '👤', label: '我的' }
];

const CATE_TAGS: string[] = ['推荐', '系统工具', '办公学习', '影音播放', '社交聊天', '游戏助手', '开发工具', '安全防护'];

TabMeta 接口定义了底部导航 Tab 的元数据结构,包含 icon(emoji 图标)和 label(标签文本)两个字段。TAB_LIST 常量定义了四个 Tab:首页(笔记本电脑图标)、网页(地球图标)、下载(向下箭头图标)、我的(人形图标)。这种 emoji 图标方案虽然不如矢量图标精致,但在单文件应用中具有零资源依赖的优势,且 emoji 在不同设备上具有一致的渲染效果。

CATE_TAGS 常量定义了八个软件分类标签,通过头部的横向滚动 Scroll 展示为可点击的 chips。分类涵盖推荐、系统工具、办公学习、影音播放、社交聊天、游戏助手、开发工具和安全防护,基本覆盖了主流软件下载站的所有分类维度。选中态通过 cateIdx 状态控制,选中时背景色变为电光蓝、文字变为白色,未选中时背景为芯片色、文字为次要色。

5.2 今日精选软件数据

interface SoftRec {
  icon: string;
  name: string;
  vendor: string;
  meta: string;
}

const TODAY_RECS: SoftRec[] = [
  { icon: '🚀', name: '极速下客户端 6.1.1', vendor: '极速实验室', meta: '全新 ArkWeb 双 URL 溯源内核' },
  { icon: '🧰', name: '全能工具箱 Pro', vendor: '拾光工作室', meta: '装机必备 · 本周下载 42 万次' },
  { icon: '🎬', name: '光影播放器 4K 版', vendor: '声色传媒', meta: '编辑精选 · 评分 4.9 分' }
];

SoftRec 接口定义了今日精选软件的数据结构,包含 icon(emoji 图标)、name(软件名)、vendor(发行方)、meta(推荐理由)四个字段。三条 Mock 数据精心设计了一条自引用——"极速下客户端 6.1.1"正是本应用自身,其推荐理由"全新 ArkWeb 双 URL 溯源内核"直接点明了应用的技术核心。另外两条分别是装机必备工具箱和编辑精选播放器,涵盖了实用工具和影音娱乐两大热门分类。

这三条数据在首页的"今日精选"渐变大卡中展示,每条包含图标、软件名(粗体)、发行方和推荐理由(次要色,单行省略),右侧有一个荧光青边框的"获取"按钮,点击后跳转到网页 Tab。这种"推荐 + 一键跳转"的设计模式是软件下载站引导用户发现新软件的经典方式。

5.3 快捷站点与环形图数据

const QUICK_SITES: string[] = [
  'https://developer.huawei.com',
  'https://www.openharmony.cn',
  'https://gitee.com',
  'https://developer.mozilla.org'
];

interface RingSlice {
  val: number;
  color: string;
  label: string;
}

const RING_DATA: RingSlice[] = [
  { val: 34, color: COLORS.blue, label: '系统工具' },
  { val: 28, color: COLORS.cyan, label: '办公学习' },
  { val: 22, color: COLORS.gold, label: '影音播放' },
  { val: 16, color: COLORS.red, label: '游戏助手' }
];

const RING_CENTER: string = '累计下载';

QUICK_SITES 常量定义了网页 Tab 的四个快捷站点入口,均为真实可访问的站点——华为开发者联盟、OpenHarmony 官网、Gitee 代码托管平台、MDN 开发者文档。这些站点选择体现了应用的开发者向定位。快捷站点以横向滚动 chips 形式展示,点击即加载,当前加载的站点会高亮显示。

RingSlice 接口定义了环形图扇区的数据结构,包含 val(占比百分数)、color(扇区颜色,引用 COLORS 常量)、label(分类名)三个字段。四条 Mock 数据合计正好 100(34+28+22+16),分别用蓝色、青色、金色、红色四个扇区表示系统工具、办公学习、影音播放、游戏助手四类软件的下载占比。系统工具占比最高,符合软件下载站的典型用户画像。

RING_CENTER 常量定义了环形图中心镂空区域的标题文案"累计下载",下方还会叠加荧光青色的"218 次"汇总数值。中心镂空 + 标题 + 数值的三层叠加是环形图的标准信息架构。

六、辅助函数

6.1 趋势映射函数

function trendColor(t: string): string {
  if (t === 'up') { return COLORS.red; }
  if (t === 'down') { return COLORS.green; }
  return COLORS.text3;
}

function trendIcon(t: string): string {
  if (t === 'up') { return '↑ 上升'; }
  if (t === 'down') { return '↓ 下降'; }
  return '— 持平';
}

trendColortrendIcon 是一对配套的辅助函数,用于将趋势状态字符串映射为视觉表达。trendColor 将 ‘up’ 映射为红色(COLORS.red,表示热度上升)、‘down’ 映射为绿色(COLORS.green,表示热度下降)、‘flat’ 映射为三级弱化色(COLORS.text3)。trendIcon 将相同的状态映射为带箭头的文案——“↑ 上升”、“↓ 下降”、“— 持平”。

这里有一个值得注意的语义设计:上升用红色、下降用绿色,这与传统股票市场的"红涨绿跌"一致,但在通用 UI 设计中红色通常代表警示。在下载站场景中,热度上升意味着软件受欢迎程度提升,用红色突出显示是一种"热点提醒",引导用户关注热门软件。这种语义映射在首页热软榜中为每条记录的右侧趋势标记提供颜色和文案。

6.2 站点域名提取函数

function siteHost(url: string): string {
  const head = 'https://';
  if (url.startsWith(head)) {
    return url.slice(head.length);
  }
  return url;
}

siteHost 函数用于将完整 URL 转换为展示用的域名文本,去掉 https:// 协议前缀。例如 https://developer.huawei.com 会被转换为 developer.huawei.com。这个函数在网页 Tab 的快捷站点 chips 中使用——chips 的空间有限,显示完整 URL 过长且不美观,只展示域名部分既简洁又信息明确。

函数实现简洁但有一个局限:只处理了 https:// 前缀,没有处理 http://。由于 QUICK_SITES 中的站点全部使用 https 协议,这个局限在实际使用中不会暴露问题,但如果未来扩展支持 http 站点,需要补充对 http:// 的处理。这种"够用即可"的设计哲学在单文件应用中是合理的取舍。

七、数据模型设计

7.1 软件榜单数据模型

@Observed export class SoftItem {
  rank: string;
  name: string;
  vendor: string;
  size: string;
  heat: string;
  trend: string;

  constructor(rank: string, name: string, vendor: string, size: string, heat: string, trend: string) {
    this.rank = rank;
    this.name = name;
    this.vendor = vendor;
    this.size = size;
    this.heat = heat;
    this.trend = trend;
  }
}

const SOFT_LIST: Array<SoftItem> = [
  new SoftItem('1', '极速下客户端', '极速实验室', '86.4 MB', '42.1万', 'up'),
  new SoftItem('2', '全能工具箱 Pro', '拾光工作室', '45.2 MB', '38.6万', 'up'),
  new SoftItem('3', '光影播放器 4K', '声色传媒', '128.0 MB', '35.9万', 'flat'),
  new SoftItem('4', '极简笔记云同步', '纸上建筑', '32.8 MB', '29.4万', 'down'),
  new SoftItem('5', '网速守护卫士', '盾山科技', '21.6 MB', '26.7万', 'up'),
  new SoftItem('6', '代码片段管家', '码上宇宙', '55.3 MB', '22.8万', 'down'),
  new SoftItem('7', '桌面美化大师', '像素方舟', '98.7 MB', '19.5万', 'up'),
  new SoftItem('8', '文件压缩精灵', '轻量工坊', '18.9 MB', '16.2万', 'flat')
];

SoftItem 类使用 @Observed 装饰器标记,使其成为可观察的数据类。@Observed 的作用是让 ArkUI 框架能够追踪该类实例属性的变化——当属性值被修改时,框架会自动刷新与该属性绑定的 UI 组件。这个类定义了软件榜单条目的六个字段:rank(名次字符串)、name(软件名)、vendor(发行方)、size(安装包大小文本)、heat(本周下载量文本)、trend(趋势状态),所有字段都在构造函数中初始化。

八条 Mock 数据按下载量从高到低排列,名次从 1 到 8。趋势状态混合了 up/down/flat 三种,使榜单呈现真实的动态特征。第一名"极速下客户端"下载量 42.1 万、趋势上升,与首页今日精选的自引用数据呼应,形成一致的品牌叙事。

7.2 下载记录数据模型——双 URL 溯源的核心载体

@Observed export class DownloadRecord {
  fileName: string;
  fileSize: string;
  finishTime: string;
  originalUrl: string;
  referrerUrl: string;
  note: string;

  constructor(fileName: string, fileSize: string, finishTime: string,
    originalUrl: string, referrerUrl: string, note: string) {
    this.fileName = fileName;
    this.fileSize = fileSize;
    this.finishTime = finishTime;
    this.originalUrl = originalUrl;
    this.referrerUrl = referrerUrl;
    this.note = note;
  }
}

const DOWNLOAD_RECORDS: Array<DownloadRecord> = [
  new DownloadRecord('JiSuXia_v6.1.1.apk', '86.4 MB', '今天 09:42',
    'https://dl.jisuxia.cn/client/JiSuXia_v6.1.1_20260825.apk',
    'https://www.jisuxia.cn/client/download?from=home_banner', '官网直链'),
  new DownloadRecord('ToolBoxPro_3.2.0.apk', '45.2 MB', '今天 08:15',
    'https://cdn.shiguang.dev/toolbox/ToolBoxPro_3.2.0_release.apk',
    'https://www.shiguang.dev/toolbox?channel=search', '装机必备'),
  new DownloadRecord('MediaPlayer4K_5.0.1.apk', '128.0 MB', '昨天 21:36',
    'https://mirror.shengse.cn/player/MediaPlayer4K_5.0.1_arm64.apk',
    'https://www.shengse.cn/rank/top?tab=video', '编辑精选'),
  new DownloadRecord('NoteSync_2.8.3.apk', '32.8 MB', '昨天 19:04',
    'https://dl.zhishang.cn/note/NoteSync_2.8.3_hap.apk',
    'https://zhishang.cn/note/detail?id=1024&ref=share', '同事推荐'),
  new DownloadRecord('SpeedGuard_1.6.0.apk', '21.6 MB', '昨天 12:48',
    'https://pkg.dunshan.cn/guard/SpeedGuard_1.6.0.apk',
    'https://www.dunshan.cn/promo/free2026', '限时免费'),
  new DownloadRecord('ZipElf_4.1.2.apk', '18.9 MB', '08-23 10:22',
    'https://down.qingliang.cn/zip/ZipElf_4.1.2_signed.apk',
    'https://so.qingliang.cn/search?q=压缩工具', '搜索下载')
];

DownloadRecord 是整个应用最核心的数据模型,它是 ArkWeb 6.1.1 双 URL 溯源特性的数据载体。类定义了六个字段,其中 originalUrlreferrerUrl 是两个带星号注释的关键字段——originalUrl 存储 getOriginalUrl() 的返回值(下载项原始 URL 地址),referrerUrl 存储 getReferrerUrl() 的返回值(引用页 URL 地址)。这两个字段构成了下载来源审计的双链条。

六条 Mock 数据精心设计了完整、真实、带域名/路径/参数的双 URL 地址。以第一条为例:originalUrlhttps://dl.jisuxia.cn/client/JiSuXia_v6.1.1_20260825.apk——一个带 CDN 域名、路径、文件名和日期后缀的标准下载直链;referrerUrlhttps://www.jisuxia.cn/client/download?from=home_banner——一个带 utm 式参数的引用页地址,from=home_banner 参数揭示了用户是从首页 Banner 位触发的下载。这种双 URL 设计使每条记录的来源信息完整可追溯。

六条记录的 note(用户备注)字段涵盖了"官网直链"、“装机必备”、“编辑精选”、“同事推荐”、“限时免费”、"搜索下载"六种典型场景,模拟了真实下载站中用户给下载任务打标签的使用习惯。在下载 Tab 的列表中,这些备注与文件名、大小、完成时间一起在行首展示,双 URL 则在行下方分两行以等宽字体展示,颜色区分——原始 URL 用荧光青、引用页 URL 用金色。

7.3 我的页功能清单数据模型

@Observed export class UserStat {
  icon: string;
  label: string;
  value: string;
  arrow: boolean;

  constructor(icon: string, label: string, value: string, arrow: boolean) {
    this.icon = icon;
    this.label = label;
    this.value = value;
    this.arrow = arrow;
  }
}

const STAT_LIST: Array<UserStat> = [
  new UserStat('⬇', '累计下载', '218 个软件', true),
  new UserStat('🔗', '来源审计', '双 URL 溯源已开启', true),
  new UserStat('📁', '下载目录', '/data/storage/files', true),
  new UserStat('⚡', '下载加速通道', '会员专享 8 线程', true),
  new UserStat('🕘', '下载历史', '近 30 天 46 条', true),
  new UserStat('🔄', '自动更新检查', '每日 22:00', true),
  new UserStat('🛡', '安装包安全扫描', '已拦截 3 个风险包', true),
  new UserStat('⚙', '下载偏好设置', '仅 Wi-Fi 下载', true)
];

UserStat 类同样使用 @Observed 装饰器,定义了我的页功能清单条目的数据结构:icon(功能图标)、label(功能名)、value(状态/数值文本)、arrow(是否显示右箭头)。arrow 字段的布尔设计预留了未来某些功能项不显示箭头的灵活性——例如纯信息展示项不需要右箭头,而可跳转的功能项则显示箭头提示可点击。

八条功能清单数据涵盖了一个成熟下载站应用的全部核心功能维度:累计下载统计、来源审计状态(直接体现双 URL 溯源特性已开启)、下载目录路径、下载加速、下载历史、自动更新检查、安装包安全扫描、下载偏好设置。其中"来源审计:双 URL 溯源已开启"这一条直接向用户展示了应用的技术特性已生效,增强了用户对安全性的信任感。

八、组件主体:状态定义

8.1 基础状态与弹窗状态

@Entry
@Component
struct Page1121 {
  @State currentTab: number = 0;
  @State breath: boolean = false;
  @State timer: number = -1;
  @State cateIdx: number = 0;
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = 0;
  @State delIdx: number = 0;
  @State softList: Array<SoftItem> = SOFT_LIST;
  @State downloadRecords: Array<DownloadRecord> = DOWNLOAD_RECORDS;
  @State statList: Array<UserStat> = STAT_LIST;
  @State formUrl: string = '';
  @State formNote: string = '';
  @State editNote: string = '';

组件主体 Page1121 使用 @Entry@Component 装饰器标记为应用的入口组件。@Entry 表示该组件是页面级入口,@Component 表示这是一个可复用的 UI 组件。所有状态变量使用 @State 装饰器声明,使框架能够追踪这些变量的变化并自动触发 UI 刷新。

currentTab 控制当前选中的 Tab 索引,初始值为 0(首页),通过点击底部导航切换。breath 是呼吸动画开关,初始值为 false,由定时器每秒翻转。timer 存储定时器句柄,初始值为 -1,在 aboutToAppear 中赋值、在 aboutToDisappear 中清除。cateIdx 控制头部分类 chips 的选中索引。

三个弹窗开关 addModaleditModaldelModal 分别控制新建下载、编辑备注、删除确认三个弹窗的显示。配合 editIdxdelIdx 两个索引变量,记录当前正在编辑或删除的记录位置。这种"开关 + 索引"的弹窗状态管理模式简洁高效——当开关为 true 时,弹窗条件渲染出现,索引指向操作的目标记录。

softListdownloadRecordsstatList 三个数组状态分别绑定首页榜单、下载记录列表和我的页功能清单。虽然初始值来自 Mock 常量,但由于声明为 @State,后续对数组的修改(如 unshift 新增、splice 删除、slice 刷新引用)都能触发对应列表的重绘。三个表单变量 formUrlformNoteeditNote 分别用于新建弹窗的下载链接输入、备注输入和编辑弹窗的备注输入。

8.2 Canvas 与 ArkWeb 状态

  @State canvasReady: boolean = false;
  private ringCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));

  private webController: webview.WebviewController = new webview.WebviewController();
  private downloadDelegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate();
  @State urlInput: string = QUICK_SITES[0];
  @State webUrl: string = QUICK_SITES[0];
  @State dlName: string = '';
  @State dlPercent: number = 0;
  @State dlState: string = '空闲';

Canvas 状态部分,canvasReady 是环形图 Canvas 的就绪标志,初始为 false,在 Canvas 组件的 onReady 回调中置为 true。ringCtx 是 Canvas 的 2D 渲染上下文,通过 new CanvasRenderingContext2D(new RenderingContextSettings(true)) 创建。注意 ringCtx 使用 private 而非 @State 声明——因为 Canvas 上下文对象本身的引用不会变化,变化的只是绘制内容,绘制内容的刷新通过手动调用 drawRingChart() 方法实现,不需要 ArkUI 的响应式机制介入。RenderingContextSettings(true) 的参数 true 表示开启抗锯齿,使环形图的扇区边缘和文字渲染更加平滑。

ArkWeb 状态部分是整个应用的技术核心。webController 是 Web 组件的控制器,用于加载网页、绑定下载代理和主动发起下载。downloadDelegate 是下载代理,提供四个下载生命周期回调的注册入口。这两个对象同样使用 private 声明,因为它们的引用在生命周期内不变。

urlInputwebUrl 是地址栏的双状态分离设计——urlInput 绑定地址栏输入框的值,用户敲字时实时更新但不触发加载;webUrl 是 Web 组件实际加载的值,只有点击"前往"按钮后才从 urlInput 同步过来。这种"输入与加载分离"的设计避免了用户每敲一个字符就重新加载网页的荒谬行为。dlNamedlPercentdlState 三个状态分别记录当前下载的文件名、进度百分比和状态文案,用于下载 Tab 的进行中任务卡展示。

九、ArkWeb 下载代理注册——双 URL 溯源核心

9.1 下载开始前回调

  setupDownloadDelegate() {
    this.downloadDelegate.onBeforeDownload((item: webview.WebDownloadItem) => {
      const hostCtx = this.getUIContext().getHostContext();
      const dir = hostCtx ? hostCtx.filesDir : '';
      item.start(dir + '/' + item.getSuggestedFileName());
    });

setupDownloadDelegate 方法是整个双 URL 溯源功能的注册入口。方法内部依次注册了下载代理的四个回调,最后将代理绑定到 Web 控制器。

第一个回调 onBeforeDownload 在下载开始前触发。这个回调有一个关键约束——必须调用 item.start() 方法并提供沙箱路径,否则下载任务会停留在 PENDING 状态无法真正开始。方法内通过 this.getUIContext().getHostContext() 获取应用的主上下文,再从 hostCtx.filesDir 获取应用的沙箱文件目录路径。拼接 dir + '/' + item.getSuggestedFileName() 作为下载文件的目标保存路径,getSuggestedFileName() 返回 Web 引擎根据下载项的 URL 和 Content-Disposition 头建议的文件名。

这种设计把文件保存路径的控制权完全交给了应用侧——开发者可以自由决定文件保存到沙箱的哪个目录、用什么文件名,而不是被动接受浏览器的默认下载行为。在软件下载站场景中,这意味着可以将所有下载的安装包统一保存到指定目录,便于后续的安装包安全扫描和自动更新检查。

9.2 下载进行中与失败回调

    this.downloadDelegate.onDownloadUpdated((item: webview.WebDownloadItem) => {
      this.dlPercent = item.getPercentComplete();
      this.dlState = '正在下载 ' + item.getPercentComplete() + '%';
    });
    this.downloadDelegate.onDownloadFailed((item: webview.WebDownloadItem) => {
      this.dlState = '下载失败 · ' + item.getGuid();
      this.dlPercent = 0;
    });

第二个回调 onDownloadUpdated 在下载进行中反复触发,每次触发时通过 item.getPercentComplete() 获取当前下载进度百分比。方法将进度值赋给 dlPercent 状态、将"正在下载 XX%"文案赋给 dlState 状态。由于这两个状态都是 @State 声明的,每次赋值都会自动触发下载 Tab 进度条和状态文案的刷新——用户可以实时看到下载进度条线性增长、状态文案数字跳动。

第三个回调 onDownloadFailed 在下载失败时触发。方法将状态文案设为"下载失败 · "加上 item.getGuid() 返回的全局唯一标识符,进度清零。getGuid() 返回的 GUID 在排查下载问题时非常有用——开发者可以将 GUID 与系统日志中的下载错误信息交叉对照,精确定位失败原因(如网络超时、磁盘空间不足、SSL 证书错误等)。

9.3 下载完成回调——6.1.1 双 URL 溯源

    this.downloadDelegate.onDownloadFinish((item: webview.WebDownloadItem) => {
      const originalUrl: string = item.getOriginalUrl();
      const referrerUrl: string = item.getReferrerUrl();
      this.downloadRecords.unshift(new DownloadRecord(
        item.getSuggestedFileName(), Math.round(item.getTotalBytes() / 1048576) + ' MB',
        '刚刚', originalUrl, referrerUrl, '本次会话下载'));
      this.dlState = '下载完成';
      this.dlPercent = 100;
    });

第四个回调 onDownloadFinish 是整个应用的技术高光——HarmonyOS 6.1.1 新增的双 URL 溯源就在这里体现。方法首先调用 item.getOriginalUrl() 获取下载项的原始 URL 地址,这是文件在网络上的真实直链位置。然后调用 item.getReferrerUrl() 获取引用页 URL 地址,这是用户触发下载时所在的页面。这两个接口都是 HarmonyOS 6.1.1 版本新增的,在之前的版本中,下载完成后只能获取文件名和大小,无法追溯来源。

获取双 URL 后,方法通过 item.getSuggestedFileName() 获取建议文件名、通过 item.getTotalBytes() 获取总字节数并除以 1048576(1MB = 1048576 字节)四舍五入为 MB 文本,然后使用 this.downloadRecords.unshift() 在下载记录列表头部插入一条新的 DownloadRecordunshift 操作将最新下载的记录置顶展示,符合"最新优先"的信息浏览习惯。由于 downloadRecords@State 声明的数组,unshift 会触发列表的自动刷新。

最后将状态文案设为"下载完成"、进度设为 100%,让下载 Tab 的进行中任务卡展示完成态。新记录的备注固定为"本次会话下载",与历史 Mock 数据的备注(官网直链/装机必备等)形成区分,用户可以一眼识别哪些是本次会话实际下载的、哪些是历史演示数据。

9.4 代理绑定与异常处理

    try {
      this.webController.setDownloadDelegate(this.downloadDelegate);
    } catch (error) {
      console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`);
    }
  }

四个回调注册完成后,需要通过 this.webController.setDownloadDelegate(this.downloadDelegate) 将下载代理绑定到 Web 控制器。绑定后,网页内用户点击下载链接触发的下载才会进入上述四个回调——如果不绑定代理,Web 组件会使用系统默认的下载行为,应用无法感知和控制下载过程。

这行代码使用 try-catch 包裹,捕获类型为 BusinessError。如果绑定失败(例如 Web 控制器尚未初始化、或设备不支持下载代理特性),会通过 console.error 输出错误码和错误消息,而不会导致应用崩溃。这种防御性编程在调用系统 API 时是必要的——系统 API 的行为可能因设备型号、系统版本、权限状态等因素而异,必须做好异常兜底。

十、ArkWeb 业务方法

10.1 地址栏加载方法

  loadUrl() {
    let url = this.urlInput.trim();
    if (url === '') {
      return;
    }
    if (!url.startsWith('https://') && !url.startsWith('http://')) {
      url = 'https://' + url;
    }
    this.urlInput = url;
    this.webUrl = url;
  }

loadUrl 方法处理地址栏的"前往"操作。首先对输入值做 trim() 去除首尾空白,如果为空直接返回不做任何操作。然后检查协议前缀——如果用户输入的地址既不以 https:// 开头也不以 http:// 开头,则自动补上 https:// 前缀。这种自动补全设计提升了用户体验:用户只需输入 developer.huawei.com 即可,无需手动输入协议前缀。

补全后,将处理后的 URL 同时赋给 urlInput(让地址栏显示补全后的完整 URL)和 webUrl(触发 Web 组件加载新页面)。由于 webUrl@State 声明的,赋值后 Web 组件的 src 属性会自动更新,页面重新加载。这种"双状态同步"的设计确保了地址栏显示的内容与 Web 组件实际加载的内容始终一致。

10.2 应用侧主动下载方法

  triggerDownload(url: string) {
    try {
      this.dlName = url.slice(url.lastIndexOf('/') + 1);
      this.dlPercent = 0;
      this.dlState = '已发起下载请求';
      this.webController.startDownload(url);
    } catch (error) {
      this.dlState = '发起失败 ' + (error as BusinessError).code;
    }
  }

triggerDownload 方法实现了应用侧主动发起下载的能力,这是 ArkWeb 区别于传统浏览器的重要特性。传统浏览器的下载只能由网页内用户点击下载链接触发,而 ArkWeb 通过 webController.startDownload(url) 允许应用侧直接传入一个 URL 发起下载——无需打开网页、无需用户点击,应用代码即可直接触发。

方法首先通过 url.lastIndexOf('/') 找到最后一个斜杠的位置,截取斜杠之后的部分作为文件名显示。然后将进度清零、状态设为"已发起下载请求",最后调用 startDownload 发起下载。整个方法用 try-catch 包裹,如果 startDownload 抛出 BusinessError(例如 URL 格式不合法、网络不可用),则将状态文案设为"发起失败"加错误码。

这种主动下载能力在软件下载站场景中极具价值:首页今日精选的"获取"按钮、弹窗中"新建下载任务"的"开始下载"按钮,都可以直接调用 triggerDownload 发起下载,用户无需跳转到网页再点击下载链接,一键直达下载。

十一、Canvas 环形图绘制

11.1 环形图绘制方法

  drawRingChart() {
    const ctx = this.ringCtx;
    const cx = 95;
    const cy = 90;
    const r = 62 * (this.breath ? 1.04 : 0.96);
    ctx.clearRect(0, 0, 330, 180);
    let start = -Math.PI / 2;
    for (const d of RING_DATA) {
      const angle = (d.val / 100) * Math.PI * 2;
      ctx.beginPath();
      ctx.moveTo(cx, cy);
      ctx.arc(cx, cy, r, start, start + angle);
      ctx.closePath();
      ctx.fillStyle = d.color;
      ctx.fill();
      start += angle;
    }

drawRingChart 方法是首页下载分类占比环形图的核心绘制逻辑。方法首先获取 Canvas 上下文 ctx,设定圆心坐标 (95, 90) 和基础半径 62。半径通过 this.breath ? 1.04 : 0.96 乘以一个倍率实现呼吸动画——当 breath 为 true 时半径放大到 64.48,为 false 时缩小到 59.52,每秒切换一次,形成脉动效果。

绘制开始前调用 ctx.clearRect(0, 0, 330, 180) 清除整个画布区域,这是重绘前的必要步骤——如果不清除,新的绘制会叠加在旧内容之上,导致颜色混合和边缘模糊。

扇区绘制采用经典的"从圆心出发画弧再闭合"技术:起始角度设为 -Math.PI / 2(即 12 点钟方向),遍历 RING_DATA 的四条数据,每条数据计算其角度占比 (d.val / 100) * Math.PI * 2(整圆为 2π)。对每个扇区,先 beginPath 开始新路径,moveTo(cx, cy) 移动画笔到圆心,arc(cx, cy, r, start, start + angle) 画弧,closePath 闭合路径形成扇形,最后 fillStyle 设为扇区颜色并 fill 填充。绘制完一个扇区后,start += angle 将起始角度推进到下一个扇区的起点,实现连续无缝的扇区排列。

11.2 中心镂空与文字叠加

    ctx.beginPath();
    ctx.arc(cx, cy, r * 0.62, 0, Math.PI * 2);
    ctx.fillStyle = COLORS.card;
    ctx.fill();
    ctx.fillStyle = COLORS.title;
    ctx.font = '16px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(RING_CENTER, cx, cy - 2);
    ctx.fillStyle = COLORS.cyan;
    ctx.font = '14px sans-serif';
    ctx.fillText('218 次', cx, cy + 18);

扇区绘制完成后,需要在中心镂空形成环形。方法在圆心位置画一个半径为 r * 0.62(约 38.44)的完整圆,填充色设为卡片背景色 COLORS.card,这样就用卡片色"挖掉"了扇区的中心部分,形成环形视觉效果。这种"画了再覆盖"的镂空技术是 Canvas 绘制环形图的标准做法。

镂空后在中心叠加两层文字:上层是标题"累计下载"(RING_CENTER 常量),用主标题色 COLORS.title 和 16px 字号渲染,位置在圆心上方 2 像素;下层是汇总数值"218 次",用荧光青色 COLORS.cyan 和 14px 字号渲染,位置在圆心下方 18 像素。textAlign = 'center' 确保文字水平居中于圆心。两层文字的大小和颜色差异形成了"标题 + 数据"的信息层次。

11.3 右侧图例绘制

    for (let i = 0; i < RING_DATA.length; i++) {
      const d = RING_DATA[i];
      const ly = 30 + i * 36;
      ctx.fillStyle = d.color;
      ctx.fillRect(190, ly, 12, 12);
      ctx.fillStyle = COLORS.sub;
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'left';
      ctx.fillText(d.label, 208, ly + 10);
      ctx.fillStyle = COLORS.title;
      ctx.fillText(d.val + '%', 268, ly + 10);
    }
  }

环形图右侧绘制图例,遍历 RING_DATA 的四条数据,每条图例包含三个元素:12x12 的色块(fillRect)、分类名(fillText)、占比百分比(fillText)。图例的纵向起始位置为 30,每条间隔 36 像素,四条图例占据 30 到 138 的纵向空间,与环形图的纵向范围基本对齐。

色块用扇区颜色填充,分类名用次要色 COLORS.sub 和 11px 字号,占比用主标题色 COLORS.title。文字的 textAlign 切换为 'left' 左对齐,与中心的居中对齐区分。图例的色块 + 文字组合是数据可视化的标准信息架构,让用户能够快速将扇区颜色与分类名和数值对应起来。

十二、弹窗业务方法

12.1 新建下载任务

  saveDownload() {
    const url = this.formUrl === '' ? 'https://dl.jisuxia.cn/client/JiSuXia_v6.1.1_20260825.apk' : this.formUrl;
    this.triggerDownload(url);
    this.formUrl = '';
    this.formNote = '';
    this.addModal = false;
  }

saveDownload 方法处理新建下载任务弹窗的"开始下载"按钮点击事件。方法首先检查表单中的 URL 输入值 formUrl,如果为空则使用一个默认的演示链接作为兜底,确保即使用户不输入任何内容也能发起一次下载演示。这种"空值兜底"设计在演示型应用中很实用——用户可以直接点击"开始下载"体验下载流程,无需手动输入 URL。

获取 URL 后调用 triggerDownload(url) 发起下载,然后清空表单变量 formUrlformNote、关闭新建弹窗。需要注意的是,formNote(备注)虽然被清空,但实际上并未传递给 triggerDownload——因为 onDownloadFinish 回调中固定使用"本次会话下载"作为备注。这是当前实现的一个简化处理,在真实应用中可以通过扩展 triggerDownload 方法接收备注参数,在下载完成回调中将备注写入 DownloadRecord

12.2 编辑备注与删除记录

  openEditRecord(idx: number) {
    this.editIdx = idx;
    this.editNote = this.downloadRecords[idx].note;
    this.editModal = true;
  }

  updateRecord() {
    if (this.editIdx >= 0 && this.editIdx < this.downloadRecords.length) {
      if (this.editNote !== '') {
        this.downloadRecords[this.editIdx].note = this.editNote;
      }
      this.downloadRecords = this.downloadRecords.slice();
    }
    this.editModal = false;
  }

  delRecord() {
    if (this.delIdx >= 0 && this.delIdx < this.downloadRecords.length) {
      this.downloadRecords.splice(this.delIdx, 1);
    }
    this.delModal = false;
  }

openEditRecord 方法在用户点击下载记录的"改"按钮时调用。方法接收记录索引 idx,将其赋给 editIdx 状态,然后将当前记录的备注值回填到 editNote 状态(实现编辑表单的预填充),最后打开编辑弹窗。这种"回填当前值"的设计让用户在编辑时能看到原有备注,在此基础上修改而非从零开始。

updateRecord 方法处理编辑弹窗的"保存"按钮。方法首先做边界检查——确保 editIdx 在有效范围内,然后检查 editNote 是否非空(空值不覆盖原有备注),将新备注写入记录的 note 字段。关键的一步是 this.downloadRecords = this.downloadRecords.slice()——slice() 方法创建数组的浅拷贝,生成一个新的数组引用。由于 downloadRecords@State 声明的,引用变更会触发 ArkUI 的响应式刷新,使 ForEach 列表重新渲染。如果不使用 slice() 创建新引用,直接修改数组元素的属性不会触发列表刷新——这是 ArkUI 响应式系统的一个关键特性,也是开发者常踩的坑。

delRecord 方法处理删除确认弹窗的"删除"按钮。同样先做边界检查,然后通过 splice(this.delIdx, 1) 从数组中移除指定索引的记录。splice 方法直接修改原数组并改变数组长度,ArkUI 能够检测到这种结构性变化并触发列表刷新。最后关闭删除弹窗。

十三、生命周期管理

  aboutToAppear() {
    this.setupDownloadDelegate();
    this.timer = setInterval(() => {
      this.breath = !this.breath;
      if (this.canvasReady) {
        this.drawRingChart();
      }
    }, 1000);
  }

  aboutToDisappear() {
    clearInterval(this.timer);
  }

aboutToAppear 是组件的生命周期回调,在组件即将出现时触发。方法执行两个初始化操作:第一是调用 setupDownloadDelegate() 注册下载代理并绑定到 Web 控制器,这是 ArkWeb 下载功能生效的前提;第二是启动呼吸动画定时器,通过 setInterval 每秒(1000 毫秒)执行一次回调——翻转 breath 布尔值,并在 Canvas 就绪时调用 drawRingChart() 重绘环形图。

这个定时器实现了一处状态驱动多处动画的高效设计:breath 状态的翻转不仅联动环形图的半径脉动(通过 drawRingChart 中的 r * (this.breath ? 1.04 : 0.96) 计算),还联动了其他使用 breath 状态的 UI 组件。定时器句柄存储在 this.timer 中,供后续清除使用。

aboutToDisappear 在组件即将销毁时触发,调用 clearInterval(this.timer) 清除定时器。这是防止内存泄漏的必要操作——如果不清除,组件销毁后定时器仍在运行,持续触发不存在的组件实例上的方法调用,导致内存泄漏和潜在的崩溃。这种"创建即清除"的生命周期管理是前端工程的基本规范。

十四、页面主构建

  build() {
    Stack() {
      Column() {
        this.headerMain()
        Divider().strokeWidth(1).color(COLORS.line)
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabHome()
            } else if (this.currentTab === 1) {
              this.tabWeb()
            } else if (this.currentTab === 2) {
              this.tabDownload()
            } else {
              this.tabMine()
            }
          }
          .padding({ left: 14, right: 14, top: 12, bottom: 12 })
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
        this.tabBar()
      }
      .width('100%')
      .height('100%')

      if (this.addModal) {
        this.panelAdd(() => {
          this.addModal = false;
        })
      }
      if (this.editModal) {
        this.panelEdit(() => {
          this.editModal = false;
        })
      }
      if (this.delModal) {
        this.panelDel(() => {
          this.delModal = false;
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
  }

build 方法是组件的构建入口,采用 Stack 作为根容器,实现主内容层与弹窗层的叠层覆盖。Stack 内部先放一个 Column 作为主内容层,包含头部 headerMain()、分割线 Divider、可滚动的内容区 Scroll 和底部导航 tabBar()

内容区的 Tab 切换通过 if-else if-else 条件分支实现——根据 currentTab 的值渲染对应 Tab 的 Builder 函数。当 currentTab 为 0 渲染首页、为 1 渲染网页、为 2 渲染下载、为 3 渲染我的。Scroll 组件设置 layoutWeight(1) 弹性占满中间区域、scrollBar(BarState.Off) 隐藏滚动条,使内容区可滚动但不显示滚动条。

三个弹窗作为 Stack 的兄弟元素叠加在主内容层之上,各自通过 if 条件判断控制显示。每个弹窗 Builder 接收一个 onClose 回调函数,回调内容为将对应的弹窗开关设为 false。这种"条件渲染 + 回调式关闭"的弹窗架构简洁灵活——弹窗出现时条件为真即渲染,消失时回调将开关设为假即不渲染,遮罩与面板解耦,互不干扰。

十五、Builder 函数群:头部

  @Builder
  headerMain() {
    Column({ space: 12 }) {
      Column({ space: 6 }) {
        Row() {
          Text('⚡ 极速下').fontSize(17).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text('今日 3.2 万人在用').fontSize(9).fontColor('rgba(255,255,255,0.78)')
        }
        .width('100%')
        Text('双 URL 溯源 · 每次下载都知道来路').fontSize(11).fontColor('rgba(255,255,255,0.9)')
        Text('ArkWeb 6.1.1 全新下载内核已上线').fontSize(9).fontColor('rgba(255,255,255,0.62)')
      }
      .width('100%')
      .padding(14)
      .borderRadius(14)
      .linearGradient({ angle: 120, colors: [[COLORS.blueD, 0], [COLORS.blue, 0.6], [COLORS.cyan, 1]] })

      Row({ space: 8 }) {
        Row({ space: 6 }) {
          Text('🔍').fontSize(12)
          Text('搜索软件 / 粘贴下载链接').fontSize(10).fontColor(COLORS.text3)
        }
        .layoutWeight(1).height(34).padding({ left: 10, right: 10 })
        .backgroundColor(COLORS.chip).borderRadius(17)
        .onClick(() => {
          this.currentTab = 1;
        })
        Text('+ 新建下载').fontSize(10).fontColor('#FFFFFF')
          .padding({ left: 12, right: 12, top: 9, bottom: 9 })
          .backgroundColor(COLORS.blue).borderRadius(17)
          .onClick(() => {
            this.addModal = true;
          })
      }
      .width('100%')

      Scroll() {
        Row({ space: 8 }) {
          ForEach(CATE_TAGS, (tag: string, idx: number) => {
            Text(tag).fontSize(10)
              .fontColor(this.cateIdx === idx ? '#FFFFFF' : COLORS.sub)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .backgroundColor(this.cateIdx === idx ? COLORS.blue : COLORS.chip)
              .borderRadius(13)
              .onClick(() => {
                this.cateIdx = idx;
              })
          }, (tag: string) => tag)
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
    }
    .width('100%')
    .padding({ left: 14, right: 14, top: 12, bottom: 10 })
    .backgroundColor(COLORS.bg)
  }

头部 Builder headerMain 由三个区块组成:渐变 Banner、搜索条 + 新建下载按钮、分类 chips 横滑区。

渐变 Banner 是头部的视觉焦点,采用 120 度线性渐变 linearGradient({ angle: 120, colors: [[COLORS.blueD, 0], [COLORS.blue, 0.6], [COLORS.cyan, 1]] })——从深蓝(#1E5FD0)经电光蓝(#3D8BFF)到荧光青(#35E0C8)的三段渐变。Banner 内部三行内容:顶行是品牌名"⚡ 极速下"和右侧的"今日 3.2 万人在用"实时在线数;中间行是核心标语"双 URL 溯源 · 每次下载都知道来路",直接向用户传达应用的技术核心价值;底行是版本提示"ArkWeb 6.1.1 全新下载内核已上线"。三行文字的字号从 17 到 11 到 9 递减、透明度从 1.0 到 0.9 到 0.62 递减,形成了清晰的信息层次。

搜索条设计为点击即跳转到网页 Tab——因为搜索功能实际上由网页 Tab 的地址栏承载,头部搜索条更多是功能入口的暗示而非真正的搜索框。"新建下载"按钮点击后打开新建下载任务弹窗,用户可以直接粘贴下载链接发起下载。两个按钮的背景色分别为芯片色和电光蓝,视觉上一弱一强,引导用户关注主要操作。

分类 chips 通过 ForEach 遍历 CATE_TAGS 常量渲染八个分类标签,选中态通过 cateIdx === idx 三元判断控制背景色和文字色。chips 容器使用 Scroll 横向滚动,设置 scrollable(ScrollDirection.Horizontal)scrollBar(BarState.Off),使超出屏幕宽度的分类可以横向滑动查看但不显示滚动条。

十六、Builder 函数群:首页 Tab

16.1 今日精选渐变大卡

  @Builder
  recBanner() {
    Column({ space: 10 }) {
      Row() {
        Text('🔥 今日精选').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('每天 10:00 更新').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      ForEach(TODAY_RECS, (rec: SoftRec) => {
        Row({ space: 10 }) {
          Text(rec.icon).fontSize(24)
          Column({ space: 3 }) {
            Text(rec.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text(rec.vendor + ' · ' + rec.meta).fontSize(9).fontColor(COLORS.sub)
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)
          Text('获取').fontSize(10).fontColor(COLORS.cyan)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(12)
            .border({ width: 1, color: COLORS.cyan })
            .onClick(() => {
              this.currentTab = 1;
            })
        }
        .width('100%')
        .padding(11)
        .borderRadius(11)
        .backgroundColor(COLORS.chip)
      }, (rec: SoftRec) => rec.name)
    }
    .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
  }

今日精选大卡通过 ForEach 遍历 TODAY_RECS 常量渲染三条推荐软件。每条推荐是一行横向布局:左侧 emoji 图标(24px)、中间软件名和发行方+推荐理由(弹性占满、左对齐、单行省略)、右侧"获取"按钮(荧光青文字 + 荧光青边框)。

"获取"按钮的设计值得注意——它不是实心背景按钮,而是边框按钮,文字和边框都使用荧光青色,背景透明。这种设计在深色主题中比实心按钮更轻盈,不会抢夺主内容的视觉焦点。点击"获取"后跳转到网页 Tab,引导用户通过网页方式获取软件,与下载站的浏览-下载使用流程一致。

中间区域的发行方和推荐理由用 ' · ' 连接为一段文本,通过 maxLines(1)textOverflow({ overflow: TextOverflow.Ellipsis }) 限制为单行省略。这种"多信息合并为一行"的设计在信息密度高的列表中很常见,节省了纵向空间。

16.2 大编号热软榜

  @Builder
  rankCard() {
    Column({ space: 10 }) {
      Row() {
        Text('🏆 本周热软榜').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('按下载量').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      ForEach(this.softList, (s: SoftItem, idx: number) => {
        Row({ space: 10 }) {
          Text(s.rank).fontSize(20).fontWeight(FontWeight.Bold)
            .fontColor(idx < 3 ? COLORS.cyan : COLORS.text3)
            .width(30)
          Column({ space: 3 }) {
            Text(s.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text(s.vendor + ' · ' + s.size).fontSize(9).fontColor(COLORS.sub)
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)
          Column({ space: 2 }) {
            Text('周下载 ' + s.heat).fontSize(9).fontColor(COLORS.sub)
            Text(trendIcon(s.trend)).fontSize(9).fontColor(trendColor(s.trend))
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding({ top: 8, bottom: 8 })
        .borderRadius(10)
        .backgroundColor(idx % 2 === 0 ? COLORS.chip : COLORS.card)
      }, (s: SoftItem) => s.rank)
    }
    .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
  }

热软榜的视觉亮点是"大编号"设计——名次用 20px 粗体字号渲染,远大于软件名的 12px。前三名的编号用荧光青色 COLORS.cyan 高亮,第四名及以后用三级弱化色 COLORS.text3,通过颜色差异强化"前三甲"的荣誉感。每个编号固定宽度 30 像素,确保所有行的软件名左对齐。

每行右侧是周下载量和趋势标记,通过 trendIcontrendColor 两个辅助函数渲染——上升用红色箭头、下降用绿色箭头、持平用灰色横线。这种"数据 + 趋势"的组合信息让用户不仅看到当前的下载量,还能感知到软件热度的变化方向。

斑马纹背景通过 idx % 2 === 0 ? COLORS.chip : COLORS.card 实现——偶数行用芯片色、奇数行用卡片色,两种深色之间的微小色差形成斑马纹效果,提升了长列表的可读性。

16.3 Canvas 环形图卡片

  @Builder
  chartCard() {
    Column({ space: 10 }) {
      Row() {
        Text('📊 下载分类占比').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('Canvas 绘制').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      Canvas(this.ringCtx)
        .width('100%')
        .height(180)
        .onReady(() => {
          this.canvasReady = true;
          this.drawRingChart();
        })

      Row() {
        Text('系统工具类占比最高').fontSize(9).fontColor(COLORS.sub)
        Column().layoutWeight(1)
        Text('环比 +4.2%').fontSize(9).fontColor(COLORS.green)
      }
      .width('100%')
    }
    .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)
  }

环形图卡片是首页的第三个区块,也是 Canvas 绘图能力的展示窗口。Canvas(this.ringCtx) 组件接收之前创建的 ringCtx 上下文对象作为参数,宽 100% 高 180 像素。onReady 回调在 Canvas 组件初始化完成、上下文可用时触发——将 canvasReady 设为 true(标记 Canvas 已就绪,呼吸动画定时器据此判断是否可以重绘),然后立即调用 drawRingChart() 绘制第一帧图表。

卡片底部是一行数据洞察文案——左侧"系统工具类占比最高"指出数据亮点,右侧"环比 +4.2%“用绿色显示环比增长率,为环形图补充了趋势性信息。这种"图表 + 洞察文案"的组合让数据可视化不只是"好看”,还能"好读"——用户看到图表后立即有文字解读引导,降低了理解成本。

十七、Builder 函数群:网页 Tab

17.1 地址栏与快捷站点

  @Builder
  tabWeb() {
    Column({ space: 10 }) {
      Row({ space: 8 }) {
        TextInput({ text: this.urlInput, placeholder: '输入网址,如 developer.huawei.com' })
          .layoutWeight(1).height(38).fontSize(11).fontColor(COLORS.title)
          .backgroundColor(COLORS.chip).borderRadius(10)
          .onChange((value: string) => {
            this.urlInput = value;
          })
        Text('前往').fontSize(11).fontColor('#FFFFFF')
          .padding({ left: 14, right: 14, top: 10, bottom: 10 })
          .backgroundColor(COLORS.blue).borderRadius(10)
          .onClick(() => {
            this.loadUrl();
          })
      }
      .width('100%')

      Scroll() {
        Row({ space: 8 }) {
          ForEach(QUICK_SITES, (site: string) => {
            Row({ space: 5 }) {
              Text('🔗').fontSize(10)
              Text(siteHost(site)).fontSize(9).fontColor(COLORS.sub)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor(this.webUrl === site ? COLORS.blue : COLORS.chip)
            .borderRadius(12)
            .onClick(() => {
              this.urlInput = site;
              this.webUrl = site;
            })
          }, (site: string) => site)
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')

网页 Tab 的地址栏采用"输入框 + 前往按钮"的经典组合。TextInput 绑定 urlInput 状态,用户输入时通过 onChange 回调实时更新 urlInput,但不会触发 Web 组件加载——只有点击"前往"按钮调用 loadUrl() 后,urlInput 的值才会同步到 webUrl 状态,触发 Web 组件重新加载。这种"输入与加载分离"的双状态设计前面已经分析过,这里看到了它在 UI 层的具体绑定方式。

快捷站点横滑区通过 ForEach 遍历 QUICK_SITES 渲染四个站点入口。每个站点显示链接 emoji 和通过 siteHost(site) 提取的域名文本。当前加载的站点通过 this.webUrl === site 判断高亮——背景色变为电光蓝,未加载的站点用芯片色。点击快捷站点时直接同时设置 urlInputwebUrl,使地址栏和 Web 组件同步更新。

17.2 Web 组件与主动下载演示

      Web({ src: this.webUrl, controller: this.webController })
        .layoutWeight(1)
        .width('100%')
        .borderRadius(10)
        .backgroundColor(COLORS.chip)

      Column({ space: 8 }) {
        Row() {
          Text('🧪 应用侧主动下载演示').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text(this.dlState).fontSize(9).fontColor(this.dlState === '下载完成' ? COLORS.green : COLORS.cyan)
        }
        .width('100%')
        Row({ space: 10 }) {
          Text('下载官方示例包').fontSize(10).fontColor('#FFFFFF')
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.blue).borderRadius(9)
            .onClick(() => {
              this.triggerDownload('https://dl.jisuxia.cn/client/JiSuXia_v6.1.1_20260825.apk');
            })
          Text('下载工具箱示例').fontSize(10).fontColor(COLORS.cyan)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 })
            .borderRadius(9).border({ width: 1, color: COLORS.cyan })
            .onClick(() => {
              this.triggerDownload('https://cdn.shiguang.dev/toolbox/ToolBoxPro_3.2.0_release.apk');
            })
        }
        .width('100%')
        Text('提示:网页内点击下载链接同样会触发 WebDownloadDelegate 四回调').fontSize(8).fontColor(COLORS.text3)
      }
      .width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)
    }
    .width('100%')
    .height('100%')
  }

Web 组件是网页 Tab 的核心,接收 src 属性绑定 webUrl 状态、controller 属性绑定 webController 控制器。当 webUrl 变化时,Web 组件自动加载新的 URL。Web 组件设置 layoutWeight(1) 弹性占满地址栏和下载演示区之间的空间,borderRadius(10) 圆角和芯片背景色使其视觉上融入深色主题。

主动下载演示区是 ArkWeb 特性的重点展示。卡片标题右侧显示当前下载状态 dlState——状态文案的颜色会根据内容动态变化:当状态为"下载完成"时用绿色、其他状态用荧光青。这种条件着色让用户一眼就能识别下载是否成功完成。

两个下载按钮分别触发不同 URL 的下载:第一个"下载官方示例包"是电光蓝实心按钮,触发极速下客户端的下载;第二个"下载工具箱示例"是荧光青边框按钮,触发工具箱的下载。两个按钮的视觉风格一实一虚,暗示了主次关系。底部的小字提示"网页内点击下载链接同样会触发 WebDownloadDelegate 四回调"告知用户:不仅主动下载会进入回调,网页内的被动下载也会被代理接管。

十八、Builder 函数群:下载 Tab

18.1 进行中任务卡

  @Builder
  tabDownload() {
    Column({ space: 12 }) {
      Column({ space: 10 }) {
        Row() {
          Text('⬇ 下载任务').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text(this.dlState).fontSize(9).fontColor(COLORS.cyan)
        }
        .width('100%')
        Text(this.dlName === '' ? '暂无进行中任务(可在网页 Tab 触发)' : this.dlName)
          .fontSize(10).fontColor(COLORS.sub)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Progress({ value: this.dlPercent, total: 100, type: ProgressType.Linear })
          .width('100%').height(6)
          .color(COLORS.blue).backgroundColor(COLORS.chip)
        Row() {
          Text('进度 ' + this.dlPercent + '%').fontSize(9).fontColor(COLORS.sub)
          Column().layoutWeight(1)
          Text('保存至沙箱 filesDir').fontSize(9).fontColor(COLORS.text3)
        }
        .width('100%')
      }
      .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

下载 Tab 的第一个区块是进行中任务卡。卡片顶部是标题行——左侧"⬇ 下载任务"和右侧的实时状态文案。状态文案 dlState 绑定了 onDownloadUpdatedonDownloadFailedonDownloadFinish 三个回调中更新的值,因此会随着下载进度实时变化。

中间是文件名文本,当 dlName 为空时显示"暂无进行中任务(可在网页 Tab 触发)"的引导文案,有值时显示文件名(单行省略)。文件名下方是 Progress 线性进度条,value 绑定 dlPercenttotal 为 100、typeProgressType.Linear。进度条颜色为电光蓝、背景为芯片色,高度仅 6 像素,是低调而清晰的进度指示。

底部行左侧显示"进度 XX%"的精确百分比、右侧显示"保存至沙箱 filesDir"的存储路径提示,告知用户文件保存位置。整个进行中任务卡通过多个 @State 状态的绑定,实现了下载进度的实时可视化。

18.2 代码预览卡

      Column({ space: 6 }) {
        Text('⌨️ onDownloadFinish 回调(HarmonyOS 6.1.1 新增)').fontSize(12)
          .fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column({ space: 4 }) {
          Text('this.downloadDelegate.onDownloadFinish(').fontSize(9).fontFamily('monospace').fontColor(COLORS.sub)
          Text('  (item: webview.WebDownloadItem) => {').fontSize(9).fontFamily('monospace').fontColor(COLORS.sub)
          Text('    const original = item.getOriginalUrl();').fontSize(9).fontFamily('monospace').fontColor(COLORS.cyan)
          Text('    const referrer = item.getReferrerUrl();').fontSize(9).fontFamily('monospace').fontColor(COLORS.gold)
          Text('  });').fontSize(9).fontFamily('monospace').fontColor(COLORS.sub)
        }
        .width('100%').padding(10).borderRadius(8).backgroundColor(COLORS.codeBg)
        Text('原始 URL 追踪文件直链来源,引用页 URL 记录触发下载的页面').fontSize(8).fontColor(COLORS.text3)
      }
      .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

代码预览卡是下载 Tab 的第二个区块,它在 UI 中直接展示 onDownloadFinish 回调的代码片段,让用户直观看到双 URL 溯源是如何实现的。代码使用 fontFamily('monospace') 等宽字体渲染,背景色为 COLORS.codeBg#0A1830),这是专门为代码展示设计的深色背景。

代码片段的关键行通过颜色区分:getOriginalUrl() 那行用荧光青色(与下载记录中原始 URL 的颜色一致),getReferrerUrl() 那行用金色(与下载记录中引用页 URL 的颜色一致)。这种颜色一致性设计让用户在代码预览卡和下方的下载记录列表之间建立了视觉关联——看到荧光青就想到原始 URL、看到金色就想到引用页 URL。

底部的小字注释"原始 URL 追踪文件直链来源,引用页 URL 记录触发下载的页面"用最精炼的语言解释了两个接口的语义区别,是代码预览的"一句话文档"。

18.3 历史下载记录列表——双 URL 溯源展示

      Column({ space: 10 }) {
        Row() {
          Text('🗂 历史下载记录').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text(this.downloadRecords.length + ' 条').fontSize(9).fontColor(COLORS.text3)
        }
        .width('100%')

        ForEach(this.downloadRecords, (rec: DownloadRecord, idx: number) => {
          Column({ space: 6 }) {
            Row({ space: 8 }) {
              Text('📦').fontSize(14)
              Column({ space: 2 }) {
                Text(rec.fileName).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                  .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
                Text(rec.fileSize + ' · ' + rec.finishTime + ' · ' + rec.note).fontSize(9).fontColor(COLORS.sub)
                  .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start)
              Text('改').fontSize(9).fontColor(COLORS.sub)
                .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                .backgroundColor(COLORS.chip).borderRadius(8)
                .onClick(() => {
                  this.openEditRecord(idx);
                })
              Text('删').fontSize(9).fontColor(COLORS.red)
                .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                .backgroundColor(COLORS.chip).borderRadius(8)
                .onClick(() => {
                  this.delIdx = idx;
                  this.delModal = true;
                })
            }
            .width('100%')

            Row({ space: 6 }) {
              Text('🔗').fontSize(9)
              Text(rec.originalUrl).fontSize(8).fontFamily('monospace').fontColor(COLORS.cyan)
                .layoutWeight(1).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .width('100%')

            Row({ space: 6 }) {
              Text('📄').fontSize(9)
              Text(rec.referrerUrl).fontSize(8).fontFamily('monospace').fontColor(COLORS.gold)
                .layoutWeight(1).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .width('100%')
          }
          .width('100%').padding(11).borderRadius(11).backgroundColor(COLORS.chip)
        }, (rec: DownloadRecord) => rec.fileName)
      }
      .width('100%').padding(14).backgroundColor(COLORS.card).borderRadius(12)

历史下载记录列表是下载 Tab 的第三个区块,也是双 URL 溯源能力的最终展示窗口。列表通过 ForEach 遍历 downloadRecords 数组渲染,每条记录是一个三行结构的卡片:

第一行是文件信息行——左侧包裹 emoji、中间文件名(粗体)和大小+时间+备注合并文本(次要色、单行省略)、右侧"改"和"删"两个操作按钮。"改"按钮调用 openEditRecord(idx) 打开编辑弹窗,"删"按钮设置 delIdx 并打开删除确认弹窗。两个按钮用芯片色背景和小圆角,视觉上低调不抢眼,但功能完整。

第二行是原始 URL 溯源行——链接 emoji 加 rec.originalUrl 文本,使用等宽字体和荧光青色,与代码预览卡中 getOriginalUrl() 的颜色一致。URL 文本弹性占满、单行省略,确保长 URL 不会撑破布局。

第三行是引用页 URL 溯源行——文档 emoji 加 rec.referrerUrl 文本,使用等宽字体和金色,与代码预览卡中 getReferrerUrl() 的颜色一致。

这种"文件信息 + 双 URL"的三行结构让每条下载记录的来源信息完整可追溯——用户一眼就能看到文件是什么、从哪个直链下载的、在哪个页面触发的。在安全审计场景下,这个列表就是一份完整的下载来源审计报告。

十九、Builder 函数群:我的 Tab

19.1 用户等级渐变大卡

  @Builder
  tabMine() {
    Column({ space: 12 }) {
      Column({ space: 8 }) {
        Row({ space: 12 }) {
          Text('🦾').fontSize(34)
          Column({ space: 4 }) {
            Text('下载狂人 · 老周').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('白金会员 · 剩余 186 天').fontSize(9).fontColor('rgba(255,255,255,0.8)')
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)
          Text('极速 Lv.8').fontSize(10).fontColor('#FFFFFF')
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .borderRadius(10).backgroundColor('rgba(255,255,255,0.22)')
        }
        .width('100%')
        Row({ space: 8 }) {
          Column({ space: 2 }) {
            Text('218').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('累计下载').fontSize(8).fontColor('rgba(255,255,255,0.7)')
          }
          .layoutWeight(1)
          Column({ space: 2 }) {
            Text('46').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('本月记录').fontSize(8).fontColor('rgba(255,255,255,0.7)')
          }
          .layoutWeight(1)
          Column({ space: 2 }) {
            Text('12.6 GB').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('节省流量').fontSize(8).fontColor('rgba(255,255,255,0.7)')
          }
          .layoutWeight(1)
          Column({ space: 2 }) {
            Text('3').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('风险拦截').fontSize(8).fontColor('rgba(255,255,255,0.7)')
          }
          .layoutWeight(1)
        }
        .width('100%')
      }
      .width('100%').padding(16).borderRadius(14)
      .linearGradient({ angle: 135, colors: [[COLORS.blueD, 0], [COLORS.blue, 0.55], [COLORS.cyan, 1]] })

我的 Tab 的用户等级大卡采用 135 度线性渐变——从深蓝经电光蓝到荧光青,与头部 Banner 的渐变方向不同(头部是 120 度),形成了微妙的方向差异,避免视觉重复。卡片内所有文字使用白色或半透明白色,因为渐变背景已经足够鲜艳,文字不需要再用彩色。

卡片上半部分是用户头像(机器人 emoji 34px)、用户名"下载狂人 · 老周"(搭配白金会员信息)和等级标签"极速 Lv.8"(半透明白色背景的胶囊标签)。下半部分是四格统计数据——累计下载 218 个、本月记录 46 条、节省流量 12.6 GB、风险拦截 3 个。四格等宽分布,数据用 15px 粗体、标签用 8px 半透明,层次分明。

19.2 功能清单行与版本信息

      Column({ space: 0 }) {
        ForEach(this.statList, (st: UserStat) => {
          Row({ space: 10 }) {
            Text(st.icon).fontSize(15)
            Text(st.label).fontSize(11).fontColor(COLORS.title)
              .layoutWeight(1)
            Text(st.value).fontSize(9).fontColor(COLORS.text3)
            if (st.arrow) {
              Text('›').fontSize(14).fontColor(COLORS.text3)
            }
          }
          .width('100%')
          .padding({ top: 11, bottom: 11 })
          .border({ width: { bottom: 1 }, color: COLORS.line })
          .onClick(() => {
            if (st.label === '下载历史') {
              this.currentTab = 2;
            }
          })
        }, (st: UserStat) => st.label)
      }
      .width('100%').padding({ left: 14, right: 14 }).backgroundColor(COLORS.card).borderRadius(12)

      Text('极速下 v6.1.1 · ArkWeb 双 URL 溯源版').fontSize(8).fontColor(COLORS.text3)
    }
    .width('100%')
  }

功能清单行通过 ForEach 遍历 statList 渲染八条功能项。每行从左到右依次是:功能图标 emoji(15px)、功能名(11px 主标题色、弹性占满)、状态值(9px 三级色)、右箭头(条件渲染,if (st.arrow))。每行底部设置 1 像素的分割线(border({ width: { bottom: 1 }, color: COLORS.line })),形成清晰的行间分隔。

点击事件中有一个条件跳转逻辑——当点击的功能项标签为"下载历史"时,切换到下载 Tab(this.currentTab = 2)。这种数据驱动的条件跳转让功能清单不仅是信息展示,还具备导航能力。其他功能项目前未实现跳转逻辑,但 onClick 已就位,后续可轻松扩展。

卡片底部是一行版本信息"极速下 v6.1.1 · ArkWeb 双 URL 溯源版",用最小字号 8px 和三级弱化色渲染,低调地标注应用版本和技术特性。这行文案在用户心中种下了"我的应用使用了前沿的双 URL 溯源技术"的品牌认知。

二十、Builder 函数群:底部导航

  @Builder
  tabBar() {
    Row() {
      ForEach(TAB_LIST, (t: TabMeta, idx: number) => {
        Column({ space: 3 }) {
          Text(t.icon).fontSize(this.currentTab === idx ? 20 : 17)
            .opacity(this.currentTab === idx ? 1 : 0.65)
          Text(t.label).fontSize(9)
            .fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
            .fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center)
        .padding({ top: 7, bottom: 7 })
        .onClick(() => {
          this.currentTab = idx;
        })
      }, (t: TabMeta) => t.label)
    }
    .width('100%')
    .backgroundColor(COLORS.card)
    .border({ width: { top: 1 }, color: COLORS.line })
  }

底部导航使用 Row 单排布局,四个 Tab 通过 ForEach 渲染,每个 Tab 占据 layoutWeight(1) 等宽空间。选中态通过四维度视觉差异体现:图标字号从 17 放大到 20、透明度从 0.65 提升到 1.0、标签文字从三级色变为电光蓝(COLORS.tabOn)、字重从常规变为粗体。这种多维度的状态反馈让用户对当前所在页面有明确的感知。

顶部使用 border({ width: { top: 1 }, color: COLORS.line }) 仅设置上边框线,将导航栏与内容区视觉分隔。点击事件直接设置 currentTab 状态,触发主内容区的条件分支切换到对应 Tab 的 Builder 函数。这种简洁的 Tab 切换机制无需 Tabs 组件的复杂配置,适合内容区不需要滑动切换的场景。

二十一、Builder 函数群:弹窗系统

21.1 全屏遮罩

  @Builder
  modalOverlay(onClose: () => void) {
    Stack() {
      Column().width('100%').height('100%').backgroundColor(COLORS.mask)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
    .onClick(() => onClose())
  }

modalOverlay 是弹窗系统的通用遮罩 Builder,接收一个 onClose 回调函数。遮罩是一个全屏的 Stack,内含一个铺满屏幕的 Column,背景色为 COLORS.maskrgba(4,10,20,0.66),深空黑 66% 透明度)。alignContent(Alignment.Center) 使 Stack 的子元素居中对齐——后续的面板会作为 Stack 的兄弟元素叠加在遮罩上方并居中显示。

遮罩的 onClick 绑定 onClose 回调,用户点击遮罩区域即可关闭弹窗。这是弹窗交互的标准设计——点击遮罩关闭比强制点击关闭按钮更符合用户直觉。遮罩作为公共 Builder 被三个弹窗面板复用,避免了重复代码。

21.2 新建下载任务弹窗

  @Builder
  panelAdd(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column({ space: 12 }) {
        Text('新建下载任务').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        Column({ space: 6 }) {
          Text('下载链接').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.formUrl, placeholder: 'https://dl.example.com/pkg.apk' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.formUrl = value;
            })
        }
        .width('100%').alignItems(HorizontalAlign.Start)

        Column({ space: 6 }) {
          Text('备注(可选)').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.formNote, placeholder: '如:官网直链 / 同事推荐' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.formNote = value;
            })
        }
        .width('100%').alignItems(HorizontalAlign.Start)

        Text('发起后经 startDownload 触发,完成回调记录双 URL').fontSize(8).fontColor(COLORS.text3)

        Row({ space: 10 }) {
          Text('取消').fontSize(12).fontColor(COLORS.sub)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.chip).borderRadius(9)
            .onClick(() => onClose())
          Text('开始下载').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.blue).borderRadius(9)
            .onClick(() => {
              this.saveDownload();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

新建下载任务弹窗的面板宽度为 78%,居中显示在遮罩之上。面板内包含标题"新建下载任务"、下载链接输入框、备注输入框(可选)、技术提示文案和取消/开始下载两个按钮。

下载链接输入框的 placeholder 使用了一个完整的示例 URL https://dl.example.com/pkg.apk,引导用户输入正确格式的 URL。备注输入框的 placeholder "如:官网直链 / 同事推荐"引导用户为下载任务打标签。底部的技术提示"发起后经 startDownload 触发,完成回调记录双 URL"告知用户下载流程的技术链路——从 startDownload 发起到 onDownloadFinish 回调记录双 URL,用户可以了解整个下载溯源的工作原理。

按钮区域"取消"用芯片色背景、电光蓝"开始下载"用粗体白色文字。点击"开始下载"调用 saveDownload() 方法发起下载并关闭弹窗。两个按钮等宽分布,视觉上一弱一强引导用户点击主操作。

21.3 编辑备注弹窗

  @Builder
  panelEdit(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column({ space: 12 }) {
        Text('编辑下载备注').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        Column({ space: 6 }) {
          Text('记录备注').fontSize(9).fontColor(COLORS.sub)
          TextInput({ text: this.editNote, placeholder: '如:官网直链 / 限时免费' })
            .fontSize(11).fontColor(COLORS.title)
            .backgroundColor(COLORS.chip).borderRadius(8)
            .onChange((value: string) => {
              this.editNote = value;
            })
        }
        .width('100%').alignItems(HorizontalAlign.Start)

        Row({ space: 10 }) {
          Text('取消').fontSize(12).fontColor(COLORS.sub)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.chip).borderRadius(9)
            .onClick(() => onClose())
          Text('保存').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.blue).borderRadius(9)
            .onClick(() => {
              this.updateRecord();
            })
        }
        .width('100%')
      }
      .width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

编辑备注弹窗的结构与新建弹窗类似但更简洁,只有一个备注输入框。输入框绑定 editNote 状态,该状态在 openEditRecord 方法中被预填充了当前记录的备注值,实现编辑表单的回填。用户修改后点击"保存"调用 updateRecord() 方法,通过 slice() 创建数组新引用触发列表刷新。

21.4 删除确认弹窗

  @Builder
  panelDel(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column({ space: 14 }) {
        Text('🗑').fontSize(30)
        Text('删除这条下载记录?').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Text('仅移除记录,不影响已保存到沙箱的文件').fontSize(9).fontColor(COLORS.text3)
        Row({ space: 10 }) {
          Text('取消').fontSize(12).fontColor(COLORS.sub)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.chip).borderRadius(9)
            .onClick(() => onClose())
          Text('删除').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            .layoutWeight(1).textAlign(TextAlign.Center)
            .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.red).borderRadius(9)
            .onClick(() => {
              this.delRecord();
            })
        }
        .width('100%')
      }
      .width('72%').padding(18).backgroundColor(COLORS.card).borderRadius(14)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

删除确认弹窗比新建和编辑弹窗更简洁,面板宽度为 72%(比其他弹窗的 78% 略窄),因为它不需要输入框,内容更少。面板顶部是一个大号垃圾桶 emoji(30px),下方是标题"删除这条下载记录?“和风险提示"仅移除记录,不影响已保存到沙箱的文件”。

风险提示文案明确告知用户:删除操作只移除列表中的记录,不会删除已保存到沙箱目录的实际文件。这种"仅移记录不删文件"的设计是安全的——用户可以放心删除记录,不必担心误删已下载的安装包。确认删除按钮使用红色背景(COLORS.red),与取消按钮的芯片色形成强烈对比,红色在 UI 设计中代表警示和危险,提醒用户这是一个需要谨慎确认的操作。

二十二、技术特性对比

特性维度本应用实现方案传统实现方案优势说明
下载来源追溯WebDownloadDelegate 的 getOriginalUrl + getReferrerUrl 双 URL 接口仅记录文件名和大小,丢失来源页面信息双链条完整审计,原始直链 + 引用页同时追溯,满足安全审计需求
下载生命周期控制onBeforeDownload + onDownloadUpdated + onDownloadFailed + onDownloadFinish 四回调浏览器默认下载行为,应用无法感知全流程精细控制,可自定义保存路径、实时进度、失败处理和完成溯源
主动下载能力webController.startDownload 应用侧直接发起必须打开网页由用户点击下载链接无需网页中转,首页推荐和弹窗可一键直达下载
文件保存路径onBeforeDownload 中通过 hostContext.filesDir 自定义沙箱路径浏览器默认下载目录,不可控统一管理下载文件,便于后续安全扫描和自动更新检查
数据可视化Canvas 原生绘制环形图 + 呼吸动画联动引入 MPChart 等第三方图表库零依赖,扇区 + 镂空 + 中心文字 + 图例一体化,支持呼吸脉动
响应式数据刷新@Observed 类 + @State 数组 + slice 创建新引用手动调用 notifyDataSetChanged精准局部刷新,unshift 新增和 splice 删除自动触发列表更新
主题方案深空黑 + 电光蓝 + 荧光青深色三色系Material Design 标准色板或浅色主题OLED 省电、暗光护眼、科技感强、品牌辨识度高
布局策略四 Tab 完全差异化布局(渐变大卡/ArkWeb特性/双URL溯源/用户中心)统一列表式布局视觉丰富度高,功能区分明确,避免审美疲劳
弹窗系统Stack 叠层 + 条件渲染 + 回调式关闭 + 公共遮罩Dialog 组件或第三方弹窗库无外部依赖,遮罩与面板解耦复用,关闭逻辑灵活可控
呼吸动画setInterval 每秒翻转 breath + Canvas 重绘属性动画或 Lottie 动画框架轻量级实现,一处状态驱动多处效果,资源占用低
地址栏设计urlInput 输入态与 webUrl 加载态双状态分离输入即加载或需手动控制加载时机防止逐字加载,前往按钮显式触发,输入与加载解耦
错误处理try-catch 捕获 BusinessError + console.error 输出异常冒泡或静默失败防止应用崩溃,错误码可追溯,便于排查下载代理绑定和主动下载失败

二十三、总结

本应用完整展示了 HarmonyOS ArkUI 框架在软件下载站场景下的工程实践能力,核心围绕 ArkWeb 的 WebDownloadDelegate 双 URL 溯源特性展开。从整体架构来看,代码通过颜色系统、常量定义、辅助函数、数据模型、组件主体、Builder 函数群六个层次的清晰划分,实现了高内聚低耦合的代码组织。颜色系统集中管理 16 个主题色字段(包括专属的 codeBg 代码背景色),为未来主题切换预留了接口契约;常量定义将所有 Mock 数据和配置选项抽离为独立常量,便于后续替换为真实接口数据;辅助函数封装了趋势映射和域名提取等可复用逻辑;数据模型使用 @Observed 装饰器实现响应式数据追踪。

ArkWeb 双 URL 溯源是本应用的技术灵魂。通过 WebDownloadDelegate 的四个生命周期回调(onBeforeDownload/onDownloadUpdated/onDownloadFailed/onDownloadFinish),应用获得了对下载全流程的精细控制权。onBeforeDownload 中通过 item.start() 提供沙箱路径使下载任务真正开始;onDownloadUpdated 实时刷新进度条和状态文案;onDownloadFailed 捕获失败并记录 GUID 便于排查;onDownloadFinish 中调用 HarmonyOS 6.1.1 新增的 getOriginalUrl() 和 getReferrerUrl() 双接口,将下载项的原始直链和引用页 URL 完整记录到 DownloadRecord 中。这种双 URL 溯源能力让每一次下载都能追溯"从哪个页面、下载的哪个直链",填补了传统下载管理器丢失来源页面信息的审计空白。

应用侧主动下载能力是 ArkWeb 区别于传统浏览器的另一重要特性。通过 webController.startDownload(url) 方法,应用代码可以直接传入一个 URL 发起下载,无需打开网页、无需用户点击下载链接。首页今日精选的"获取"按钮、新建下载任务弹窗的"开始下载"按钮、网页 Tab 的主动下载演示区,都可以直接调用 triggerDownload 方法发起下载。这种主动下载能力在软件下载站场景中极具价值——用户在首页看到推荐软件即可一键下载,无需跳转到网页再寻找下载链接,大幅缩短了从发现到下载的操作路径。

Canvas 环形图绘制展示了 ArkUI 的原生图形渲染能力。通过 CanvasRenderingContext2D 上下文对象,应用使用与 Web Canvas API 高度一致的接口绘制了下载分类占比环形图——扇区填充、中心镂空、中心文字叠加、右侧图例四个步骤一气呵成。更巧妙的是,环形图与呼吸动画定时器联动,每秒根据 breath 状态调整半径倍率(1.04 或 0.96)实现脉动效果,让静态图表变成了"活"的数据可视化。这种纯 Canvas 绘制方案零第三方依赖,可定制性高,且支持呼吸动画无缝集成。

四 Tab 差异化布局设计是本应用在 UI 层面的亮点。首页 Tab 采用今日精选渐变大卡 + 大编号热软榜 + Canvas 环形图的组合,信息密度高、视觉层次丰富;网页 Tab 采用地址栏 + 快捷站点 + Web 组件 + 主动下载演示的组合,完整展示了 ArkWeb 的网页加载和下载能力;下载 Tab 采用进行中任务卡 + 代码预览卡 + 双 URL 溯源列表的组合,将下载管理和来源审计融为一体;我的 Tab 采用用户等级渐变大卡 + 功能清单行的组合,简洁地组织个人信息和功能入口。每个 Tab 的布局都经过独立设计,避免了千篇一律的列表式界面。

深色主题方案的实现也值得称道。深空黑 #0A1220 作为背景色带有微弱蓝调,比纯黑更有层次感,在 OLED 屏幕上具有显著的省电效果。电光蓝 #3D8BFF 和荧光青 #35E0C8 的冷暖对比创造了科技感十足的视觉张力。多处使用 linearGradient 渐变背景——头部 Banner 用 120 度渐变、用户等级大卡用 135 度渐变,微妙的角度差异避免了视觉重复。呼吸动画通过一个 setInterval 定时器每秒翻转 breath 布尔值,驱动环形图半径脉动,实现了"一处状态控制多处动画"的高效设计。

弹窗系统采用了 Stack 叠层 + 条件渲染 + 回调式关闭的架构。modalOverlay 提供通用的半透明遮罩,panelAdd/panelEdit/panelDel 三个面板分别处理新建、编辑、删除场景。每个面板接收 onClose 回调函数,实现了关闭逻辑的灵活委托。编辑保存时通过 slice() 创建数组新引用确保 ForEach 列表刷新,这是 ArkUI 响应式系统的关键技巧——直接修改数组元素属性不会触发刷新,必须通过创建新引用让框架检测到变化。删除确认弹窗使用红色背景的确认按钮和"仅移除记录不影响文件"的风险提示,遵循了破坏性操作的设计规范。

从工程实践的角度看,本应用虽然是一个功能演示级别的单文件应用,但其代码组织方式、状态管理策略、UI 设计理念都体现了成熟的前端工程思维。@Observed + @State 的响应式数据流、Builder 函数的参数化复用、条件渲染驱动的弹窗系统、try-catch 包裹 BusinessError 的防御性编程、双状态分离的地址栏设计,这些模式都可以直接迁移到更大规模的模块化项目中。对于希望学习 HarmonyOS ArkUI 开发和 ArkWeb 下载溯源能力的开发者来说,本应用是一个结构完整、注释详尽、可直接运行参考的优质学习材料。

附录:DevEco Studio 创建新项目与查看 SDK 版本

本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。


一、创建新项目

1.1 进入欢迎界面

启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:

  • 新建项目:从头创建新项目
  • 打开项目:打开本地已有项目
  • 克隆仓库:从 Git 等版本控制拉取代码

点击 “新建项目” 按钮,进入项目创建向导。

在这里插入图片描述

1.2 选择项目模板

在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:

类型说明
应用(Application)开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期
元服务(Atomic Service)开发轻量级的原子化服务,无需安装即可使用

选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

在这里插入图片描述

1.3 配置项目信息

点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:

配置项示例值说明
项目名称(Project name)rollboat应用的项目名称,建议使用英文命名
包名(Bundle name)com.rollboat.myapplication应用唯一标识,采用反向域名格式
保存路径(Save location)D:\CodeFactory\rollboat项目本地存储路径,避免使用中文和空格
兼容 SDK(Compatible SDK)6.1.1(24)目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异
模块名称(Module name)entry主模块名称,默认 entry 为应用入口模块
设备类型(Device types)☑ Phone勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV

右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

在这里插入图片描述

1.4 完成创建

确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 Hvigor 构建初始化(Build Init

构建日志中显示 “退出代码为 0” 表示项目初始化成功。

在这里插入图片描述

1.5 项目结构概览

创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:

rollboat/
├── .hvigor/                   # Hvigor 构建工具缓存
├── .idea/                     # IDE 配置文件
├── AppScope/                  # 应用级全局配置
│   └── app.json5
├── entry/                     # 主模块(入口模块)
│   ├── src/main/ets/
│   │   ├── entryability/      # Ability 生命周期管理
│   │   │   └── EntryAbility.ets
│   │   └── pages/             # UI 页面
│   │       └── Index.ets      # 首页(默认 Hello World)
│   ├── src/main/resources/    # 资源文件
│   ├── module.json5           # 模块配置
│   └── build-profile.json5    # 构建配置
├── oh_modules/                # OHPM 依赖包
├── build-profile.json5        # 工程构建配置
├── hvigorfile.ts              # Hvigor 构建脚本
└── oh-package.json5           # 包管理配置

核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.message = 'Welcome';
        })
    }
    .height('100%')
    .width('100%')
  }
}
关键语法作用
@Entry标记为页面入口,可用于路由跳转
@Component声明为自定义组件
@State状态变量,数据变更时自动触发 UI 刷新
RelativeContainer相对布局容器,替代传统线性布局
.onClick()点击事件,此处点击后文本变为 “Welcome”

打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

在这里插入图片描述


二、查看 SDK 版本

2.1 查看 HarmonyOS SDK

DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:

文件 → 设置 → HarmonyOS SDK(或快捷键 Ctrl + Alt + S 搜索 “HarmonyOS SDK”)

在设置面板中,可以看到当前已安装的 SDK 版本信息:

名称阶段状态
HarmonyOS 6.1.1Release✅ 已安装

界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

在这里插入图片描述

2.2 查看 ArkUI-X SDK(跨平台扩展)

如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:

文件 → 设置 → 语言和框架 → ArkUI-X

在这里可以查看已安装和可选的 ArkUI-X SDK 版本:

版本SDK 版本号阶段状态
API Version 246.1.1.100Release✅ 已安装
API Version 236.1.0.28Beta1未安装
API Version 226.0.2.112Release未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

在这里插入图片描述


三、小结

步骤操作关键点
创建项目欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成使用 Stage 模型 + ArkTS 语言
查看 SDK设置 → HarmonyOS SDKSDK 已内置,无需手动安装
跨平台扩展设置 → ArkUI-X根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。

Logo

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

更多推荐