一、技术前言

在高校信息化建设浪潮中,校园资讯阅读平台正经历从"静态公告板"到"多媒体信息枢纽"的深刻转型。从头条要闻的横滑大卡推送,到频道×栏目的双层嵌套浏览,再到网页直达的校园站点访问与下载溯源追踪,每一项功能都需要匹配不同的交互模式和数据处理策略。传统校园资讯应用面临三大技术瓶颈:双语听力场景下字幕语言无法灵活切换导致听觉体验割裂、下载来源不透明导致文件溯源困难、多层列表嵌套滑动时手势冲突导致翻页卡顿。
在这里插入图片描述

HarmonyOS ArkUI 框架为这些瓶颈提供了系统级的声明式解决方案。ArkUI 的声明式 UI 范式通过 @Component 装饰器封装可复用组件,以 @State 管理响应式状态,用 @Builder 拆分复杂 UI 结构,天然适配"资讯-频道-用户"三层架构。@Observed 装饰器使数据模型字段级变化被 UI 感知,实现"要闻新增即列表刷新"的流畅体验。ForEach 的键值回调确保列表渲染高效且状态不丢失。

在这里插入图片描述
本平台深度融合 HarmonyOS 6.1.1 的三大前沿特性。Speech Kit 的 AI 字幕组件引入了 sourceLanguagetargetLanguagefontSizefontColor 四个新字段,支持中英双向翻译、中英双语对照、四档字号调节和五色字体预设,配合 writeAudio 640 字节 PCM 块写入实现实时语音转字幕——中文源时目标语言锁定 'zh',英文源时可在中文、英文、中英双语三选。ArkWeb 的下载代理 WebDownloadDelegate 提供四回调齐全的下载生命周期管理,onDownloadFinishgetOriginalUrl(原始 URL)与 getReferrerUrl(引用页 URL)双接口还原每次下载来路,getTotalBytes 换算文件大小,startDownload 支持应用侧主动发起下载。Tabs 嵌套滚动通过 nestedScroll(TabsNestedScrollMode) 实现 SELF_FIRST 模式下内层栏目滑到边缘后外层频道接力翻页的流畅体验,API 24 全局枚举零 import。

在这里插入图片描述

二、整体架构流程图

数据模型层

弹窗三态系统

三大特性挂载点

六大 Tab 模块

页面根容器

Page1222 主组件

headerBanner 头部渐变横幅

内容区 6 Tab 切换

tabBar 底部导航

弹窗系统 三态渲染

Tab0 头条
要闻横滑大卡+校园热榜+月度柱状图

Tab1 频道
外层频道×内层栏目双层嵌套Tabs

Tab2 网页
地址栏+快捷站点+Web组件+主动下载

Tab3 下载
进行中任务卡+双URL溯源记录

Tab4 听报
AI字幕五区块设置面板

Tab5 我的
订阅身份渐变卡+收藏清单

特性C Tabs嵌套滚动
nestedScroll SELF_FIRST

特性B ArkWeb下载
双URL溯源四回调

特性A Speech Kit
AI字幕四新字段

breath呼吸动画
柱状图±6%波动

panelAdd 新增要闻

panelEdit 编辑来源时间

panelDel 删除确认

NewsItem 要闻实体

InnerCard 栏目卡片

SwipeLog 翻页日志

DownloadRecord 下载记录

CaptionScene 字幕场景

架构以 Page1222 为根组件,使用 Stack 容器层叠:底层 Column 纵向排列头部渐变横幅、分割线、内容区和底部 Tab 栏,顶层是三个独立弹窗各自条件渲染。内容区通过 currentTab 在 6 个 @Builder 方法间切换,三大特性分散在频道(嵌套滚动)、网页+下载(ArkWeb 双 URL 溯源)和听报(AI 字幕)四个 Tab 上,状态变量统一声明在组件顶层实现跨 Tab 共享。breath 呼吸动画定时器驱动柱状图波动和圆点闪烁,aboutToAppear 中绑定下载代理,aboutToDisappear 中清理定时器,形成完整的生命周期闭环。

在这里插入图片描述

三、色彩体系设计

3.1 ColorPalette 接口定义

interface ColorPalette {
  bg: string;       // 页面底色·浅云蓝白
  card: string;     // 卡片底色·纯白
  chip: string;     // 胶囊/输入底色·浅湖蓝
  title: string;    // 主标题·墨蓝黑
  sub: string;      // 次级文字·青灰蓝
  text3: string;    // 弱化文字·雾蓝灰
  blue: string;     // 主色·青春蓝
  red: string;      // 辅色·活力红
  green: string;    // 辅色·青葱绿
  orange: string;   // 辅色·暖阳橙
  line: string;     // 分割线·浅雾线
  tabOn: string;    // Tab 激活色·青春蓝
  mask: string;     // 弹窗遮罩·墨蓝半透
  white: string;    // 渐变卡上的纯白文字
  whiteSoft: string; // 渐变卡上的弱化白文字
  trackW: string;   // 渐变卡上的进度条轨道色
  gradA: string;    // 渐变起点·青春蓝
  gradB: string;    // 渐变终点·深青春蓝
}

ColorPalette 接口以结构化方式集中声明了页面所有颜色字段。这种设计将视觉规范从业务逻辑中解耦,使主题色调整只需修改一处常量即可全局生效。接口字段涵盖三个层次:基础层(bg/card/chip 用于容器底色)、文字层(title/sub/text3 三级灰阶)、强调层(blue/red/green/orange 四色辅色),以及渐变专用层(white/whiteSoft/trackW/gradA/gradB)。特别值得注意的是渐变字段组——渐变卡上的文字不能使用普通文字色,因为渐变背景较深,需要纯白或半透白才能保证对比度。

在这里插入图片描述

3.2 COLORS 常量逐色分析

const COLORS: ColorPalette = {
  bg: '#F4F7FB',       // 浅云蓝白,营造清爽校园氛围
  card: '#FFFFFF',     // 纯白卡片底,内容承载面
  chip: '#E9F0F8',     // 浅湖蓝,胶囊与输入框底色
  title: '#1F2A3A',    // 墨蓝黑,主标题高对比
  sub: '#5C6B80',      // 青灰蓝,次级信息柔和
  text3: '#93A2B5',    // 雾蓝灰,辅助提示不抢视觉
  blue: '#3B82F6',     // 青春蓝,主色贯穿全局
  blueD: '#2563C9',    // 深青春蓝,渐变终点与招聘分类
  red: '#EF5350',      // 活力红,热榜 TOP1 与删除操作
  green: '#34A870',    // 青葱绿,学术分类与完成状态
  orange: '#F5A623',   // 暖阳橙,社团分类与进行中状态
  line: '#DFE8F2',     // 浅雾线,低对比分割
  tabOn: '#3B82F6',    // Tab 激活色与主色一致
  mask: 'rgba(31,42,58,0.5)', // 墨蓝半透遮罩
  white: '#FFFFFF',              // 渐变卡纯白文字
  whiteSoft: 'rgba(255,255,255,0.85)', // 渐变卡弱化白
  trackW: 'rgba(255,255,255,0.35)',    // 渐变卡进度轨道
  gradA: '#3B82F6',    // 渐变起点·青春蓝
  gradB: '#2563C9'     // 渐变终点·深青春蓝
};

色彩体系以"青春蓝 + 活力红"为核心对比。青春蓝(#3B82F6)作为主色贯穿头部渐变横幅、按钮、选中态、进度条等全局交互元素,传递校园的朝气与科技感。活力红(#EF5350)仅用于热榜 TOP1 编号和删除确认按钮两处,形成强烈的视觉警示锚点。四色辅色系统与要闻分类一一对应:头条青春蓝、学术青葱绿、社团暖阳橙、体育活力红、招聘深青春蓝——用户通过色条封面即可快速辨识资讯类别。头部横幅使用 linearGradientgradA(青春蓝)到 gradB(深青春蓝)的 160° 渐变,模拟校园天空从晨光到正午的蓝色过渡。渐变卡上的文字和进度条轨道使用半透白(whiteSofttrackW),确保在渐变背景上的可读性。

在这里插入图片描述

四、Tab 元数据与辅助数据

4.1 底部导航 Tab 定义

interface TabMeta {
  icon: string;   // Tab 图标
  label: string;  // Tab 标签
}

const TAB_LIST: TabMeta[] = [
  { icon: '📰', label: '头条' },
  { icon: '🌀', label: '频道' },
  { icon: '🌐', label: '网页' },
  { icon: '📥', label: '下载' },
  { icon: '🎧', label: '听报' },
  { icon: '👤', label: '我的' }
];

底部导航采用 6 Tab 单排布局,TabMeta 接口仅含 iconlabel 两个字段,保持元数据的极简性。6 个 Tab 分别对应资讯阅读的全链路:头条是内容消费入口,频道是分类浏览枢纽,网页是外部站点延伸,下载是离线资料管理,听报是无障碍音频入口,我的是个性化中心。底部导航在 tabBar() Builder 中通过 ForEach 遍历渲染,选中项使用 COLORS.tabOn(青春蓝)着色文字,未选中使用 COLORS.text3(雾蓝灰),选中图标在 breath 为 true 时透明度提升至 1.0,形成微妙的呼吸闪烁效果。

在这里插入图片描述

4.2 嵌套频道数据结构

interface ChannelItem {
  name: string;  // 频道名
  icon: string;  // 频道图标
}

const OUTER_CHANNELS: ChannelItem[] = [
  { name: '要闻', icon: '📰' },
  { name: '学术', icon: '🔬' },
  { name: '社团', icon: '🎭' },
  { name: '体育', icon: '⚽' },
  { name: '招聘', icon: '💼' }
];

const INNER_TABS: string[] = ['推荐', '最新', '热门', '深度', '图集'];

嵌套频道数据是特性 C(Tabs 嵌套滚动)的核心数据源。外层 OUTER_CHANNELS 定义 5 个校园频道,使用 BarMode.Scrollable 横滑页签模式渲染。内层 INNER_TABS 定义 5 个栏目子页签,每个子页签内通过 innerMockData() 生成器动态创建 8 条 InnerCard 资讯卡片。双层结构使得频道×栏目形成 5×5=25 个内容象限,用户可以在一个手势内完成两级导航切换。onChange 回调在每次翻页时记录 SwipeLog 日志,形成翻页行为的可视化追踪链。

4.3 快捷站点与语言选项

interface QuickSite {
  icon: string;
  name: string;
  url: string;
}

const QUICK_SITES: QuickSite[] = [
  { icon: '🏫', name: '北京大学', url: 'https://www.pku.edu.cn' },
  { icon: '📚', name: '北大图书馆', url: 'https://lib.pku.edu.cn' },
  { icon: '🎓', name: '清华大学', url: 'https://www.tsinghua.edu.cn' },
  { icon: '📖', name: '中国教育在线', url: 'https://www.eol.cn' }
];

interface SizeOption {
  size: AICaptionFontSize;
  name: string;
}

const SIZE_OPTIONS: SizeOption[] = [
  { size: AICaptionFontSize.SMALL, name: '小号' },
  { size: AICaptionFontSize.NORMAL, name: '标准' },
  { size: AICaptionFontSize.BIG, name: '大号' },
  { size: AICaptionFontSize.LARGE, name: '超大' }
];

快捷站点数据为网页 Tab 提供 4 个高校/教育类真实站点入口,点击即加载至 Web 组件。语言选项和字号档位服务于特性 A(AI 字幕)。SizeOptionsize 字段类型为 AICaptionFontSize 枚举而非 number,这体现了 Speech Kit 的类型安全设计——字号档位受控于枚举值,避免传入非法数值导致初始化失败。字幕颜色预设 CAPTION_FONT_COLORS 提供五色(白、暖黄、薄荷绿、天蓝、粉红),fontColor 类型为 ResourceColor,字符串形式即可直接传入。

4.4 热榜与月度图表数据

const MONTH_NAME: string[] = ['03月', '04月', '05月', '06月', '07月', '08月'];
const READ_VAL: number[] = [126, 168, 143, 192, 176, 214];
const BAR_MAX: number = 240;

interface HotItem {
  title: string;
  hot: number;
}

const HOT_RANK: HotItem[] = [
  { title: '我校团队破解硅光芯片耦合难题', hot: 48600 },
  { title: '2026 春季双选会 320 家企业进校', hot: 41200 },
  // ...共 8 条
];

月度阅读量数据为头条 Tab 的柱状图提供数据源,6 个月数据通过 barHeight() 方法换算为柱高。BAR_MAX(240 千次)是满刻度基准,breath 状态驱动奇偶柱交替波动 ±6%,形成动态的"呼吸"柱状图效果。校园热榜 HOT_RANK 包含 8 条真实校园话题,TOP1~3 的大编号使用 rankColor() 函数分别着色为活力红、暖阳橙、青葱绿,其余为雾蓝灰,形成前三名高亮的视觉层次。热度值通过 hotText() 格式化,过万缩写为 w(如 48600 显示为 4.9w)。

五、工具函数分析

5.1 嵌套模式与语言翻译函数

function modeLabel(mode: TabsNestedScrollMode): string {
  return mode === TabsNestedScrollMode.SELF_FIRST
    ? 'SELF_FIRST·先内后外' : 'SELF_ONLY·仅内层';
}

function modeShort(mode: TabsNestedScrollMode): string {
  return mode === TabsNestedScrollMode.SELF_FIRST ? '先内后外' : '仅内层';
}

function langName(code: string): string {
  if (code === 'zh') { return '中文'; }
  if (code === 'en') { return '英文'; }
  return '中英双语';
}

modeLabelmodeShort 是特性 C 的枚举翻译函数。TabsNestedScrollMode 是 API 24 全局枚举,无需 import 即可使用。SELF_FIRST 表示内层栏目优先消费滑动事件,滑到边缘后外层频道接力翻页;SELF_ONLY 表示内层独占滑动,滑到边缘后手势终结。两个函数分别提供完整文案和短文案——完整文案用于模式状态卡的说明文字,短文案用于头部胶囊和切换 chips 的紧凑展示。langName 将语言码翻译为展示名,支持 zh/en/zh-en 三种码值,用于字幕语言方向胶囊和场景卡的展示。

5.2 分类配色与热榜配色函数

function catColor(cat: string): string {
  if (cat === '头条') { return COLORS.blue; }
  if (cat === '学术') { return COLORS.green; }
  if (cat === '社团') { return COLORS.orange; }
  if (cat === '体育') { return COLORS.red; }
  if (cat === '招聘') { return COLORS.blueD; }
  return COLORS.text3;
}

function rankColor(idx: number): string {
  if (idx === 0) { return COLORS.red; }
  if (idx === 1) { return COLORS.orange; }
  if (idx === 2) { return COLORS.green; }
  return COLORS.text3;
}

catColor 实现要闻分类与色彩的映射:头条青春蓝、学术青葱绿、社团暖阳橙、体育活力红、招聘深青春蓝。这五色映射贯穿横滑大卡的色条封面、分类图标徽标和热度值文字,用户通过颜色即可快速辨识资讯所属类别。rankColor 为热榜大编号配色:TOP1 活力红、TOP2 暖阳橙、TOP3 青葱绿,其余为雾蓝灰——前三名使用高饱和度辅色突出,其余降级为灰色,形成金字塔式的视觉优先级。

5.3 热度格式化与下载状态配色

function hotText(hot: number): string {
  if (hot >= 10000) {
    return (hot / 10000).toFixed(1) + 'w';
  }
  return hot.toString();
}

function dlStateColor(state: string): string {
  if (state.indexOf('失败') >= 0) { return COLORS.red; }
  if (state.indexOf('下载') >= 0 || state.indexOf('开始') >= 0 || state.indexOf('发起') >= 0) {
    return COLORS.orange;
  }
  if (state.indexOf('完成') >= 0) { return COLORS.green; }
  return COLORS.text3;
}

hotText 将大数值热度缩写为万级单位(48600 → 4.9w),在热榜徽标和要闻卡片中保持紧凑显示。dlStateColor 是特性 B 的下载状态色彩映射函数,通过字符串匹配判断下载阶段:含"失败"返回活力红、含"下载"/“开始”/"发起"返回暖阳橙(进行中)、含"完成"返回青葱绿、其余返回雾蓝灰(空闲)。该函数在头部胶囊、下载任务卡和下载记录列表中统一调用,确保下载状态色彩全页面一致。

六、数据模型层

6.1 NewsItem 要闻实体

@Observed
export class NewsItem {
  title: string;   // 要闻标题
  cat: string;     // 分类
  source: string;  // 来源
  time: string;    // 发布时间
  hot: number;     // 热度值

  constructor(title: string, cat: string, source: string,
    time: string, hot: number) {
    this.title = title;
    this.cat = cat;
    this.source = source;
    this.time = time;
    this.hot = hot;
  }
}

NewsItem 是头条 Tab 的核心业务实体,@Observed 装饰器使其字段级变化被 UI 自动感知。当 saveNews()unshift 新增条目或 editNews() 中修改 source/time 字段后,绑定该数组的 ForEach 会自动刷新。newsList = this.newsList.slice() 的赋值触发数组引用变更,确保 @Observed 的深层监听生效。8 条 Mock 数据覆盖头条、学术、社团、体育、招聘五类分类,热度值从 48600 到 21500 递减,与热榜内容遥相呼应。

6.2 InnerCard 栏目卡片与生成器

@Observed
export class InnerCard {
  id: string;     // 唯一键
  tag: string;    // 所属栏目
  title: string;  // 卡片标题
  desc: string;   // 卡片描述

  constructor(id: string, tag: string, title: string, desc: string) {
    this.id = id;
    this.tag = tag;
    this.title = title;
    this.desc = desc;
  }
}

function innerMockData(channel: ChannelItem, tabName: string): InnerCard[] {
  const list: InnerCard[] = [];
  for (let i = 1; i <= 8; i++) {
    list.push(new InnerCard(
      `${channel.name}-${tabName}-${i}`,
      tabName,
      `${tabName}·${INNER_TITLES[i - 1]}${i}`,
      `${channel.icon}${channel.name}」频道「${tabName}」栏目第 ${i} 条:${INNER_NOTES[i - 1]}`));
  }
  return list;
}

InnerCard 是频道 Tab 嵌套列表的条目实体,id 字段格式为"频道名-栏目名-序号"确保全局唯一,作为 ForEach 的键值回调防止列表渲染错乱。innerMockData 生成器接收频道和栏目名参数,循环创建 8 条卡片——8 条的容量保证内容超出一屏,这是 nestedScroll 演示的前提条件:只有内层内容可滚动,才能触发"滑到边缘后外层接力"的嵌套行为。标题拼接栏目名与素材池 INNER_TITLES,描述拼接频道图标、频道名、栏目名和行业注解 INNER_NOTES,形成层次丰富的卡片信息。

6.3 SwipeLog 翻页日志模型

@Observed
export class SwipeLog {
  layer: string;    // 层级
  tabName: string;  // 翻到的页签
  fromIdx: number;  // 起始索引
  toIdx: number;    // 目标索引
  mode: string;     // 嵌套模式
  time: string;     // 记录时间

  constructor(layer: string, tabName: string, fromIdx: number,
    toIdx: number, mode: string) {
    this.layer = layer;
    this.tabName = tabName;
    this.fromIdx = fromIdx;
    this.toIdx = toIdx;
    this.mode = mode;
    const d = new Date();
    this.time = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
  }
}

SwipeLog 是特性 C 嵌套滚动的行为追踪模型,记录每次翻页的层级(外层频道/内层栏目)、页签名、起止索引、触发时的嵌套模式和精确到秒的时间戳。构造函数内联生成时间字符串,使用 padStart(2, '0') 确保时分秒两位补零。日志通过 unshift 置顶插入,最多保留 40 条(超出时 pop 尾部),recentLogs() 方法取最近 4 条在模式状态卡中压缩展示,形成嵌套行为的可视化证据链。

6.4 DownloadRecord 下载记录模型

@Observed
export class DownloadRecord {
  fileName: string;     // 文件名
  fileSize: string;     // 文件大小
  finishTime: string;   // 完成时间
  originalUrl: string;  // 原始 URL
  referrerUrl: string;  // 引用页 URL

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

DownloadRecord 是特性 B 双 URL 溯源的核心数据载体。originalUrl 存储 getOriginalUrl() 的返回值——文件直链地址(如 https://news.pku.edu.cn/download/campus_paper.pdf?from=campus),referrerUrl 存储 getReferrerUrl() 的返回值——触发下载的引用页地址(如 https://news.pku.edu.cn/paper/list?year=2026)。这两个 URL 还原了"从哪个页面点了哪个链接下载了什么文件"的完整来路链条。6 条 Mock 数据的 URL 均采用"域名+路径+参数"的完整格式,模拟真实校园站点的下载场景。onDownloadFinish 回调中通过 unshift 将新记录置顶插入,用户完成下载后立即在下载 Tab 看到最新记录。

6.5 CaptionScene 字幕场景模型

@Observed
export class CaptionScene {
  scene: string;  // 场景名
  desc: string;   // 场景说明
  src: string;    // 推荐源语言
  tgt: string;    // 推荐目标语言

  constructor(scene: string, desc: string, src: string, tgt: string) {
    this.scene = scene;
    this.desc = desc;
    this.src = src;
    this.tgt = tgt;
  }
}

CaptionScene 是特性 A AI 字幕的场景推荐模型,每条记录封装一个听报场景(如"英语新闻听力")、场景说明和推荐的语言组合。5 条场景覆盖英文源和中文源两种情况:英文源场景推荐 zh-en(中英双语)或 zh(整体译成中文),中文源场景锁定 zh(无翻译方向可选)。用户点击场景卡时,applyScene() 调用 switchSourceLang() 联动源语言与目标语言,一键应用推荐的组合配置,降低用户理解语言参数的学习成本。

七、组件主体结构

7.1 @State 状态变量群

@Entry
@Component
struct Page1222 {
  // 基础 UI 状态
  @State currentTab: number = 0;
  @State breath: boolean = false;
  private timer: number = -1;

  // 弹窗三态
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = -1;
  @State delIdx: number = -1;

  // 表单状态
  @State formTitle: string = '';
  @State formCat: string = '头条';
  @State formSource: string = '';
  @State editSource: string = '';
  @State editTime: string = '';

  // 头条业务状态
  @State newsList: NewsItem[] = NEWS_LIST;

  // 特性 A 状态(AI 字幕)
  private captionController: AICaptionController = new AICaptionController();
  @State captionShown: boolean = false;
  @State srcLang: string = 'zh';
  @State tgtLang: string = 'zh';
  @State captionSize: AICaptionFontSize = AICaptionFontSize.NORMAL;
  @State captionColor: string = CAPTION_FONT_COLORS[0];
  @State captionReady: boolean = false;
  @State captionErrMsg: string = '';
  @State captionFed: number = 0;
  @State sceneList: CaptionScene[] = SCENE_LIST;

  // 特性 B 状态(ArkWeb 下载)
  private webController: webview.WebviewController = new webview.WebviewController();
  private downloadDelegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate();
  @State urlInput: string = QUICK_SITES[0].url;
  @State webUrl: string = QUICK_SITES[0].url;
  @State dlName: string = '';
  @State dlPercent: number = 0;
  @State dlState: string = '空闲';
  @State downloadRecords: DownloadRecord[] = DOWNLOAD_RECORDS;

  // 特性 C 状态(Tabs 嵌套滚动)
  @State nestedMode: TabsNestedScrollMode = TabsNestedScrollMode.SELF_FIRST;
  @State outerIndex: number = 0;
  @State innerIndex: number = 0;
  @State swipeLogs: SwipeLog[] = [];
}

状态变量按功能分为五组,组织清晰。基础 UI 组控制 Tab 切换和呼吸动画;弹窗三态组管理新增/编辑/删除弹窗的显示与索引;表单组暂存用户输入的标题、分类、来源和时间;特性 A 组集中管理 AI 字幕的控制器、显示状态、语言方向、字号、颜色、就绪状态和错误信息;特性 B 组管理 Web 控制器、下载代理、地址栏双状态、下载进度和记录列表;特性 C 组管理嵌套模式、内外层索引和翻页日志。

特别值得关注的是 urlInputwebUrl 的双状态分离设计——用户在地址栏输入时只更新 urlInput,点击"前往"后才将值同步到 webUrl 并加载到 Web 组件,避免输入过程中频繁触发网页加载。captionControllerwebController 使用 private 修饰符而非 @State,因为控制器实例本身不变化,只有其内部状态变化需要通知 UI。

7.2 生命周期方法

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

aboutToDisappear() {
  if (this.timer !== -1) {
    clearInterval(this.timer);
    this.timer = -1;
  }
}

aboutToAppear 在组件创建后、UI 渲染前执行两件事:注册下载代理和启动呼吸定时器。下载代理必须在 Web 组件渲染前绑定,否则网页内的下载操作无法进入回调。呼吸定时器每 1000ms 翻转 breath 布尔值,驱动柱状图奇偶柱交替波动和圆点透明度闪烁。aboutToDisappear 在组件销毁时清理定时器,防止内存泄漏——通过 timer !== -1 的哨兵检查避免重复清理。这一对生命周期方法构成了资源管理的完整闭环。

八、头部区域详解

@Builder
headerBanner() {
  Column({ space: 10 }) {
    Row() {
      Column({ space: 4 }) {
        Text('📰 校闻在线 · 校园资讯阅读').fontSize(19).fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
        Text(this.currentTab === 0 ? `头条编辑部 · 今日要闻 ${this.newsList.length}`
          : this.currentTab === 1 ? '频道 · 频道×栏目双层 Tabs 嵌套'
            : this.currentTab === 2 ? '网页 · ArkWeb 校园站点直达'
              : this.currentTab === 3 ? `下载 · 双 URL 溯源 ${this.downloadRecords.length}`
                : this.currentTab === 4 ? `听报 · AI 字幕 ${langName(this.srcLang)}${langName(this.tgtLang)}`
                  : '我的 · 订阅与收藏').fontSize(11).fontColor(COLORS.whiteSoft)
      }.alignItems(HorizontalAlign.Start).layoutWeight(1)

      Circle({ width: 10, height: 10 })
        .fill(COLORS.white)
        .opacity(this.breath ? 0.9 : 0.45)
    }.width('100%')

    Row({ space: 8 }) {
      // 要闻胶囊
      Row({ space: 6 }) {
        Text('🗞').fontSize(10)
        Text(`要闻 ${this.newsList.length}`).fontSize(10).fontColor(COLORS.sub)
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.chip)

      // 嵌套模式胶囊
      Row({ space: 4 }) {
        Circle({ width: 6, height: 6 })
          .fill(this.nestedMode === TabsNestedScrollMode.SELF_FIRST ? COLORS.green : COLORS.orange)
        Text(`嵌套 ${modeShort(this.nestedMode)}`).fontSize(10).fontColor(COLORS.sub)
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.chip)

      // 字幕语言胶囊
      Row({ space: 4 }) {
        Circle({ width: 6, height: 6 }).fill(COLORS.blue)
        Text(`${this.srcLang}${this.tgtLang}`).fontSize(10).fontColor(COLORS.sub)
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.chip)

      // 下载状态胶囊
      Row({ space: 4 }) {
        Circle({ width: 6, height: 6 }).fill(dlStateColor(this.dlState))
        Text(`下载 ${this.dlState}`).fontSize(10).fontColor(COLORS.sub)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.chip).layoutWeight(1)
    }.width('100%')
  }.padding({ left: 16, right: 16, top: 12, bottom: 12 })
  .width('100%')
  .linearGradient({ angle: 160, colors: [[COLORS.gradA, 0], [COLORS.gradB, 1]] })
}

头部横幅是整个页面的视觉锚点,采用 160° 渐变从青春蓝到深青春蓝,模拟校园天空的蓝色过渡。横幅分为上下两行:上行左侧是标题和 Tab 联动副标题,右侧是呼吸圆点;下行是四枚状态胶囊。

副标题使用嵌套三元表达式根据 currentTab 切换文案——头条 Tab 显示要闻条数、频道 Tab 提示双层嵌套、网页 Tab 标注 ArkWeb、下载 Tab 显示溯源条数、听报 Tab 展示字幕语言方向、我的 Tab 简述订阅收藏。这种联动设计让用户在任何 Tab 下都能从头部获得当前功能的简明状态摘要。

呼吸圆点的透明度由 breath 状态驱动,在 0.9 和 0.45 之间每秒切换,形成微妙的"在线心跳"效果。四枚状态胶囊分别映射三大特性的实时状态:要闻胶囊显示条数、嵌套胶囊用圆点颜色区分模式(绿色 SELF_FIRST/橙色 SELF_ONLY)、字幕胶囊显示语言方向、下载胶囊用 dlStateColor 函数着色圆点并自适应文案长度。胶囊的 layoutWeight(1) 仅赋予最后一个下载胶囊,使其在空间不足时优先压缩并显示省略号。

九、头条 Tab 深度分析

9.1 要闻横滑大卡

@Builder
newsBigCard(item: NewsItem, idx: number) {
  Column({ space: 8 }) {
    // 分类色条封面
    Row() {
      Column().width(5).height('100%').backgroundColor(catColor(item.cat))
      Row({ space: 8 }) {
        Text(catIcon(item.cat)).fontSize(22)
        Column({ space: 2 }) {
          Text(item.cat + '频道').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
          Text('校闻在线 · ' + item.time).fontSize(9).fontColor(COLORS.text3)
        }.alignItems(HorizontalAlign.Start).layoutWeight(1)
        Text('🔥' + hotText(item.hot)).fontSize(10).fontColor(catColor(item.cat))
      }.layoutWeight(1).padding({ left: 8, right: 8 }).alignItems(VerticalAlign.Center)
    }.width('100%').height(58).backgroundColor(COLORS.chip).borderRadius(10)

    // 标题
    Text(item.title).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })

    // 来源行 + 编辑删除入口
    Row() {
      Text(item.source).fontSize(9).fontColor(COLORS.sub)
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
        .backgroundColor(COLORS.chip).borderRadius(7)
      Column().layoutWeight(1)
      Text('编辑').fontSize(9).fontColor(COLORS.blue)
        .onClick(() => { this.openEdit(idx); })
      Text('删除').fontSize(9).fontColor(COLORS.red)
        .onClick(() => { this.delIdx = idx; this.delModal = true; })
    }.width('100%')
  }.width(230).padding(10).backgroundColor(COLORS.card).borderRadius(12)
  .alignItems(HorizontalAlign.Start)
}

要闻横滑大卡是头条 Tab 的视觉核心。卡片宽度固定 230,在 Scroll 横滑容器中排列。封面区采用左侧 5px 分类色竖条的设计——catColor() 返回的分类色作为竖条背景,与分类图标、频道名、时间组成分类色条封面体系。热度徽标使用分类色着色,使每张大卡的色彩与分类一一对应,用户横滑浏览时通过色条即可快速定位类别。

标题限制两行并显示省略号,防止长标题撑破卡片。来源行右侧提供"编辑"和"删除"两个文字按钮,分别调用 openEdit(idx) 打开编辑弹窗和设置 delIdx 后打开删除确认弹窗。编辑按钮使用青春蓝、删除按钮使用活力红,色彩语义与操作后果一致。这种"卡片内直接操作"的设计减少了用户到达编辑/删除功能的操作步数。

9.2 校园热榜大编号榜

ForEach(HOT_RANK, (row: HotItem, idx: number) => {
  Row({ space: 10 }) {
    Text(String(idx + 1)).fontSize(idx < 3 ? 20 : 16).fontWeight(FontWeight.Bold)
      .fontColor(rankColor(idx)).width(34).textAlign(TextAlign.Center)
      .fontFamily('monospace')
    Column({ space: 3 }) {
      Text(row.title).fontSize(12).fontColor(COLORS.title).maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Text('热度 ' + hotText(row.hot) + ' · 校园热议中').fontSize(9).fontColor(COLORS.text3)
    }.layoutWeight(1).alignItems(HorizontalAlign.Start)
    Text('🔥 ' + hotText(row.hot)).fontSize(10).fontColor(rankColor(idx))
      .padding({ left: 8, right: 8, top: 4, bottom: 4 })
      .backgroundColor(COLORS.chip).borderRadius(10)
  }.width('100%').alignItems(VerticalAlign.Center)
}, (row: HotItem, idx: number) => 'hot-' + idx.toString())

校园热榜采用大编号榜布局,编号列固定宽度 34 使用等宽字体(monospace)确保数字对齐。TOP1~3 的编号字号 20、颜色分别为活力红/暖阳橙/青葱绿,第 4 名起字号降为 16、颜色降为雾蓝灰,形成前三名高亮的金字塔视觉。每行右侧的火焰热度徽标使用与编号相同的配色,保持行内色彩一致。标题单行截断,热度值通过 hotText 格式化为万级缩写。ForEach 的键值回调使用 'hot-' + idx 保证列表渲染稳定性。

9.3 月度阅读量柱状图

@Builder
chartCard() {
  Column({ space: 10 }) {
    Row() {
      Text('📊 校报近 6 个月阅读量').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Column().layoutWeight(1)
      Text('单位:千次').fontSize(9).fontColor(COLORS.text3)
    }.width('100%')

    Row({ space: 6 }) {
      ForEach(MONTH_IDX, (i: number) => {
        Column({ space: 4 }) {
          Text(`${READ_VAL[i]}`).fontSize(8).fontColor(COLORS.text3).fontFamily('monospace')
          Column()
            .width('62%')
            .height(this.barHeight(i))
            .borderRadius(4)
            .linearGradient({ angle: 180, colors: [[COLORS.blue, 0], [COLORS.blueD, 1]] })
          Text(MONTH_NAME[i]).fontSize(9).fontColor(COLORS.sub)
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
      }, (i: number) => `bar_${i}_${this.breath}`)
    }.width('100%').alignItems(VerticalAlign.Bottom).height(132)

    Text('柱高随 breath 呼吸在 ±6% 区间交替波动,满刻度按 240 千次换算')
      .fontSize(9).fontColor(COLORS.text3).width('100%')
  }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)
}

柱状图采用 Column + ForEach 的传统柱状实现方案,而非 Canvas 绘制。每根柱子由三部分组成:顶部的数值标签(等宽字体)、中间的渐变色柱体、底部的月份标签。柱体宽度设为 62%,留出柱间间距。柱高通过 barHeight(i) 方法动态计算——READ_VAL[i] / BAR_MAX * 96 得到基础高度,然后根据 breath 状态和柱索引的奇偶性乘以 1.06 或 0.94 的波动系数,实现奇偶柱交替波动的呼吸效果。柱体使用 180° 纵向渐变从青春蓝到深青春蓝,顶部亮底部暗,模拟光照立体感。

ForEach 的键值回调为 `bar_${i}_${this.breath}`——将 breath 值拼入键中,使得每次 breath 翻转时 ForEach 认为是全新的数据项,强制重新渲染所有柱子,从而触发柱高动画。这是一个巧妙的"键值驱动重渲染"技巧,无需显式动画 API 即可实现周期性的视觉刷新。

十、频道 Tab 与嵌套滚动深度分析

10.1 双层 Tabs 结构

@Builder
tabChannel() {
  Column({ space: 10 }) {
    // 模式切换 chips
    Row({ space: 8 }) {
      Text(`嵌套模式:${modeLabel(this.nestedMode)}`)
        .fontSize(11).fontColor(COLORS.sub).layoutWeight(1)
      ForEach([TabsNestedScrollMode.SELF_ONLY, TabsNestedScrollMode.SELF_FIRST],
        (m: TabsNestedScrollMode) => {
          Text(modeShort(m)).fontSize(10)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
            .fontColor(this.nestedMode === m ? COLORS.white : COLORS.text3)
            .backgroundColor(this.nestedMode === m ? COLORS.blue : COLORS.card)
            .onClick(() => { this.nestedMode = m; })
        }, (m: TabsNestedScrollMode) => `mode_${m}`)
    }.width('100%')

    // 外层频道 Tabs
    Tabs({ barPosition: BarPosition.Start }) {
      ForEach(OUTER_CHANNELS, (ch: ChannelItem) => {
        TabContent() {
          this.innerTabs(ch)
        }.tabBar(`${ch.icon} ${ch.name}`)
      }, (ch: ChannelItem) => ch.name)
    }
    .barMode(BarMode.Scrollable)
    .onChange((index: number) => {
      this.swipeLogs.unshift(new SwipeLog('外层频道', OUTER_CHANNELS[index].name,
        this.outerIndex, index, modeLabel(this.nestedMode)));
      this.outerIndex = index;
      if (this.swipeLogs.length > 40) { this.swipeLogs.pop(); }
    })
    .layoutWeight(1).width('100%')

    this.modeStateCard()
  }.width('100%').height('100%').padding({ left: 14, right: 14, top: 10, bottom: 8 })
}

频道 Tab 是特性 C 的核心宿主。顶部模式切换 chips 允许用户在 SELF_ONLYSELF_FIRST 两种嵌套模式间切换,选中态使用青春蓝底白字,未选中使用雾蓝灰。外层 Tabs 使用 BarMode.Scrollable 横滑页签模式渲染 5 个校园频道,每个 TabContent 内嵌套调用 innerTabs(ch) 构建内层栏目 Tabs。

onChange 回调在每次外层翻页时创建 SwipeLog 记录,包含层级标识"外层频道"、目标页签名、起止索引和当前嵌套模式。日志通过 unshift 置顶插入,超过 40 条时 pop 尾部,形成固定容量的滚动日志队列。这一设计使得用户可以随时在模式状态卡中回溯最近的翻页行为,验证嵌套模式是否生效。

10.2 内层 Tabs 与 nestedScroll 挂载

@Builder
innerTabs(channel: ChannelItem) {
  Tabs({ barPosition: BarPosition.Start }) {
    ForEach(INNER_TABS, (name: string) => {
      TabContent() {
        List({ space: 10 }) {
          ForEach(innerMockData(channel, name), (item: InnerCard) => {
            ListItem() {
              Column({ space: 6 }) {
                // ...卡片内容
              }.width('100%').padding(12).borderRadius(10).backgroundColor(COLORS.card)
            }
          }, (item: InnerCard) => item.id)
        }.width('100%').height('100%').scrollBar(BarState.Off)
      }.tabBar(name)
    }, (name: string) => name)
  }
  .barMode(BarMode.Scrollable)
  .onChange((index: number) => {
    this.swipeLogs.unshift(new SwipeLog('内层栏目', INNER_TABS[index],
      this.innerIndex, index, modeLabel(this.nestedMode)));
    this.innerIndex = index;
    if (this.swipeLogs.length > 40) { this.swipeLogs.pop(); }
  })
  .nestedScroll(this.nestedMode)
  .layoutWeight(1).width('100%')
}

内层 Tabs 是 nestedScroll 的挂载点。5 个栏目子页签内各含一个 List,每个 List 渲染 8 条 InnerCard 资讯卡片。nestedScroll(this.nestedMode) 是关键的 API 调用——当模式为 SELF_FIRST 时,内层 List 滑到底部边缘后,继续同向滑动会传递给外层 Tabs,触发外层频道翻页;当模式为 SELF_ONLY 时,内层滑到边缘后手势终结,需抬手重新滑外层页签才能换频道。

内层卡片的键值回调使用 item.id(格式"频道名-栏目名-序号"),确保 ForEach 在频道或栏目切换时正确复用或重建列表项。卡片内容包含频道图标+名称行、标题行、描述行和标签行,信息层次丰富但视觉密度适中。标签行的"频道"标签使用暖阳橙、"校园认证稿源"使用青葱绿,形成双重来源标识。

10.3 模式状态卡

@Builder
modeStateCard() {
  Column({ space: 8 }) {
    Row() {
      Text('🧭 嵌套模式状态').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Column().layoutWeight(1)
      Text(modeLabel(this.nestedMode)).fontSize(9).fontColor(
        this.nestedMode === TabsNestedScrollMode.SELF_FIRST ? COLORS.green : COLORS.orange)
    }.width('100%')

    Text(this.nestedMode === TabsNestedScrollMode.SELF_FIRST
      ? 'SELF_FIRST:内层栏目滑到最后一页后继续同向滑,外层频道立即接力翻页,一次手势完成两级切换'
      : 'SELF_ONLY(默认):内层栏目滑到边缘后手势终结,需抬手再滑外层页签才能换频道')
      .fontSize(9).fontColor(COLORS.sub).width('100%')

    // 最近翻页记录
    ForEach(this.recentLogs(), (log: SwipeLog) => {
      Row({ space: 8 }) {
        Text(log.layer === '外层频道' ? '外' : '内').fontSize(9)
          .fontColor(log.layer === '外层频道' ? COLORS.orange : COLORS.green)
          .width(16).height(16).textAlign(TextAlign.Center)
          .backgroundColor(COLORS.chip).borderRadius(8)
        Text(log.tabName).fontSize(10).fontColor(COLORS.title)
          .layoutWeight(1).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(`${log.fromIdx}${log.toIdx}`).fontSize(9).fontColor(COLORS.sub)
          .fontFamily('monospace')
        Text(log.time).fontSize(9).fontColor(COLORS.text3).fontFamily('monospace')
      }.width('100%').alignItems(VerticalAlign.Center)
    }, (log: SwipeLog) => `${log.time}_${log.tabName}_${log.toIdx}`)
  }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)
}

模式状态卡是嵌套滚动行为的可视化证据面板。顶部显示当前模式名和行为说明——SELF_FIRST 用青葱绿标注,SELF_ONLY 用暖阳橙标注,色彩暗示"推荐"与"默认"的区别。中部显示翻页记录计数和"清空"按钮。底部通过 recentLogs() 渲染最近 4 条翻页日志,每条日志以双徽标"外"/"内"标识层级(外层暖阳橙/内层青葱绿),后跟页签名、起止索引(等宽字体)和时间戳。

日志的键值回调使用 `${log.time}_${log.tabName}_${log.toIdx}` 拼接,保证唯一性。当用户在 SELF_FIRST 模式下连续滑动内层到边缘触发外层翻页时,会看到"内层→外层"的连续日志记录,这就是嵌套接力行为的直接证据。

十一、网页 Tab 与 ArkWeb 下载深度分析

11.1 地址栏与快捷站点

@Builder
tabWeb() {
  Column({ space: 10 }) {
    // 地址栏
    Row({ space: 8 }) {
      TextInput({ text: this.urlInput, placeholder: '输入校园站点,如 pku.edu.cn' })
        .layoutWeight(1).height(38).fontSize(11).fontColor(COLORS.title)
        .backgroundColor(COLORS.card).borderRadius(10)
        .onChange((value: string) => { this.urlInput = value; })
      Text('前往').fontSize(11).fontColor(COLORS.white)
        .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: QuickSite) => {
          Row({ space: 5 }) {
            Text(site.icon).fontSize(10)
            Text(site.name).fontSize(9)
              .fontColor(this.webUrl === site.url ? COLORS.white : COLORS.sub)
          }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
          .backgroundColor(this.webUrl === site.url ? COLORS.blue : COLORS.card)
          .borderRadius(12)
          .onClick(() => {
            this.urlInput = site.url;
            this.webUrl = site.url;
          })
        }, (site: QuickSite) => site.url)
      }
    }.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')

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

    // 主动下载按钮区
    Column({ space: 8 }) {
      Row({ space: 10 }) {
        Text('下载校报合订本 PDF').fontSize(10).fontColor(COLORS.white)
          .layoutWeight(1).textAlign(TextAlign.Center)
          .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.blue).borderRadius(9)
          .onClick(() => {
            this.triggerDownload('https://news.pku.edu.cn/download/campus_paper_2026summer.pdf?from=app');
          })
        Text('下载讲座回放 MP4').fontSize(10).fontColor(COLORS.blue)
          .layoutWeight(1).textAlign(TextAlign.Center)
          .padding({ top: 9, bottom: 9 })
          .borderRadius(9).border({ width: 1, color: COLORS.blue })
          .onClick(() => {
            this.triggerDownload('https://media.tsinghua.edu.cn/lecture/2026/ai_chip_full.mp4?track=campus');
          })
      }.width('100%')
    }.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(12)
  }.width('100%').height('100%').padding({ left: 14, right: 14, top: 10, bottom: 8 })
}

网页 Tab 是特性 B 的主阵地。地址栏采用 urlInput/webUrl 双状态分离设计——用户输入时只更新 urlInput,点击"前往"调用 loadUrl() 方法,该方法自动补全 https:// 协议前缀后将值同步到 webUrlWeb 组件才实际加载。快捷站点横滑区高亮当前已加载站点(webUrl === site.url 时使用青春蓝底白字),点击站点直接同步 urlInputwebUrl,一步加载。

Web 组件本体使用 webController 控制器管理,下载代理已在 aboutToAppear 中通过 setDownloadDelegate 绑定。用户在网页内点击下载链接时,下载事件自动进入代理的四回调链。底部提供两个主动下载按钮——"下载校报合订本 PDF"使用青春蓝填充样式,"下载讲座回放 MP4"使用青春蓝描边样式,两种视觉权重区分主次操作。点击按钮调用 triggerDownload(url),通过 webController.startDownload(url) 主动发起下载,无需依赖网页内的下载链接。

11.2 下载代理四回调链

setupDownloadDelegate() {
  // 下载开始前
  this.downloadDelegate.onBeforeDownload((item: webview.WebDownloadItem) => {
    const hostCtx = this.getUIContext().getHostContext();
    const dir = hostCtx ? hostCtx.filesDir : '';
    this.dlName = item.getSuggestedFileName();
    this.dlPercent = 0;
    this.dlState = '已开始';
    item.start(dir + '/' + item.getSuggestedFileName());
  });
  // 下载进行中
  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;
  });
  // 下载完成:双 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;
  });
  try {
    this.webController.setDownloadDelegate(this.downloadDelegate);
  } catch (error) {
    console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`);
  }
}

下载代理的四回调链是特性 B 的核心实现。onBeforeDownload 在下载任务创建后、文件写入前触发——此时必须调用 item.start(dir + '/' + item.getSuggestedFileName()) 提供沙箱存储路径,否则任务永远停留在 PENDING 状态。沙箱路径通过 getUIContext().getHostContext().filesDir 获取,确保文件保存到应用私有目录。文件名通过 getSuggestedFileName() 从下载响应头中提取。

onDownloadUpdated 在下载过程中持续触发,通过 getPercentComplete() 更新进度条和百分比文案。onDownloadFailed 在下载异常时触发,通过 getGuid() 获取任务唯一标识用于错误追踪。onDownloadFinish 是 6.1.1 新特性的核心——getOriginalUrl() 返回文件直链地址(如 https://news.pku.edu.cn/download/campus_paper.pdf),getReferrerUrl() 返回触发下载的引用页地址(如 https://news.pku.edu.cn/paper/list),这两个 URL 组合还原了完整的下载来路。getTotalBytes() 返回文件字节数,除以 1048576 转换为 MB 单位。新记录通过 unshift 置顶插入 downloadRecords 数组,用户切换到下载 Tab 即可看到最新记录。

setDownloadDelegate 的绑定包裹在 try-catch 中,捕获可能的 BusinessError 并打印错误码和消息,消除运行时抛错告警。

11.3 下载记录卡

@Builder
recordCard(item: DownloadRecord) {
  Column({ space: 6 }) {
    Row({ space: 8 }) {
      Text('📄 ' + item.fileName).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        .layoutWeight(1).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      Text(item.finishTime).fontSize(8).fontColor(COLORS.text3)
    }.width('100%')
    Row({ space: 6 }) {
      Text(item.fileSize).fontSize(8).fontColor(COLORS.sub)
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
        .backgroundColor(COLORS.chip).borderRadius(7)
      Text('校园官方渠道').fontSize(8).fontColor(COLORS.green)
    }.width('100%')
    // 原始 URL 行
    Row({ space: 4 }) {
      Text('🔗').fontSize(9)
      Text(item.originalUrl).fontSize(8).fontColor(COLORS.blueD)
        .fontFamily('monospace').layoutWeight(1)
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
    }.width('100%')
    // 引用页 URL 行
    Row({ space: 4 }) {
      Text('📄').fontSize(9)
      Text(item.referrerUrl).fontSize(8).fontColor(COLORS.sub)
        .fontFamily('monospace').layoutWeight(1)
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
    }.width('100%')
  }.width('100%').padding(10)
  .backgroundColor(COLORS.chip).borderRadius(10)
}

下载记录卡是双 URL 溯源信息的可视化载体。卡片分四行:文件名与完成时间行、文件大小与来源标签行、原始 URL 行、引用页 URL 行。原始 URL 行使用 🔗 图标和深青春蓝(blueD)等宽字体,引用页 URL 行使用 📄 图标和青灰蓝等宽字体,两种 URL 的图标和颜色差异帮助用户快速区分"文件从哪来"和"从哪个页面点的"。URL 使用 monospace 等宽字体显示并单行截断,保持技术信息的可读性。

十二、听报 Tab 与 AI 字幕深度分析

12.1 AICaptionComponent 实时预览

AICaptionComponent({
  isShown: this.captionShown,
  controller: this.captionController,
  options: this.buildCaptionOptions()
}).width('100%').height(110).borderRadius(10)

AI 字幕组件是特性 A 的核心载体。isShown 参数为 @Link 双向绑定,父组件直接传入 @State captionShown 引用,子组件内部修改会同步回父组件。controllerAICaptionController 实例,用于调用 writeAudio 写入音频数据。options 通过 buildCaptionOptions() 方法动态组装,每次渲染时重新构建配置对象。

12.2 AICaptionOptions 组装

buildCaptionOptions(): AICaptionOptions {
  const opts: AICaptionOptions = {
    initialOpacity: 1,
    sourceLanguage: this.srcLang,
    targetLanguage: this.tgtLang,
    fontSize: this.captionSize,
    fontColor: this.captionColor,
    onPrepared: () => {
      this.captionReady = true;
      this.captionErrMsg = '';
    },
    onError: (error: BusinessError) => {
      this.captionErrMsg = '字幕服务异常 ' + error.code + ':' + error.message;
    }
  };
  return opts;
}

buildCaptionOptions 方法体现了 6.1.1 新增的四字段配置能力。sourceLanguage 指定音频源语言('zh''en'),targetLanguage 指定字幕目标语言——中文源时锁定 'zh'(取值范围仅 ['zh'],选其他值初始化失败),英文源时可选 'zh'(译成中文)、'en'(英文原样)或 'zh-en'(中英双语)。fontSizeAICaptionFontSize 枚举四档(SMALL/NORMAL/BIG/LARGE),非 number 类型确保类型安全。fontColor 类型为 ResourceColor,字符串形式的 '#RRGGBB' 即可直接传入。

onPrepared 回调在字幕服务初始化完成后触发,将 captionReady 置为 true 并清空错误信息。onError 回调在服务异常时触发,将错误码和消息拼接为用户可读的提示文案。这两个回调确保用户始终能看到字幕服务的当前状态。

12.3 语言联动与音频写入

switchSourceLang(code: string) {
  this.srcLang = code;
  if (code === 'zh') {
    this.tgtLang = 'zh';
  } else {
    this.tgtLang = 'zh-en';
  }
}

feedAudioStream() {
  const block = new Uint8Array(640);
  for (let i = 0; i < 640; i += 2) {
    const t = (i / 2) / 16000;
    const v = Math.round(Math.sin(2 * Math.PI * 440 * t) * 6000);
    block[i] = v & 0xFF;
    block[i + 1] = (v >> 8) & 0xFF;
  }
  try {
    const audioData: AudioData = { data: block };
    this.captionController.writeAudio(audioData);
    this.captionFed++;
  } catch (e) {
    this.captionErrMsg = '音频写入失败';
  }
}

switchSourceLang 实现源语言与目标语言的联动逻辑——切换到中文源时目标语言自动锁定 'zh'(无翻译方向可选),切换到英文源时默认设为 'zh-en'(中英双语),用户可再手动切换为 'zh''en'。这一联动避免了中文源时目标语言设为非 'zh' 导致的初始化失败。

feedAudioStream 生成 640 字节的 PCM 音频块模拟真实音频输入。640 字节在 16kHz 采样率、16bit 位深、单声道的 PCM 格式下约等于 20ms 的音频数据。通过正弦函数生成 440Hz(标准音 A4)的测试信号,将振幅值以小端序写入 Uint8ArraywriteAudio 调用包裹在 try-catch 中,防止字幕服务未就绪时抛出异常。每次写入成功后 captionFed 计数器递增,UI 上显示"已写 N 块"。

12.4 字幕设置五区块面板

听报 Tab 的设置面板由五个区块组成:实时预览卡、语言设置卡、字号四档卡、五色预设卡和场景推荐卡。

语言设置卡通过条件渲染实现联动——当 srcLang === 'zh' 时目标语言区域显示"中文(锁定)"只读标签和提示文案;当 srcLang === 'en' 时渲染 TGT_LANGS_EN 的三选 chips(中文/英文/中英双语)。这种条件渲染确保用户不会在中文源时误选无效的目标语言。

字号四档卡将 SIZE_OPTIONS 的四档枚举渲染为等宽 chips,选中态使用青春蓝底白字,未选中使用浅湖蓝底青灰蓝字。五色预设卡使用 Stack 容器叠放圆形色块和选中勾标(✓),点击色块更新 captionColor 状态。场景推荐卡列表化展示 5 个 CaptionScene,每条卡片右侧的语言组合标签在当前配置匹配时变为青葱绿,提供"已应用"的视觉反馈。

十三、我的 Tab 分析

@Builder
tabMine() {
  Scroll() {
    Column({ space: 12 }) {
      // 订阅身份渐变大卡
      Column({ space: 12 }) {
        Row({ space: 12 }) {
          Text('👨‍🎓').fontSize(34)
          Column({ space: 4 }) {
            Text('林晓 · 新闻学院 2024 级').fontSize(16).fontWeight(FontWeight.Bold)
              .fontColor(COLORS.white)
            Text('校闻在线金牌读者 · 已订阅 12 个频道').fontSize(10).fontColor(COLORS.whiteSoft)
          }.layoutWeight(1).alignItems(HorizontalAlign.Start)
        }.width('100%')
        Row({ space: 8 }) {
          this.idStat('连续读报', '68 天')
          this.idStat('累计阅读', '1284 篇')
          this.idStat('收藏内容', '36 篇')
        }.width('100%')
      }.width('100%').padding(16).borderRadius(14)
      .linearGradient({ angle: 135, colors: [[COLORS.gradA, 0.0], [COLORS.gradB, 1.0]] })

      // 订阅频道 chips
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(SUB_CHIPS, (chip: string) => {
          Text(chip).fontSize(10).fontColor(chip === '要闻' ? COLORS.white : COLORS.sub)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(chip === '要闻' ? COLORS.blue : COLORS.chip)
            .borderRadius(11).margin({ right: 8, bottom: 8 })
        }, (chip: string) => 'sub-' + chip)
      }.width('100%')

      // 收藏清单
      ForEach(FAV_ROWS, (row: FavRow, idx: number) => {
        Row({ space: 10 }) {
          Text(row.icon).fontSize(16)
          Column({ space: 2 }) {
            Text(row.label).fontSize(12).fontColor(COLORS.title)
            Text('收藏于下载 Tab · 大小 ' + row.hint).fontSize(9).fontColor(COLORS.text3)
          }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          Text('溯源').fontSize(9).fontColor(COLORS.blue)
            .onClick(() => { this.currentTab = 3; })
        }.width('100%').padding({ top: 10, bottom: 10 })
        .backgroundColor(COLORS.card).borderRadius(10)
      }, (row: FavRow, idx: number) => 'fav-' + idx.toString())
    }.padding({ left: 14, right: 14, top: 12, bottom: 12 })
  }
}

我的 Tab 以订阅身份渐变大卡为视觉核心,使用 135° 渐变从青春蓝到深青春蓝,与头部横幅的 160° 渐变形成角度差异,避免视觉重复。身份卡内嵌用户头像 emoji、姓名院系和读者等级,底部三格统计(连续读报、累计阅读、收藏内容)通过 idStat Builder 渲染,文字使用纯白和半透白确保渐变背景上的可读性。

订阅频道 chips 使用 FlexFlexWrap.Wrap 换行布局,"要闻"频道高亮为青春蓝底白字,其余为浅湖蓝底青灰蓝字。收藏清单的每一行右侧提供"溯源"按钮,点击后设置 currentTab = 3 跳转到下载 Tab,实现跨 Tab 导航——收藏的内容与下载溯源记录一一呼应,用户可以从收藏列表直接跳转到下载溯源查看文件来路。

十四、底部 Tab 栏

@Builder
tabBar() {
  Row() {
    ForEach(TAB_LIST, (tab: TabMeta, index: number) => {
      Column({ space: 3 }) {
        Text(tab.icon).fontSize(17)
          .opacity(this.currentTab === index && this.breath ? 1 : 0.78)
        Text(tab.label).fontSize(9)
          .fontColor(this.currentTab === index ? COLORS.tabOn : COLORS.text3)
      }.justifyContent(FlexAlign.Center)
      .layoutWeight(1)
      .padding({ top: 7, bottom: 7 })
      .onClick(() => { this.currentTab = index; })
    }, (tab: TabMeta) => tab.label)
  }.width('100%').backgroundColor(COLORS.card)
}

底部导航采用自绘单排 6 Tab 布局,纯白底色与浅云蓝白页面底色形成微妙的层次差异。选中 Tab 的图标在 breath 为 true 时透明度提升至 1.0,配合 breath 为 false 时的 0.78,形成每秒一次的微闪烁——这一效果与头部呼吸圆点同步,构成页面整体的"在线心跳"节律。选中标签使用青春蓝(tabOn),未选中使用雾蓝灰(text3),色彩对比足够清晰但不刺眼。layoutWeight(1) 确保六个 Tab 等分底部宽度。

十五、弹窗系统

15.1 弹窗遮罩层

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

弹窗遮罩层是三个弹窗共用的底层组件,使用墨蓝半透色(rgba(31,42,58,0.5))覆盖全屏。点击遮罩区域触发 onClose 回调关闭弹窗,符合用户"点击空白处取消"的交互直觉。遮罩层作为 Stack 的底层,弹窗内容卡片作为上层居中显示。

15.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.formTitle, placeholder: '如:校游泳队蝉联省市金牌' })
          .fontSize(11).fontColor(COLORS.title)
          .backgroundColor(COLORS.chip).borderRadius(8)
          .onChange((value: string) => { this.formTitle = value; })
      }.width('100%').alignItems(HorizontalAlign.Start)

      // 分类 chips
      Column({ space: 6 }) {
        Text('分类').fontSize(9).fontColor(COLORS.sub)
        Row({ space: 8 }) {
          ForEach(NEWS_CATS, (cat: string) => {
            Text(cat).fontSize(10).fontWeight(FontWeight.Bold)
              .fontColor(this.formCat === cat ? COLORS.white : COLORS.sub)
              .layoutWeight(1).textAlign(TextAlign.Center)
              .padding({ top: 6, bottom: 6 })
              .backgroundColor(this.formCat === cat ? COLORS.blue : COLORS.chip)
              .borderRadius(9)
              .onClick(() => { this.formCat = cat; })
          }, (cat: string) => 'form-cat-' + cat)
        }.width('100%')
      }.width('100%').alignItems(HorizontalAlign.Start)

      // 来源输入 + 操作按钮
      // ...
    }.width('82%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
  }.width('100%').height('100%').alignContent(Alignment.Center)
}

新增弹窗绑定 NewsItem 实体的三个核心字段:标题、分类和来源。标题和来源使用 TextInput 输入框,分类使用五选 chips(头条/学术/社团/体育/招聘),选中态使用青春蓝底白字。点击"发布"按钮调用 saveNews(),该方法校验标题和来源非空后,创建 NewsItemunshiftnewsList 头部,然后 slice() 触发数组引用变更使 @Observed 刷新 UI。点击"取消"或遮罩调用 onClose 关闭弹窗。

15.3 编辑要闻弹窗

编辑弹窗回填当前条目的来源和发布时间。标题以只读形式展示(浅湖蓝底),来源和时间使用 TextInput 可编辑。openEdit(idx) 方法在打开弹窗前将 editIdx 设为当前索引,并从 newsList[editIdx] 读取 sourcetime 回填到 editSourceeditTime 表单状态。点击"保存"调用 editNews(),该方法通过 editIdx 索引定位到 newsList 中的目标条目,更新其 sourcetime 字段后 slice() 触发刷新。

15.4 删除确认弹窗

@Builder
panelDel(onClose: () => void) {
  Stack() {
    this.modalOverlay(onClose)
    Column({ space: 12 }) {
      Text('删除要闻').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
      Text(this.delIdx >= 0 && this.delIdx < this.newsList.length
        ? `确认删除「${this.newsList[this.delIdx].title}」?删除后不可恢复。`
        : '索引无效,请返回重试。')
        .fontSize(11).fontColor(COLORS.sub)
        .maxLines(3).textOverflow({ overflow: TextOverflow.Ellipsis })

      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(COLORS.white).fontWeight(FontWeight.Bold)
          .layoutWeight(1).textAlign(TextAlign.Center)
          .padding({ top: 9, bottom: 9 }).backgroundColor(COLORS.red).borderRadius(9)
          .onClick(() => { this.delNews(); })
      }.width('100%')
    }.width('78%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
  }.width('100%').height('100%').alignContent(Alignment.Center)
}

删除确认弹窗使用活力红主按钮传递危险操作的视觉警示。弹窗展示待删除条目的标题(含索引有效性检查),"确认删除"按钮使用 COLORS.red 背景白字,与"取消"按钮的浅湖蓝底形成强烈的色彩对比。点击"确认删除"调用 delNews(),通过 splice(delIdx, 1) 从数组中移除目标条目后 slice() 刷新。三个弹窗的卡片宽度略有差异(新增 82%、编辑 82%、删除 78%),删除弹窗最窄以突出其"确认对话"的简约属性。

十六、功能模块对比表

功能模块核心特性关键 API/组件数据模型交互模式状态变量数
头条 Tab要闻横滑大卡 + 热榜 + 柱状图Scroll + ForEach + linearGradientNewsItem横滑浏览 + 点击编辑/删除5
频道 Tab双层 Tabs 嵌套滚动nestedScroll(TabsNestedScrollMode)InnerCard + SwipeLog双层横滑 + 模式切换4
网页 TabArkWeb 站点访问 + 主动下载Web + WebviewController + startDownload无(URL 状态)地址栏输入 + 快捷站点6
下载 Tab双 URL 溯源记录WebDownloadDelegate 四回调DownloadRecord进度查看 + 记录浏览5
听报 TabAI 字幕四新字段配置AICaptionComponent + writeAudioCaptionScene五区块面板 + 场景推荐8
我的 Tab订阅身份 + 收藏清单linearGradient + Flex + ForEachFavRow统计查看 + 跨 Tab 跳转0(纯常量)
弹窗系统新增/编辑/删除三态Stack + modalOverlay + 条件渲染NewsItem(共享)遮罩点击关闭7
头部横幅三特性状态胶囊linearGradient + Circle 呼吸无(读取全局状态)被动展示0
底部导航6 Tab 单排切换ForEach + layoutWeightTabMeta点击切换1

十七、总结与展望

本文深度解析了基于 HarmonyOS ArkUI 框架构建的"校闻在线"校园资讯阅读平台,该平台在单文件内叠加了 HarmonyOS 6.1.1 的三大前沿特性,展现了 ArkUI 声明式范式在复杂业务场景下的系统级编排能力。

架构层面,平台采用 @Entry @Component 根组件 + @Builder 方法群的扁平化架构,状态变量按功能分组集中声明在组件顶层,通过 currentTab 索引在 6 个 @Builder 方法间切换内容区。这种"单组件多 Builder"模式避免了组件间状态传递的复杂性,同时保持了代码的结构化。Stack 容器层叠底层内容区和顶层弹窗系统,三个弹窗各自条件渲染,共享 modalOverlay 遮罩组件,形成统一的弹窗交互范式。

特性 A(AI 字幕) 的亮点在于 sourceLanguagetargetLanguage 的联动约束——中文源时目标语言锁定 'zh',英文源时三选自由。switchSourceLang() 方法将这一约束编码为函数逻辑,配合条件渲染的 UI 确保用户不会误选无效组合。writeAudio 的 640 字节 PCM 块写入模拟了真实音频流的分块处理流程,onPrepared/onError 回调提供了完整的服务状态感知。五色字体预设和四档字号枚举为无障碍场景提供了充分的可定制性。

特性 B(ArkWeb 下载双 URL 溯源) 的核心价值在于 getOriginalUrl + getReferrerUrl 双接口还原下载来路。WebDownloadDelegate 的四回调链覆盖了下载的完整生命周期,onBeforeDownload 中必须调用 item.start() 提供沙箱路径否则任务停滞在 PENDING 状态这一细节,是实际开发中容易踩的坑。startDownload 支持应用侧主动发起下载,使下载能力不依赖于网页内的用户点击。下载记录卡的等宽字体 URL 展示使技术信息可读性良好。

特性 C(Tabs 嵌套滚动) 通过 nestedScroll(TabsNestedScrollMode) 一行 API 实现了内层滑到边缘后外层接力翻页的流畅体验。SELF_FIRSTSELF_ONLY 两种模式的切换允许用户对比体验差异,SwipeLog 翻页日志提供了嵌套行为的可视化证据链。innerMockData 生成器保证每个栏目 8 条内容超一屏,这是 nestedScroll 演示的前提——只有内层可滚动才能触发边缘接力。

设计层面,青春蓝(#3B82F6)+ 活力红(#EF5350)的浅色主题搭配营造了清爽的校园氛围。ColorPalette 接口将 19 个颜色字段结构化集中管理,四色辅色系统与要闻分类一一映射,使用户通过色条即可快速辨识资讯类别。breath 呼吸动画以 1 秒为周期驱动柱状图 ±6% 波动、圆点透明度闪烁和 Tab 图标微闪烁,为静态页面注入了微妙的动态生命力。柱状图的 `bar_${i}_${this.breath}` 键值拼接技巧无需显式动画 API 即可实现周期性重渲染。

展望未来,该平台可在以下方向持续演进:一是引入 @Observed 的深层属性监听替代当前的 slice() 引用变更触发方式,进一步优化列表刷新性能;二是将 ColorPalette 扩展为深色/浅色双主题,利用 ArkUI 的 MediaQuery 实现跟随系统主题切换;三是将 AI 字幕的 writeAudio 替换为真实麦克风音频流采集,使听报场景从"演示模式"进入"实用模式";四是为下载代理增加断点续传和后台下载能力,提升大文件下载的用户体验;五是利用 ArkUI 的 Navigation 组件替代当前的 currentTab 索引切换,实现页面栈管理和转场动画。随着 HarmonyOS NEXT 的持续演进,ArkUI 声明式范式将在更多垂直行业场景中展现其"一次开发、多端部署"的独特价值。

附录: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、测试、元服务和应用上架分发等。

更多推荐