一、技术前言

在文博文旅与文化遗产保护领域,数字化探索正经历从"静态展示"到"沉浸交互"的深刻变革。从大雁塔的唐风楼阁到西安城墙的明代城垣,从碑林石刻的千年墨迹到大明宫遗址的宫殿残基,每一处古建遗产都承载着独特的历史信息、保护等级和地理坐标,需要匹配不同的呈现方式、交互深度和数据维度。传统文博应用面临三大挑战:地图标注仅支持静态点位导致长按事件无法捕获、百科浏览与资源下载割裂导致溯源链路断裂、图片元数据读写缺乏沙箱验证导致编解码链路不可见。
在这里插入图片描述

HarmonyOS ArkUI 框架为这些挑战提供了系统级解决方案。ArkUI 的声明式 UI 范式通过 @Component 封装可复用组件、@State 管理响应式状态、@Builder 拆分复杂 UI 结构,天然适合"地图-搜索-浏览-下载-工坊-个人"七层架构。@Observed 装饰器让数据模型字段级变化被 UI 感知,实现"事件触发即视图刷新"的流畅体验。ForEach 配合 layoutWeightFlex 弹性布局,使双列古迹卡、POI 列表和足迹清单的自适应排列简洁高效。

在这里插入图片描述

本平台深度融合 HarmonyOS 6.1.1 的三大前沿特性。Map Kit 提供地图组件初始化、Marker 群批量标注与双长按事件监听能力链——通过 MapComponent 组件渲染底图、mapCallback 回调装配控制器、getEventManager 获取事件管理器三步实现地图就绪;同时 onMarkerLongClickonPoiLongClick 两个 6.1.1 新特性接口实现标记长按与 POI 长按的双路事件捕获,记录类型、名称、经纬度和时间戳四维信息,配合 offMarkerLongClick / offPoiLongClick 实现监听动态开关。ArkWebWebDownloadDelegate 引入了双 URL 溯源机制——通过 onBeforeDownload 提供沙箱路径并调用 start()onDownloadUpdated 刷新进度条、onDownloadFailed 兜底失败、onDownloadFinish 读取 getOriginalUrl()getReferrerUrl() 双溯源字段,配合 startDownload 实现应用侧主动下载。Image KitImageSource 引入了类型化元数据读写——通过 readImageMetadataByType + WEBP_METADATA 读取五字段快照(canvasWidth/canvasHeight/delayTime/unclampedDelayTime/loopCount),通过 writeImageMetadata 字面量构造 WebPMetadata 写回,写后立即重建 ImageSource 回读校验,形成"生成-读取-写入-回读"全链路闭环。

在这里插入图片描述

二、整体架构流程图

Page1216 主组件

headerMain 头部品牌行

内容区 7 Tab 切换

tabBar 底部导航

弹窗系统 add/edit/del

Tab0 首页
统计格+双列古迹卡+月度柱状图

Tab1 地图
双长按Toggle+MapComponent+事件日志流

Tab2 搜索
搜索框+reliability分数结果列表

Tab3 百科
地址栏+快捷站点+Web组件+主动下载

Tab4 下载
进度任务卡+双URL溯源列表

Tab5 工坊
像素画生成+元数据四区块

Tab6 我的
渐变大卡+足迹清单+说明行

Map Kit
双长按事件监听

Map Kit
searchByText可靠性分数

ArkWeb
WebDownloadDelegate四回调

ArkWeb
双URL溯源字段

Image Kit
WebP元数据读写回读

panelAdd 收录新古迹

panelEdit 修订古迹档案

panelDel 移出名录确认

架构以 Page1216 为根组件,使用 Column 容器纵向排列:顶部 headerMain 品牌头行、分割线、内容区和底部 Tab 栏。内容区根据 currentTab 值在 7 个 Builder 方法间切换,其中地图(Tab1)和百科(Tab3)因需要全屏高度而不套 Scroll,其余 5 个 Tab 走 Scroll 分支支持内容滚动。三大特性分散在地图(Map Kit 双长按)、搜索(Map Kit POI 搜索)、百科与下载(ArkWeb 下载代理)和工坊(Image Kit WebP 元数据)四个 Tab 上,状态变量统一声明在组件顶层实现跨 Tab 共享。弹窗系统三个独立面板(panelAdd / panelEdit / panelDel)各自条件渲染,通过 Stack 层叠在内容区之上。

在这里插入图片描述

三、色彩体系设计

3.1 ColorPalette 接口定义

interface ColorPalette {
  bg: string;      // 页面背景(深褐)
  card: string;    // 卡片底色(深木)
  title: string;   // 标题(宣纸暖白)
  sub: string;     // 副文(绢帛黄)
  text3: string;   // 三级弱文(陶土灰)
  bronze: string;  // 青铜绿主色
  bronzeD: string; // 青铜深色
  gold: string;    // 鎏金
  red: string;     // 朱砂红(国保徽章/删除)
  blue: string;    // 青蓝
  line: string;    // 分割线
  tabOn: string;   // Tab 选中色
  mask: string;    // 弹窗遮罩
}

色彩体系以"青铜绿 + 鎏金"为核心对比,取意于青铜器的氧化锈绿与宫廷漆器的鎏金装饰。接口定义了 13 个语义化色彩字段,从页面背景到弹窗遮罩全覆盖,确保每一个视觉元素都有明确的色彩归属。

在这里插入图片描述

3.2 COLORS 常量逐色分析

const COLORS: ColorPalette = {
  bg: '#1B150F',      // 深褐墨色,模拟夜晚博物馆展厅的沉浸式暗光环境
  card: '#282017',    // 深木色卡片底,比背景亮一档暖褐
  dark: '#332A1E',    // 次级容器底色(徽章底/统计格/选中态底)
  title: '#F5EDDE',   // 宣纸暖白标题,暗光高对比阅读
  sub: '#C8B99C',     // 绢帛黄副文,层次柔和
  text3: '#8F8266',   // 陶土灰弱文本,辅助信息不抢视觉
  bronze: '#5E9C8B',  // 青铜绿主色,按钮与进度条
  bronzeD: '#46786A', // 青铜深色,渐变起点与偶数柱
  gold: '#D9A441',    // 鎏金强调色,收录数/分数/选中态
  red: '#C0392B',     // 朱砂红,国保徽章与删除操作
  blue: '#6B8FBF',    // 青蓝,POI 命中标识
  line: '#3A3122',    // 深褐分割线,低对比不干扰
  tabOn: '#D9A441',   // Tab 选中色为鎏金(非主色青铜绿)
  mask: 'rgba(0,0,0,0.6)' // 半透黑弹窗遮罩
};

色彩体系以"青铜锈绿 + 鎏金"为核心对比。绿色取自青铜器千年的氧化锈层,金色取自宫廷器物的鎏金点缀。值得注意的是 Tab 选中色使用 gold(鎏金)而非 bronze(青铜绿),这是因为金色在深褐背景上对比度更高,用户视觉定位更迅速。我的 Tab 的渐变大卡使用 linearGradientbronzeDdark 的 135° 渐变,模拟青铜器从深色锈层到暗木底座的过渡。柱状图偶数月使用 bronze(青铜绿)、奇数月使用 bronzeD(青铜深色),形成双色交替的节奏感。朱砂红 red 专门用于"全国重点文物保护单位"等级徽章和删除操作,取意于传统碑刻朱批。
在这里插入图片描述

四、Tab元数据与辅助数据

4.1 底部导航 Tab 定义

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

const TAB_LIST: TabMeta[] = [
  { icon: '🏛️', label: '首页' },
  { icon: '🗺️', label: '地图' },
  { icon: '🔍', label: '搜索' },
  { icon: '🌐', label: '百科' },
  { icon: '📥', label: '下载' },
  { icon: '🧪', label: '工坊' },
  { icon: '👤', label: '我的' }
];

7 个 Tab 从首页到个人中心覆盖文博探索全流程:首页是名录入口与探访热度概览,地图提供 Map Kit 标注与长按事件,搜索实现 POI 可靠性查询,百科支持 ArkWeb 浏览与主动下载,下载展示双 URL 溯源记录,工坊是 Image Kit WebP 元数据沙箱,我的展示探访者档案与足迹清单。底部 Tab 栏单排 7 格,每格等宽 layoutWeight(1) 均分,选中态用鎏金色 tabOn 高亮文本。

4.2 地图标注点与城市中心

const CITY_CENTER: mapCommon.LatLng = { latitude: 34.3416, longitude: 108.9398 };

interface SpotItem {
  name: string;
  lat: number;
  lng: number;
  tag: string;
}

const MARKER_SPOTS: SpotItem[] = [
  { name: '大雁塔', lat: 34.2185, lng: 108.9640, tag: '唐 · 楼阁式砖塔' },
  { name: '小雁塔', lat: 34.2411, lng: 108.9434, tag: '唐 · 密檐式砖塔' },
  { name: '西安城墙', lat: 34.2569, lng: 108.9424, tag: '明 · 城垣遗存' },
  { name: '碑林博物馆', lat: 34.2555, lng: 108.9313, tag: '唐 · 石刻碑林' },
  { name: '大明宫遗址', lat: 34.2763, lng: 108.9420, tag: '唐 · 宫殿遗址' },
  { name: '兴教寺塔', lat: 34.1166, lng: 108.9570, tag: '唐 · 玄奘墓塔' }
];

城市中心点定于西安钟楼一带(34.3416°N, 108.9398°E),Map Kit 初始 zoom=12。6 处长安古迹标注点覆盖唐、明两代:大雁塔(楼阁式砖塔)与小雁塔(密檐式砖塔)并称唐代双塔,西安城墙是明代城垣遗存,碑林博物馆珍藏唐代石刻,大明宫遗址再现唐代宫殿,兴教寺塔是玄奘法师墓塔。这组数据同时用于地图 Marker 群和我的 Tab 足迹清单,实现数据复用。

4.3 百科快捷站点与资源下载 URL

const QUICK_SITES: string[] = [
  'https://www.ncha.gov.cn',
  'https://www.dpm.org.cn',
  'https://www.sxhm.com',
  'https://www.kaogu.cn'
];

const RESOURCE_DL_URL: string = 'https://www.ncha.gov.cn/upload/file/gujianzhu-atlas-2026.pdf?zone=north';

快捷站点 4 个,均为文博类真实站点:国家文物局(ncha.gov.cn)、故宫博物院(dpm.org.cn)、陕西历史博物馆(sxhm.com)、中国考古网(kaogu.cn)。用户点击快捷站点标签直接跳转,省去地址栏输入。RESOURCE_DL_URL 是百科 Tab "触发下载"按钮的预设资源 URL,指向国家文物局的古建图册 PDF,带 zone=north 查询参数模拟区域分片资源。

4.4 WebP 编码参数与纹理预设

const WEBP_QUALITY: number = 90;
const CANVAS_SIZE: number = 96;
const DELAY_PRESETS: number[] = [120, 200, 500];
const LOOP_PRESETS: number[] = [0, 1, 3, 5];

interface TextureItem {
  key: string;
  label: string;
  note: string;
}

const TEXTURE_LIST: TextureItem[] = [
  { key: 'mottle', label: '斑驳', note: '对角斜纹' },
  { key: 'carve', label: '雕花', note: '棋盘格' },
  { key: 'rammed', label: '夯土', note: '横带层理' },
  { key: 'stele', label: '碑刻', note: '竖带刻痕' },
  { key: 'ring', label: '年轮', note: '同心环' }
];

WebP 编码质量 90(高保真),画布边长 96px(轻量级样图)。帧延迟三档预设 120/200/500ms,均在 API 合法区间 [100, 65535] 内;循环次数四档预设 0(不限)/1/3/5。五种老照片纹理模拟古建表面质感:斑驳用对角斜纹模拟风化剥落,雕花用棋盘格模拟木构装饰,夯土用横带层理模拟土筑墙体,碑刻用竖带刻痕模拟石刻纹路,年轮用同心环模拟木材截面。

4.5 月度探访量与保护等级选项

const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['03月', '04月', '05月', '06月', '07月', '08月'];
const VISIT_VAL: number[] = [126, 98, 154, 183, 216, 172];

const LEVEL_OPTIONS: string[] = ['全国重点文物保护单位', '省级文物保护单位', '市县级文物保护单位'];

近 6 个月探访量从 03 月的 126 次上升到 07 月峰值 216 次,08 月回落到 172 次,整体呈上升后微降趋势,反映暑期文博旺季特征。保护等级三档对应国家文保体系标准,LEVEL_OPTIONS 作为弹窗面板归一化后的标准取值。

4.6 我的 Tab 说明行

interface AboutRow {
  icon: string;
  title: string;
  note: string;
}

const ABOUT_ROWS: AboutRow[] = [
  { icon: '🗺️', title: '地图数据', note: 'Map Kit · 需 INTERNET 权限与签名' },
  { icon: '🌐', title: '百科与下载', note: 'ArkWeb · 双 URL 溯源 since 24' },
  { icon: '🧪', title: '工坊沙箱', note: 'Image Kit · WebP 元数据读写' },
  { icon: '📌', title: '当前版本', note: 'HarmonyOS 6.1.1 · API 24' }
];

说明行 4 条,前三条对应三大特性声明,第四条标注运行环境版本。这些说明行让用户在"我的"Tab 一目了然地了解平台的技术栈构成。

五、工具函数

5.1 相关性分数配色

interface ScoreInfo {
  label: string;
  color: string;
}

function reliabilityScore(v: number): ScoreInfo {
  if (v >= 0.8) {
    return { label: '高相关', color: COLORS.gold };
  }
  if (v >= 0.5) {
    return { label: '中相关', color: COLORS.bronze };
  }
  return { label: '低相关', color: COLORS.text3 };
}

reliabilityScore 函数将 0~1 的相关性分数映射为三档标签与颜色:0.8 以上为高相关(鎏金色),0.5 以上为中相关(青铜绿色),其余为低相关(陶土灰色)。该函数用于搜索 Tab 的 POI 结果卡,让用户直观判断搜索结果与查询关键词的匹配程度。

5.2 保护等级配色

function levelColor(level: string): string {
  if (level === '全国重点文物保护单位') {
    return COLORS.red;
  }
  if (level === '省级文物保护单位') {
    return COLORS.bronze;
  }
  return COLORS.text3;
}

levelColor 函数将三档保护等级映射为三色:全国重点用朱砂红(最高规格),省级用青铜绿(中等规格),市县级用陶土灰(基础规格)。该函数用于古迹卡的等级徽章染色,让保护级别一目了然。

5.3 颜色格式转换

function hexToRgba(hex: string): number {
  const r: number = parseInt(hex.slice(1, 3), 16);
  const g: number = parseInt(hex.slice(3, 5), 16);
  const b: number = parseInt(hex.slice(5, 7), 16);
  return 0xFF000000 | (b << 16) | (g << 8) | r;
}

hexToRgba 函数将 #RRGGBB 格式颜色转为 0xFFBBGGRR 数值。RGBA_8888 缓冲区按字节序排列 R、G、B、A 四个分量,Alpha 固定 255(不透明)。位移操作 (b << 16) | (g << 8) | r 将 B 放到高位、G 放到中位、R 放到低位,再与 0xFF000000 做或运算注入 Alpha 分量。该函数用于工坊 Tab 的像素画生成,将主题色转为像素值写入 ArrayBuffer。

5.4 元数据格式化与体积转换

function fmtField(v: number, unit: string): string {
  return v < 0 ? '未提供' : `${v}${unit}`;
}

function formatSize(bytes: number): string {
  if (bytes >= 1048576) {
    return `${(bytes / 1048576).toFixed(1)}MB`;
  }
  if (bytes >= 1024) {
    return `${(bytes / 1024).toFixed(1)}KB`;
  }
  return `${bytes}B`;
}

fmtField 将 -1(undefined 的占位值)格式化为"未提供",避免直接显示负数。formatSize 将字节数按量级转换为可读体积文本:1MB 以上用兆字节、1KB 以上用千字节、以下直接用字节。两个函数分别用于 WebP 元数据快照卡和下载记录卡的字段渲染。

5.5 域名提取与时间格式化

function hostOf(url: string): string {
  const rest: string = url.replace('https://', '').replace('http://', '');
  return rest.split('/')[0];
}

function nowTime(): string {
  const d: Date = new Date();
  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
}

hostOf 从完整 URL 提取域名部分,用于百科 Tab 的快捷站点标签——显示 “ncha.gov.cn” 而非完整 URL,节省空间。nowTime 返回当前时间的 HH:mm:ss 格式字符串,用 padStart(2, '0') 补零保证两位数显示,供事件日志、下载记录和操作日志共用时间戳。

六、数据模型层

6.1 HeritageItem 古迹名录模型

@Observed export class HeritageItem {
  name: string;
  dynasty: string;
  level: string;
  region: string;
  score: number;

  constructor(name: string, dynasty: string, level: string, region: string, score: number) {
    this.name = name;
    this.dynasty = dynasty;
    this.level = level;
    this.region = region;
    this.score = score;
  }
}

HeritageItem 是古迹名录的核心实体,5 个字段:名称、朝代、保护等级、区域、文保分。@Observed 装饰器使字段变化被 UI 感知,编辑弹窗修改后名录即时刷新。初始注入 8 条长安古迹数据,覆盖唐、西汉、明、清四个朝代,保护等级从全国重点到市县级,文保分从 61 到 98。弹窗系统的增删改均绑定此实体。

6.2 SearchRecord POI 搜索结果模型

@Observed export class SearchRecord {
  name: string;
  address: string;
  distance: number;
  reliability: number;

  constructor(name: string, address: string, distance: number, reliability: number) {
    this.name = name;
    this.address = address;
    this.distance = distance;
    this.reliability = reliability;
  }
}

SearchRecord 是 POI 搜索结果实体,4 个字段:名称、地址、直线距离、相关性分数。reliability 取值 [0,1],由 Map Kit searchByText 返回的 site.Site.reliability 字段提供。初始注入 7 条 Mock 数据,相关性从 0.97(高相关)到 0.18(低相关),覆盖三档分级。搜索失败时保留 Mock 数据保证演示链路完整。

6.3 EventLog 长按事件日志模型

@Observed export class EventLog {
  type: string;
  name: string;
  lat: number;
  lng: number;
  time: string;

  constructor(type: string, name: string, lat: number, lng: number) {
    this.type = type;
    this.name = name;
    this.lat = lat;
    this.lng = lng;
    this.time = nowTime();
  }
}

EventLog 是长按事件日志实体,5 个字段:类型(Marker/POI/系统)、名称、纬度、经度、时间。构造函数自动调用 nowTime() 填充时间戳。Marker 长按事件记录 #id 和经纬度,POI 长按事件记录 POI 名称和经纬度,系统事件(监听开关)经纬度填 0。日志使用 unshift 插入数组头部,实现最新事件置顶显示。

6.4 DownloadRecord 下载记录模型

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

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

DownloadRecord 是下载记录实体,5 个字段:文件名、字节数、完成时间、原始 URL、引用页 URL。后两个字段是 6.1.1 双溯源特性——originalUrl 来自 getOriginalUrl() 方法(文件的真实下载地址),referrerUrl 来自 getReferrerUrl() 方法(触发下载的网页地址)。初始注入 5 条 Mock 数据,覆盖 PDF、ZIP、XLSX、PNG 四种文件类型,来自四个文博站点。

6.5 WebpMetaSnapshot 元数据快照模型

@Observed export class WebpMetaSnapshot {
  canvasWidth: number;
  canvasHeight: number;
  delayTime: number;
  unclampedDelayTime: number;
  loopCount: number;

  constructor(w: number, h: number, d: number, u: number, l: number) {
    this.canvasWidth = w;
    this.canvasHeight = h;
    this.delayTime = d;
    this.unclampedDelayTime = u;
    this.loopCount = l;
  }
}

WebpMetaSnapshot 是 WebP 五字段快照实体,对应 image.WebPMetadata 的五个字段:画布宽、画布高、钳制后帧延迟、未钳制帧延迟、循环次数。undefined 值用 -1 占位,渲染时由 fmtField 转为"未提供"。读取快照和回读快照共用此模型,通过 highlight 参数区分卡片底色。

6.6 MetaOpLog 元数据操作日志模型

@Observed export class MetaOpLog {
  op: string;
  detail: string;
  time: string;

  constructor(op: string, detail: string) {
    this.op = op;
    this.detail = detail;
    this.time = nowTime();
  }
}

MetaOpLog 是元数据操作日志实体,3 个字段:操作名(生成样图/读取元数据/写入元数据/回读校验)、详情、时间。工坊 Tab 的全链路操作均记录到此模型,使用 unshift 插入头部实现最新操作置顶,让用户清晰看到"生成→读取→写入→回读"的完整链路。

七、组件主体结构

7.1 @State 状态变量总览

组件 Page1216 的状态变量按功能分为六组:

Tab/弹窗状态currentTab(当前 Tab 索引)、addModal/editModal/delModal(三弹窗显隐)、editIdx/delIdx(编辑/删除目标索引)、breath/timer(呼吸动画与定时器)。

古迹业务数据heritageList(古迹名录)、inputName/inputDynasty/inputLevel/inputRegion(弹窗面板四个输入字段)。

Map Kit 状态mapOptions/mapCallback/mapController/mapEventManager(地图初始化四件套)、eventLogs(长按事件日志)、markerListenOn/poiListenOn(双监听开关)。

POI 搜索状态queryInput(搜索关键词)、searchRecords(结果列表)、searchState(搜索状态文案)。

ArkWeb 状态webController/downloadDelegate(Web 控制器与下载代理)、urlInput/webUrl(地址栏输入与实际加载 URL 双状态)、dlName/dlPercent/dlState(下载任务名/进度/状态)、downloadRecords(完成记录列表)。

WebP 元数据状态pixelMap(样图像素图)、webpPath(沙箱文件路径)、genState(生成状态)、textureSel(选中纹理)、metaSnapshot(读取快照)、writeDelay/writeLoop(写入参数)、verifySnapshot(回读快照)、opLogs(操作日志)。

7.2 生命周期

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

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

aboutToAppear 生命周期做三件事:启动 1 秒间隔的呼吸定时器(breath 在 true/false 间切换,联动统计格变色、柱状图柱高波动、我的 Tab 数值变色);调用 setupMapCallback 装配地图回调(Marker 群添加 + 双长按监听注册);调用 setupDownloadDelegate 绑定下载代理四回调到 webControlleraboutToDisappear 仅清理呼吸定时器,地图和 Web 的资源由组件框架自动回收。

7.3 build() 根构建

build() {
  Column() {
    this.headerMain()
    Divider().strokeWidth(1).color(COLORS.line)
    if (this.currentTab === 1) {
      this.tabMap()
    } else if (this.currentTab === 3) {
      this.tabWeb()
    } else {
      Scroll() {
        Column({ space: 12 }) {
          if (this.currentTab === 0) { this.tabHome() }
          else if (this.currentTab === 2) { this.tabSearch() }
          else if (this.currentTab === 4) { this.tabDownload() }
          else if (this.currentTab === 5) { this.tabStudio() }
          else if (this.currentTab === 6) { this.tabMine() }
        }.width('100%').padding({ left: 12, right: 12, top: 12, bottom: 12 })
      }.layoutWeight(1).width('100%').scrollBar(BarState.Off)
    }
    this.tabBar()
    if (this.addModal) { this.panelAdd(() => { this.addModal = false; }) }
    if (this.editModal) { this.panelEdit(() => { this.editModal = false; }) }
    if (this.delModal) { this.panelDel(() => { this.delModal = false; }) }
  }.backgroundColor(COLORS.bg).height('100%')
}

根构建使用 Column 纵向排列四层:头部、分割线、内容区、底部 Tab 栏。内容区的关键设计是"地图/百科全高不套 Scroll,其余走 Scroll 分支"——地图 Tab 的 MapComponent 和百科 Tab 的 Web 组件需要占据全屏高度并自行管理滚动,若外层再套 Scroll 会导致组件高度坍缩或手势冲突。其余 5 个 Tab 内容可能超屏,用 Scroll + layoutWeight(1) 保证可滚动。弹窗系统三个面板各自条件渲染在 Column 内部,通过 Stack 层叠在内容区之上。整个根容器背景色为 COLORS.bg(深褐墨色),高度 100%。

八、头部区域详解

@Builder
headerMain() {
  Row({ space: 10 }) {
    Text('🏛️').fontSize(26)
    Column({ space: 2 }) {
      Text('古建寻访').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Text('文化遗产探索地图 · 长安篇').fontSize(10).fontColor(COLORS.sub)
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    Text(`收录 ${this.heritageList.length}`)
      .fontSize(10).fontColor(COLORS.gold)
      .padding({ left: 8, right: 8, top: 4, bottom: 4 })
      .borderRadius(10).backgroundColor(COLORS.dark)
    Text('+')
      .fontSize(16).fontColor(COLORS.tabOn)
      .width(30).height(30).textAlign(TextAlign.Center)
      .borderRadius(15).backgroundColor(COLORS.bronze)
      .onClick(() => {
        this.clearInputs();
        this.addModal = true;
      })
  }
  .width('100%')
  .padding({ left: 14, right: 14, top: 10, bottom: 10 })
}

头部区域是一个横向 Row,包含四个元素:古建 emoji 图标(fontSize 26,作为品牌视觉锚点);品牌名与副标题的纵向 Column(“古建寻访” 用宣纸暖白粗体 18 号字,“文化遗产探索地图 · 长安篇” 用绢帛黄 10 号字,layoutWeight(1) 占据剩余空间,alignItems(Start) 左对齐);收录数量徽章(鎏金色文字 + 深木色圆角背景,动态显示 heritageList.length);快捷收录按钮(圆形 “+” 号,青铜绿背景 + 鎏金文字,点击清空输入并打开新增弹窗)。头部无动画,保持简洁高效的信息呈现。

九、首页 Tab(Tab0)详解

9.1 统计行

@Builder
statCell(icon: string, value: string, label: string) {
  Column({ space: 4 }) {
    Text(icon).fontSize(16)
    Text(value).fontSize(15).fontWeight(FontWeight.Bold)
      .fontColor(this.breath ? COLORS.gold : COLORS.sub)
    Text(label).fontSize(9).fontColor(COLORS.text3)
  }
  .layoutWeight(1)
  .padding({ top: 10, bottom: 10 })
  .borderRadius(10).backgroundColor(COLORS.card)
  .justifyContent(FlexAlign.Center)
}

statCell 是首页统计格的通用 Builder,三参数(图标、数值、标签),纵向排列。数值的 fontColor 联动 breath 状态:呼吸 true 时用鎏金色(高亮),false 时用绢帛黄(柔和),形成 1 秒间隔的金色闪烁效果。首页统计行三格:收录古迹数(动态)、地图标注数(6)、探索里程(128.6km)。每格 layoutWeight(1) 等宽,justifyContent(Center) 居中,深木色圆角卡片底色。

9.2 古迹双列卡

@Builder
heritageCard(item: HeritageItem, idx: number) {
  Column({ space: 6 }) {
    Row({ space: 6 }) {
      Text(item.dynasty).fontSize(10).fontColor(COLORS.gold)
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
        .borderRadius(6).backgroundColor(COLORS.dark)
      Text(item.level).fontSize(8).fontColor(levelColor(item.level))
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
        .borderRadius(6).backgroundColor(COLORS.dark)
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        .layoutWeight(1)
    }
    .width('100%')
    Text(item.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
    Text(item.region).fontSize(10).fontColor(COLORS.text3)
      .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
    Row({ space: 6 }) {
      Text(`文保分 ${item.score}`).fontSize(10).fontColor(COLORS.bronze).layoutWeight(1)
      Text('编').fontSize(9).fontColor(COLORS.sub)
        .width(22).height(22).textAlign(TextAlign.Center)
        .borderRadius(11).backgroundColor(COLORS.dark)
        .onClick(() => { this.openEdit(idx); })
      Text('删').fontSize(9).fontColor(COLORS.red)
        .width(22).height(22).textAlign(TextAlign.Center)
        .borderRadius(11).backgroundColor(COLORS.dark)
        .onClick(() => { this.delIdx = idx; this.delModal = true; })
    }
    .width('100%')
  }
  .width('49%')
  .padding(10).borderRadius(12).backgroundColor(COLORS.card)
  .alignItems(HorizontalAlign.Start)
}

古迹卡宽度 49%,配合 FlexSpaceBetween 布局实现双列排列。卡片纵向四层:朝代徽章(鎏金色)+ 等级徽章(levelColor 函数动态配色)的横向行,等级徽章 layoutWeight(1) 占满剩余并省略号截断;古迹名称(宣纸白粗体 14 号,省略号截断);区域信息(陶土灰 10 号,省略号截断);文保分(青铜绿色)+ "编"和"删"两个圆形操作按钮的横向行。"编"按钮调用 openEdit(idx) 回填字段并打开编辑弹窗,"删"按钮设置 delIdx 并打开删除弹窗。

9.3 首页整体布局

@Builder
tabHome() {
  Column({ space: 12 }) {
    Row({ space: 8 }) {
      this.statCell('🏛️', `${this.heritageList.length}`, '收录古迹')
      this.statCell('📍', `${MARKER_SPOTS.length}`, '地图标注')
      this.statCell('🧭', '128.6', '探索里程')
    }
    .width('100%')
    Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
      ForEach(this.heritageList, (item: HeritageItem, idx: number) => {
        this.heritageCard(item, idx)
      }, (item: HeritageItem, idx: number) => `${item.name}_${idx}`)
    }
    .width('100%')
    this.chartCard()
  }
  .width('100%')
}

首页纵向三层:统计行(三格等宽);古迹卡 Flex 流式布局(wrap 允许换行,SpaceBetween 两端对齐,ForEach 遍历 heritageList 动态渲染,key 用 name_idx 保证唯一);月度柱状图卡。古迹数据增删后 ForEach 自动重渲染,呼吸动画联动统计格和柱状图。

十、地图 Tab(Tab1)详解

10.1 双长按监听开关

Row({ space: 12 }) {
  Toggle({ type: ToggleType.Switch, isOn: this.markerListenOn })
    .onChange(() => { this.toggleMarkerListen(); })
  Text('Marker长按').fontSize(11).fontColor(COLORS.sub).layoutWeight(1)
  Toggle({ type: ToggleType.Switch, isOn: this.poiListenOn })
    .onChange(() => { this.togglePoiListen(); })
  Text('POI长按').fontSize(11).fontColor(COLORS.sub)
}

地图 Tab 顶部是双 Toggle 开关行,分别控制 Marker 长按监听和 POI 长按监听的开启/关闭。ToggleType.Switch 使用开关样式,isOn 绑定 markerListenOn / poiListenOn 状态。onChange 回调调用对应的 toggle 方法,方法内部判断当前状态:开启时调用 offXxxLongClick() 清除订阅并记录系统日志,关闭时调用 onXxxLongClick() 重新注册并记录系统日志,最后翻转状态。

10.2 MapComponent 与事件日志

MapComponent({ mapOptions: this.mapOptions, mapCallback: this.mapCallback })
  .layoutWeight(1).width('100%').borderRadius(12)

MapComponent 接收 mapOptions(位置:城市中心 + zoom 12)和 mapCallback(异步回调函数)两个参数,layoutWeight(1) 占据中间区域全部剩余高度。回调函数内完成四步:err 判空兜底 → 获取 controller → 获取 eventManager → 批量添加 6 个古迹 Marker(addMarker 异步,逐个 await + try-catch)→ 注册双长按监听。

manager.onMarkerLongClick((marker: map.Marker) => {
  const pos: mapCommon.LatLng = marker.getPosition();
  this.eventLogs.unshift(new EventLog('Marker', `#${marker.getId()}`, pos.latitude, pos.longitude));
});
manager.onPoiLongClick((poi: mapCommon.Poi) => {
  this.eventLogs.unshift(new EventLog('POI', poi.name ?? '未命名POI',
    poi.position.latitude, poi.position.longitude));
});

Marker 长按监听回调接收 map.Marker 对象,通过 getId()getPosition() 获取标记 ID 和经纬度,构造 EventLog 插入日志头部。POI 长按监听回调接收 mapCommon.Poi 对象,仅暴露 idnameposition 三个字段,用 ?? '未命名POI' 兜底空名称。两个监听器是 6.1.1 新增的 Map Kit 事件接口。

10.3 事件日志流

List({ space: 6 }) {
  ForEach(this.eventLogs, (log: EventLog, idx: number) => {
    ListItem() {
      this.eventLogRow(log)
    }
  }, (log: EventLog, idx: number) => `${log.time}_${idx}_${log.type}`)
}
.width('100%').height(116)

事件日志流使用 List + ForEach 渲染,高度固定 116px(约 4 条日志可见)。每行用 eventLogRow Builder 渲染:类型徽标(Marker 用青铜绿、POI 用鎏金、系统用陶土灰)、名称(省略号截断)、经纬度(monospace 字体精确对齐)、时间。空日志时显示提示文本"暂无事件:长按地图 Marker 或 POI 试一试…"。

十一、搜索 Tab(Tab2)详解

11.1 搜索框与状态行

Row({ space: 8 }) {
  TextInput({ text: this.queryInput, placeholder: '输入关键字,如 古迹' })
    .layoutWeight(1).height(40).fontSize(12)
    .backgroundColor(COLORS.dark).fontColor(COLORS.title)
    .onChange((v: string) => { this.queryInput = v; })
  Button('搜索').height(36).fontSize(12).backgroundColor(COLORS.bronze)
    .onClick(() => { this.runSearch(); })
}

搜索框由 TextInput 和"搜索"按钮组成。TextInput 绑定 queryInput,初始值"古迹",onChange 实时更新状态。按钮点击调用 runSearch 异步方法。

async runSearch() {
  this.searchState = '搜索中…';
  const params: site.SearchByTextParams = {
    query: this.queryInput,
    location: CITY_CENTER,
    radius: 5000,
    language: 'zh'
  };
  try {
    const result: site.SearchByTextResult = await site.searchByText(params);
    const sites: site.Site[] = result.sites ?? [];
    if (sites.length === 0) {
      this.searchState = '无结果,已保留当前推荐';
      return;
    }
    this.searchRecords = sites.map((s: site.Site) => new SearchRecord(
      s.name ?? '未命名地点', s.formatAddress ?? '暂无地址',
      s.distance ?? 0, s.reliability ?? 0));
    this.searchState = `返回 ${sites.length} 条结果`;
  } catch (e) {
    const err = e as BusinessError;
    this.searchState = `搜索失败(${err.code}),保留当前推荐`;
  }
}

runSearch 方法封装了 Map Kit site.searchByText 调用。搜索参数:查询关键词、定位坐标(城市中心)、半径 5000 米、语言中文。searchByText 返回 SearchByTextResult,内含 sites 数组。每个 site.Site 提取 name、formatAddress、distance、reliability 四个字段构造 SearchRecord。无结果时保留当前推荐数据,异常时(无 AGC 配置/无网络)也保留 Mock 数据并提示错误码,保证演示链路完整。

11.2 POI 结果卡

@Builder
searchResultCard(rec: SearchRecord) {
  Column({ space: 6 }) {
    Row({ space: 8 }) {
      Text(rec.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        .layoutWeight(1)
      Text(reliabilityScore(rec.reliability).label).fontSize(10)
        .fontColor(reliabilityScore(rec.reliability).color)
        .padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .borderRadius(8).backgroundColor(COLORS.dark)
    }
    Text(rec.address).fontSize(11).fontColor(COLORS.sub)
      .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
    Row({ space: 8 }) {
      Text(`直线 ${rec.distance}m`).fontSize(10).fontColor(COLORS.text3)
      Text('POI 命中').fontSize(10).fontColor(COLORS.blue)
    }
    Row({ space: 8 }) {
      Progress({ value: rec.reliability * 100, total: 100, type: ProgressType.Linear })
        .layoutWeight(1).color(COLORS.bronze).backgroundColor(COLORS.dark)
      Text(`reliability ${rec.reliability.toFixed(2)}`).fontSize(10).fontColor(COLORS.gold)
    }
  }
  .width('100%').padding(12).borderRadius(10).backgroundColor(COLORS.card)
  .alignItems(HorizontalAlign.Start)
}

搜索结果卡纵向四层:名称 + 相关性等级标签(reliabilityScore 函数返回标签和颜色,鎏金/青铜绿/陶土灰三档);地址(省略号截断);直线距离(陶土灰)+ “POI 命中”(青蓝色);线性进度条(青铜绿色,值 = reliability * 100)+ reliability 数值(鎏金色,保留两位小数)。进度条直观展示相关性高低,配合文字标签双重传达匹配程度。

十二、百科 Tab(Tab3)详解

12.1 地址栏与快捷站点

Row({ space: 8 }) {
  TextInput({ text: this.urlInput, placeholder: '输入网址开始溯源' })
    .layoutWeight(1).height(38).fontSize(12)
    .backgroundColor(COLORS.dark).fontColor(COLORS.title)
    .onChange((v: string) => { this.urlInput = v; })
  Button('前往').height(34).fontSize(12).backgroundColor(COLORS.bronze)
    .onClick(() => { this.loadUrl(); })
}

地址栏由 TextInput 和"前往"按钮组成。urlInput 是地址栏输入值,webUrl 是实际加载到 Web 组件的 URL,双状态分离设计避免输入过程中 Web 组件频繁刷新。loadUrl 方法对输入做协议补全:无 http://https:// 前缀时自动补 https://

Scroll() {
  Row({ space: 8 }) {
    ForEach(QUICK_SITES, (u: string) => {
      Text(hostOf(u)).fontSize(10)
        .fontColor(this.webUrl === u ? COLORS.gold : COLORS.sub)
        .padding({ left: 10, right: 10, top: 5, bottom: 5 })
        .borderRadius(12)
        .backgroundColor(this.webUrl === u ? COLORS.dark : COLORS.card)
        .onClick(() => { this.urlInput = u; this.webUrl = u; })
    }, (u: string) => u)
  }
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)

快捷站点横滑行,ForEach 遍历 4 个站点 URL,用 hostOf 提取域名作为标签文本。选中态(webUrl === u)用鎏金色文字 + 深木色背景,未选中用绢帛黄文字 + 卡片色背景。点击直接同时更新 urlInputwebUrl,实现即时跳转。

12.2 Web 组件与主动下载

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

Web 组件接收 src(当前 URL)和 controller(WebviewController),layoutWeight(1) 占据中间全部高度。webControlleraboutToAppear 中已绑定 downloadDelegate,网页内触发的下载会进入四回调。

Row({ space: 8 }) {
  Column({ space: 2 }) {
    Text('webController.startDownload(url) 应用侧发起')
      .fontSize(9).fontColor(COLORS.text3)
      .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
  }
  .alignItems(HorizontalAlign.Start).layoutWeight(1)
  Button('触发').fontSize(11).height(30).backgroundColor(COLORS.gold)
    .onClick(() => { this.triggerDownload(RESOURCE_DL_URL); })
}

百科 Tab 底部有"触发下载"按钮,调用 triggerDownload 方法,传入预设的 RESOURCE_DL_URL(古建图册 PDF)。triggerDownload 内部调用 webController.startDownload(url) 实现应用侧主动发起下载,无需用户在网页内点击下载链接。这个设计展示了 ArkWeb 6.1.1 的应用侧下载能力。

十三、下载 Tab(Tab4)详解

13.1 进行中任务卡

Column({ space: 8 }) {
  Row({ space: 8 }) {
    Text('📥').fontSize(14)
    Column({ space: 2 }) {
      Text(this.dlName === '' ? '暂无进行中任务' : this.dlName)
        .fontSize(12).fontColor(COLORS.title)
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        .layoutWeight(1)
      Text(this.dlState).fontSize(10).fontColor(COLORS.sub)
    }
    .alignItems(HorizontalAlign.Start).layoutWeight(1)
    Text(`${this.dlPercent}%`).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
  }
  Progress({ value: this.dlPercent, total: 100, type: ProgressType.Linear })
    .width('100%').color(COLORS.bronze).backgroundColor(COLORS.dark)
}

进行中任务卡显示当前下载状态:文件名(dlName,空时提示"暂无进行中任务")、状态文案(dlState,“空闲”/“下载中”/“已完成”/“下载失败”)、百分比(dlPercent,鎏金色粗体)和线性进度条(青铜绿色)。下载代理的四回调实时更新这三个状态变量,UI 即时刷新。

13.2 双 URL 溯源记录卡

@Builder
downloadRecordCard(rec: DownloadRecord) {
  Column({ space: 6 }) {
    Row({ space: 8 }) {
      Text('📄').fontSize(14)
      Column({ space: 2 }) {
        Text(rec.fileName).fontSize(12).fontColor(COLORS.title)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(`${formatSize(rec.fileSize)} · ${rec.finishTime} 完成`)
          .fontSize(10).fontColor(COLORS.text3)
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
    }
    Row({ space: 6 }) {
      Text('🔗').fontSize(10)
      Text('原始URL getOriginalUrl()').fontSize(9).fontColor(COLORS.gold)
    }
    Text(rec.originalUrl).fontSize(10).fontFamily('monospace').fontColor(COLORS.sub)
      .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
    Row({ space: 6 }) {
      Text('📄').fontSize(10)
      Text('引用页URL getReferrerUrl()').fontSize(9).fontColor(COLORS.bronze)
    }
    Text(rec.referrerUrl).fontSize(10).fontFamily('monospace').fontColor(COLORS.sub)
      .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
  }
  .width('100%').padding(12).borderRadius(10).backgroundColor(COLORS.card)
  .alignItems(HorizontalAlign.Start)
}

下载记录卡是双 URL 溯源的核心展示组件,纵向三层:文件信息行(文件名 + 体积/完成时间);原始 URL 行("🔗"图标 + "原始URL getOriginalUrl()"标签鎏金色 + monospace 字体的 URL 文本);引用页 URL 行("📄"图标 + "引用页URL getReferrerUrl()"标签青铜绿色 + monospace 字体的 URL 文本)。两个 URL 标签明确标注了对应的 API 方法名,让用户理解溯源数据的来源。URL 文本用 monospace 字体保证等宽对齐,超长省略号截断。

13.3 下载 Tab 整体布局

下载 Tab 纵向两层:进行中任务卡(含文件名、状态、百分比和进度条)和完成记录列表。完成记录区有标题行(“完成记录” + 条数)和 ForEach 遍历 downloadRecords 渲染双 URL 溯源卡。下载完成后 onDownloadFinish 回调内调用 unshift 将新记录插入头部,实现最新记录置顶。

十四、工坊 Tab(Tab5)详解

14.1 像素画生成与纹理选色

private pickTextureColor(key: string, row: number, col: number): number {
  const palette: string[] = [COLORS.bronze, COLORS.gold, COLORS.sub, COLORS.text3, COLORS.bronzeD];
  if (key === 'mottle') {
    return hexToRgba(palette[(row + col) % palette.length]);
  }
  if (key === 'carve') {
    return hexToRgba(palette[(Math.floor(row / 8) + Math.floor(col / 8)) % palette.length]);
  }
  if (key === 'rammed') {
    return hexToRgba(palette[Math.floor(row / 8) % palette.length]);
  }
  if (key === 'stele') {
    return hexToRgba(palette[Math.floor(col / 8) % palette.length]);
  }
  const dx: number = row - CANVAS_SIZE / 2;
  const dy: number = col - CANVAS_SIZE / 2;
  const ring: number = Math.floor(Math.sqrt(dx * dx + dy * dy));
  return hexToRgba(palette[ring % palette.length]);
}

pickTextureColor 是纹理选色核心函数,五种纹理对应五种像素着色算法:斑驳用 (row + col) % 5 对角斜纹取色;雕花用 (Math.floor(row/8) + Math.floor(col/8)) % 5 棋盘格取色;夯土用 Math.floor(row/8) % 5 横带取色;碑刻用 Math.floor(col/8) % 5 竖带取色;年轮用 Math.floor(Math.sqrt(dx*dx + dy*dy)) % 5 同心环取色(dx/dy 为到画布中心的偏移)。五色调色板取自主题色系,确保像素画与整体视觉统一。

14.2 WebP 编码与沙箱落盘

async genWebpFile() {
  this.genState = '生成中…';
  // 1. 释放旧 PixelMap
  if (this.pixelMap) { this.pixelMap.release(); this.pixelMap = undefined; }
  // 2. 生成像素画
  const total: number = CANVAS_SIZE * CANVAS_SIZE;
  const buf: ArrayBuffer = new ArrayBuffer(total * 4);
  const pixels: Uint32Array = new Uint32Array(buf);
  for (let i = 0; i < total; i++) {
    const row = Math.floor(i / CANVAS_SIZE);
    const col = i % CANVAS_SIZE;
    pixels[i] = this.pickTextureColor(this.textureSel, row, col);
  }
  const pm: image.PixelMap = await image.createPixelMap(buf, opts);
  // 3. 编码为 WebP
  const packer: image.ImagePacker = image.createImagePacker();
  const webpBuf: ArrayBuffer = await packer.packToData(pm, packOpts);
  await packer.release();
  // 4. 写入沙箱文件
  const file: fileIo.File = fileIo.openSync(path, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
  fileIo.writeSync(file.fd, webpBuf);
  fileIo.closeSync(file);
}

genWebpFile 是工坊 Tab 的核心方法,四步完成像素画到 WebP 文件的全链路:释放旧 PixelMap(防内存泄漏)→ 生成 96x96 RGBA_8888 像素画(四字节一像素,Uint32Array 视图写入)→ ImagePacker 编码为 WebP 字节流(质量 90)→ 写入沙箱文件 heritage_photo.webp。生成后重置读取和回读快照,记录操作日志。文件以 READ_WRITE | CREATE | TRUNC 模式打开,保证可写可创建且覆盖旧内容。

14.3 元数据读取

async readMeta() {
  const file: fileIo.File = fileIo.openSync(this.webpPath, fileIo.OpenMode.READ_WRITE);
  const source: image.ImageSource = image.createImageSource(file.fd);
  const types: image.MetadataType[] = [image.MetadataType.WEBP_METADATA];
  const meta: image.ImageMetadata = await source.readImageMetadataByType(types, 0);
  const webp: image.WebPMetadata | undefined = meta.webPMetadata;
  this.metaSnapshot = new WebpMetaSnapshot(
    webp?.canvasWidth ?? -1, webp?.canvasHeight ?? -1,
    webp?.delayTime ?? -1, webp?.unclampedDelayTime ?? -1,
    webp?.loopCount ?? -1);
}

readMeta 使用 readImageMetadataByType + WEBP_METADATA 类型化读取,index=0(静态 WebP 恒取 0)。返回的 ImageMetadatawebPMetadata 字段是可选的,五个子字段也都是可选的,用 ?? -1 兜底 undefined。读取结果构造为 WebpMetaSnapshot 快照,用 metaCard Builder 渲染为五字段卡片。静态 WebP 通常仅提供画布尺寸,帧延迟和循环次数显示"未提供"。

14.4 元数据写入与回读校验

async writeMeta() {
  const webpMeta: image.WebPMetadata = {
    canvasWidth: CANVAS_SIZE,
    canvasHeight: CANVAS_SIZE,
    delayTime: this.writeDelay,
    unclampedDelayTime: this.writeDelay,
    loopCount: this.writeLoop
  };
  const meta: image.ImageMetadata = { webPMetadata: webpMeta };
  await source.writeImageMetadata(meta);
  await source.release();
  fileIo.closeSync(file);
  await this.verifyRead();
}

writeMeta 用字面量构造 WebPMetadata(canvasWidth/Height 固定 96,delayTime 和 unclampedDelayTime 都设为用户选中的 writeDelay,loopCount 设为 writeLoop),挂到 ImageMetadata 上调用 writeImageMetadata 写回文件。写入成功后立即调用 verifyRead 回读校验。

async verifyRead() {
  const file: fileIo.File = fileIo.openSync(this.webpPath, fileIo.OpenMode.READ_WRITE);
  const source: image.ImageSource = image.createImageSource(file.fd);
  const meta = await source.readImageMetadataByType([image.MetadataType.WEBP_METADATA], 0);
  this.verifySnapshot = new WebpMetaSnapshot(/* ... */);
  const ok = this.verifySnapshot.delayTime === this.writeDelay
    && this.verifySnapshot.loopCount === this.writeLoop;
}

verifyRead 重建 ImageSource(先 release 再重开 fd),回读五字段构造 verifySnapshot,比对 delayTimeloopCount 是否与写入值一致。一致记录"已生效",不一致记录差异值。回读快照用 metaCard 渲染且 highlight=true,使用 dark 底色区分于读取快照的 card 底色。

14.5 工坊四区块布局

工坊 Tab 纵向四区块,每区块有序号标记:①老照片样图生成(纹理选择 + 画布参数 + 预览 + 生成按钮);②五字段元数据读取(读取按钮 + metaCard 或提示);③写入控制台(帧延迟三档 + 循环四档 + 写入按钮);④回读校验与操作日志(verifySnapshot 或提示 + MetaOpLog 列表)。四区块通过序号 ①②③④ 鎏金色标记形成清晰的操作流程引导,用户从上到下依次完成"生成→读取→写入→回读"全链路。

十五、我的 Tab(Tab6)详解

15.1 渐变大卡

Column({ space: 10 }) {
  Row({ space: 10 }) {
    Text('🏛️').fontSize(34)
    Column({ space: 3 }) {
      Text('长安拾遗人').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Text('青铜级探访者 · 已点亮 6 处唐迹').fontSize(10).fontColor(COLORS.sub)
    }
    .alignItems(HorizontalAlign.Start).layoutWeight(1)
    Text('Lv.5').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
  }
  Row({ space: 8 }) {
    this.mineStat('收录', `${this.heritageList.length}`, '处')
    this.mineStat('踏访', '18', '次')
    this.mineStat('里程', '128.6', 'km')
  }
  Progress({ value: 64, total: 100, type: ProgressType.Linear })
    .width('100%').color(COLORS.gold).backgroundColor(COLORS.dark)
  Text('距离白银级还差 2 处踏访').fontSize(9).fontColor(COLORS.sub)
}
.width('100%').padding(16).borderRadius(14)
.linearGradient({ angle: 135, colors: [[COLORS.bronzeD, 0.0], [COLORS.dark, 1.0]] })

渐变大卡是整个页面唯一的渐变容器,135° 从青铜深色到次级容器底色的渐变,模拟青铜器从深锈到暗木的过渡。卡内三层:用户信息行(古建 emoji + 称号"长安拾遗人" + 等级"青铜级探访者 · 已点亮 6 处唐迹" + Lv.5 鎏金色);统计三格(收录数动态联动、踏访 18 次、里程 128.6km,数值联动 breath 变色);成长进度条(64% 鎏金色青铜绿底,配文"距离白银级还差 2 处踏访")。

15.2 足迹清单

@Builder
footRow(spot: SpotItem) {
  Row({ space: 8 }) {
    Text('✅').fontSize(12)
    Column({ space: 2 }) {
      Text(spot.name).fontSize(12).fontColor(COLORS.title)
      Text(spot.tag).fontSize(10).fontColor(COLORS.text3)
    }
    .alignItems(HorizontalAlign.Start).layoutWeight(1)
    Text('已踏访').fontSize(10).fontColor(COLORS.bronze)
  }
  .width('100%').padding(10).borderRadius(10).backgroundColor(COLORS.card)
}

足迹清单复用 MARKER_SPOTS 数据,每行:勾选 emoji + 古迹名称 + 朝代标签 + "已踏访"青铜绿色标签。6 处古迹全部标记为已踏访,与地图标注点形成数据闭环。

15.3 权限与说明行

说明行 4 条,前三条对应三大特性声明(Map Kit 地图数据、ArkWeb 百科与下载、Image Kit 工坊沙箱),第四条标注当前版本(HarmonyOS 6.1.1 · API 24)。每行:emoji + 标题 + 说明文本(省略号截断),深木色卡片底色。这组说明行让用户在"我的"Tab 直接了解平台的技术栈构成和运行环境。

十六、图表卡片

@Builder
chartCard() {
  Column({ space: 8 }) {
    Row() {
      Text('近 6 个月探访热度').fontSize(13).fontWeight(FontWeight.Bold)
        .fontColor(COLORS.title).layoutWeight(1)
      Text('次/月').fontSize(10).fontColor(COLORS.text3)
    }
    Row({ space: 10 }) {
      ForEach(MONTH_IDX, (i: number) => {
        Column({ space: 4 }) {
          Text(`${VISIT_VAL[i]}`).fontSize(8).fontColor(COLORS.sub)
          Column()
            .width('100%')
            .height(this.breath ? VISIT_VAL[i] * 0.62 + 8 : VISIT_VAL[i] * 0.62)
            .borderRadius({ topLeft: 5, topRight: 5 })
            .backgroundColor(i % 2 === 0 ? COLORS.bronze : COLORS.bronzeD)
          Text(MONTH_NAME[i]).fontSize(9).fontColor(COLORS.text3)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.End)
      }, (i: number) => `m_${i}`)
    }
    .height(150).alignItems(VerticalAlign.Bottom).width('100%')
  }
  .width('100%').padding(12).borderRadius(12).backgroundColor(COLORS.card)
  .alignItems(HorizontalAlign.Start)
}

柱状图卡是首页的数据可视化组件,纯 ArkUI 组件绘制(无 Canvas)。ForEach 遍历 6 个月索引,每月一个纵向 Column:数值文本(绢帛黄 8 号)、柱体(width('100%') + 动态高度 + 顶部圆角 + 双色交替)、月份名(陶土灰 9 号)。柱体高度 VISIT_VAL[i] * 0.62,呼吸 true 时加 8px 微波动,模拟数据实时变化。容器 height(150) + alignItems(VerticalAlign.Bottom) 保证柱体从底部对齐。双色交替(偶数月青铜绿、奇数月青铜深色)形成视觉节奏。

十七、底部 Tab 栏

@Builder
tabBar() {
  Row() {
    ForEach(TAB_LIST, (t: TabMeta, idx: number) => {
      Column({ space: 3 }) {
        Text(t.icon).fontSize(18)
        Text(t.label).fontSize(9)
          .fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
      }
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
      .onClick(() => { this.currentTab = idx; })
    }, (t: TabMeta, idx: number) => `${t.label}_${idx}`)
  }
  .width('100%').height(58).backgroundColor(COLORS.card)
  .padding({ top: 6, bottom: 6 })
}

底部 Tab 栏是 7 格单排导航,ForEach 遍历 TAB_LIST,每格 layoutWeight(1) 等宽均分。每格纵向两行:emoji 图标(18 号)+ 标签名(9 号)。选中态标签用 tabOn(鎏金色),未选中用 text3(陶土灰)。点击设置 currentTab = idx,触发内容区 Builder 切换。Tab 栏高度 58px,深木色背景,上下各 6px 内边距。

十八、弹窗系统

18.1 通用遮罩与输入行

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

@Builder
fieldRow(label: string, value: string, placeholder: string, onInput: (v: string) => void) {
  Column({ space: 4 }) {
    Text(label).fontSize(11).fontColor(COLORS.text3)
    TextInput({ text: value, placeholder: placeholder })
      .height(38).fontSize(12)
      .backgroundColor(COLORS.dark).fontColor(COLORS.title)
      .onChange((v: string) => { onInput(v); })
  }
  .width('100%').alignItems(HorizontalAlign.Start)
}

modalOverlay 是通用弹窗遮罩 Builder,全屏半透黑(rgba(0,0,0,0.6)),点击空白处调用 onClose 回调关闭弹窗。fieldRow 是通用输入行 Builder,标签 + TextInputonChange 通过回调函数将输入值同步到组件状态。两个通用 Builder 被三个弹窗面板复用,减少代码重复。

18.2 新增弹窗

新增弹窗(panelAdd)用于收录新古迹,4 个 fieldRow(古迹名称、朝代、保护等级、所在区域)+ 取消/确认按钮行。确认按钮调用 confirmAdd 方法:名称为空时直接返回,否则用 unshift 将新 HeritageItem 插入名录顶部,朝代和区域为空时分别填"未考"和"待考订",保护等级经 normLevel 归一化为三档标准名称,文保分默认 60。

18.3 编辑弹窗

编辑弹窗(panelEdit)用于修订古迹档案,比新增弹窗多一行提示文本"留空的字段将保持原值不变"。openEdit 方法在打开前回填当前条目的四个字段。confirmEdit 方法仅覆盖非空字段(名称和朝代 trim 后非空才覆盖),保护等级始终归一化,确保用户只需修改关注的字段。

18.4 删除弹窗

删除弹窗(panelDel)用于确认移出名录,宽度 72%(比新增/编辑的 86% 更窄,聚焦注意力)。显示条目名(delName 方法返回 delIdx 对应的古迹名称),"再想想"按钮(陶土灰文字深木色底)取消关闭,"确认移出"按钮(朱砂红色)调用 confirmDel 方法用 splice 删除条目并清零 delIdx

十九、功能模块对比表

维度首页 Tab地图 Tab搜索 Tab百科 Tab下载 Tab工坊 Tab我的 Tab
布局方式统计格+Flex双列卡+柱状图Toggle行+全屏地图+日志流搜索框+结果列表地址栏+快捷站点+全屏Web任务卡+溯源列表四区块纵向渐变大卡+足迹清单
数据模型HeritageItemEventLogSearchRecordDownloadRecordWebpMetaSnapshot+MetaOpLogSpotItem
字段数55455+34
核心操作编/删古迹双长按Toggle开关关键字搜索地址栏跳转/主动下载生成/读取/写入/回读
动画效果统计格breath变色+柱状图波动数值breath变色
状态颜色等级红/绿/灰类型青铜/鎏金/灰相关性金/绿/灰进度青铜绿快照card/dark区分等级青铜绿
数据量8古迹0→N事件7 POI4站点5记录5纹理+4档+4档6足迹+4说明
特殊组件Flex+ForEach柱状图MapComponent+ToggleProgress进度条Web+Scroll横滑Progress进度条Image+PixelMaplinearGradient渐变
HarmonyOS特性Map Kit双长按Map Kit searchByTextArkWeb下载代理ArkWeb双URL溯源Image Kit WebP元数据

深化解析:从代码结构到业务闭环

布局方式与数据流

非遗文博页面既要体现文化内容,也要维护年代、类别、来源与处理记录。素材馆或首页负责概览,地图和搜索提供空间探索,网页与下载承接外部资料,工坊和元数据页面支持数字化加工。逐段分析应关注朝代筛选、等级配色、双列卡片、地图事件和元数据日志之间的数据关系,避免只解释组件属性而忽略文化资产的流转。

页面根结构通常由头部、内容区和底部 Tab 栏组成。头部负责展示当前业务状态,内容区根据索引选择不同的 @Builder,底部导航负责修改索引。这样的结构把“当前显示什么”收敛为一个明确状态:用户点击 Tab 后先更新索引,ArkUI 再重新计算相关分支。各个 Builder 虽然共享主题色和页面级数据,却可以采用完全不同的布局方式;高密度列表适合纵向 Scroll,概览数据适合横向统计卡或双列 Flex,实时预览类组件需要独占有界高度,历史事件则适合时间轴或固定行高 List。

数据模型层承担界面与业务之间的契约。使用 @Observed 的实体保存可编辑字段,页面级 @State 数组负责驱动 ForEach。新增时创建新实体并插入数组,编辑时修改目标实体,删除时移除对应项。为了让列表差分稳定,key 应来自不会改变的唯一标识,不宜使用标题等可编辑字段。统计数字、完成比例和分类数量属于派生信息,可以从数组即时计算,避免同时维护两份状态后出现卡片已经更新、图表仍显示旧值的情况。

弹窗表单使用独立缓存是必要的。打开新增弹窗时清空缓存,打开编辑弹窗时复制目标字段,用户确认后才写回正式模型。这样点击取消不会污染列表数据。若直接把 TextInput 双向绑定到列表实体,用户尚未保存时卡片就可能跟着变化,破坏“确认提交”的交互语义。删除弹窗还需要保存目标索引或唯一标识,并在确认时再次校验目标存在,避免列表变化后误删其他项。

核心代码与状态驱动机制

@State 的价值不是简单替代普通变量,而是建立状态与界面之间的依赖关系。当前 Tab、筛选条件、动画开关、弹窗显隐、下载进度或能力状态发生变化时,只有读取这些变量的组件需要刷新。代码段中连续的修饰器调用分别控制尺寸、间距、背景、字体和事件,它们共同构成声明式描述;阅读时应从容器方向、子项分布、状态绑定和交互回调四个层面理解,而不是逐个孤立翻译属性名称。

ForEach 负责把数组映射为重复 UI。回调中的 item 提供业务字段,index 适合显示顺序,但不适合作为长期身份。列表发生新增或删除时,稳定 key 可以让框架复用未变化节点,减少重建。若直接修改对象属性后界面没有按预期刷新,可在保持实体身份的前提下替换数组引用;但不应为了刷新把所有元素都重新构造,否则会增加无意义渲染并丢失局部状态。

条件渲染体现了页面状态机。空闲时展示引导,准备中展示进度,成功时展示结果,失败时展示原因和重试入口。相比一个布尔值,四态文案更能覆盖异步能力。系统接口调用前先检查权限、设备支持和会话状态,调用后再读取结果校验。异常处理除了记录错误码,还要把可理解的反馈写入响应式状态,让用户知道失败发生在哪一步。

动画效果与颜色使用策略

呼吸动画通常由定时器周期翻转 breath,再把该状态映射为透明度、柱高或圆点半径的小幅变化。它适合表达“正在运行”或让统计图保持生命感,但幅度应克制,不能改变核心数据含义。柱状图的基础高度仍由真实数值计算,动画只能在很小范围内偏移;进度环的角度仍由完成比例决定,不能为了视觉效果显示超过真实进度的结果。页面离开时必须清理定时器,避免后台继续刷新。

颜色常量应按语义使用。主色承担选中态和主要操作,辅助色突出数据或次级动作,绿色表达完成与可用,橙色表达进行中或需要注意,红色只用于失败、逾期和删除等高风险场景。弱文本与分割线降低视觉权重,遮罩色用于聚焦弹窗。颜色不能成为唯一的状态信息,还要配合文字、图标或进度值,保证色觉差异用户也能理解。

渐变更适合头部大卡、核心指标或柱状图,不宜在每个小元素上重复使用。深色主题要检查正文与卡片背景的对比度,浅色主题则要避免辅助文字过淡。选中和未选中 Tab 除颜色差异外,还可以通过字重、图标透明度或底部指示器区分。这样既保持主题统一,又能建立清晰的信息层级。

各 Tab 之间的交互联动

各 Tab 不应只共享一个导航索引,还应围绕业务对象建立必要联动。列表页新增或编辑数据后,头部计数、图表和个人统计要同步更新;网页或地图产生的结果应写入记录模型,供下载、日志或我的页面继续展示;通知、字幕、相机等系统能力的状态应在头部胶囊或对应 Tab 中保持一致。跨 Tab 跳转时先更新必要参数,再修改当前索引,可以避免目标页面读取到旧条件。

切换离开重型组件时需要处理资源边界。相机输入、地图监听、字幕控制器、Web 下载代理和定时器都不能只创建不释放。可以在统一的 switchTab 方法中判断来源与目标,离开能力页时解除监听或停止会话;页面销毁时再执行兜底释放。释放方法应允许重复调用,并对每个资源独立判空,确保一次异常不会阻止后续清理。

交互反馈要覆盖成功与失败。按钮点击后先进入处理中状态并防止重复提交;成功后更新模型、关闭弹窗并显示结果;失败后保留用户输入,展示错误原因和重试入口。权限拒绝、能力不支持、网络失败、文件不存在和输入非法都属于正常业务分支。通过状态卡或行内提示展示这些分支,比只在控制台打印更符合完整产品体验。

边界场景与验证思路

空列表时应显示占位说明和新增入口,不能只留下空白。长标题需要限制行数并使用省略号,数字字段需要限定上下界,文本提交前要去除首尾空格。筛选后无结果应保留清除条件的入口。删除最后一项后,当前选择索引要回退到有效范围。异步搜索连续触发时,应防止较早请求晚返回后覆盖新结果。

验证数据链路时,可以依次检查新增、编辑、删除和筛选:新增后列表条数、统计数字和图表是否同时变化;编辑取消后正式数据是否保持不变;删除后 ForEach key 是否稳定;切换 Tab 再返回时必要数据是否仍在。验证系统能力时分别模拟支持、拒绝和异常,确认界面都有明确状态。验证动画时检查页面离开后是否停止,低性能设备上是否仍保持流畅。

视觉验收需要检查不同屏幕宽度、系统字体放大、深浅背景对比和长文本换行。表格中的布局方式、模型、字段数、核心操作、动画、状态颜色、数据量和特殊组件应与正文一致。Mermaid 图则需要对应真实的数据流和能力链路,节点文字加引号以避免中文或特殊字符导致解析失败。

组件化设计的进一步理解

参数化 Builder 适合抽取重复的统计格、状态行、标签和按钮组。参数只传入渲染所需数据和事件,不让子构建器直接依赖过多页面变量,可以降低耦合。业务复杂后,可把模型与系统能力封装为独立控制器,页面只负责组合 UI 和响应状态。这样既保留声明式代码的直观性,也能让权限、错误码翻译和资源释放得到集中管理。

当前单页面集中展示完整源码,便于博文逐段讲解。若演进为正式项目,可以按领域拆分组件:导航和页面框架位于容器层,列表、图表和弹窗位于展示层,数据读写和 Kit 接入位于服务层。组件之间通过参数、回调、@Link@ObjectLink 传递状态,不使用全局变量代替清晰的数据流。

性能优化首先来自减少不必要刷新。派生数据不要重复存储,动画状态不要进入列表 key,长列表使用稳定标识,Canvas 只在数据或尺寸变化时重绘。其次是控制资源生命周期,页面不可见时停止高成本任务。最后才是微调阴影、渐变和绘制细节。这样的优先级能保证页面在功能增加后仍然可维护。

通过以上补充,可以看到 ArkUI 的声明式模式并非只让布局语法更简洁,它更重要的价值是把数据变化、界面刷新和交互反馈连接为可追踪链路。理解每个代码段读取什么状态、写入什么状态、影响哪些组件,才能真正掌握文章中多个 Tab、图表、弹窗和系统能力协同工作的原理。

二十、总结与展望

本平台以"青铜锈绿 + 鎏金"的深色视觉体系为基底,将 HarmonyOS 6.1.1 的三大前沿特性——Map Kit 双长按事件监听、ArkWeb 下载代理双 URL 溯源、Image Kit WebP 元数据类型化读写——有机融合进文博文旅的 7 个业务场景中,构建了一个从古迹名录管理到地图探索、从 POI 搜索到资源下载、从图片生成到元数据校验的完整探索链路。

在技术架构上,平台采用"状态集中声明 + Builder 分散渲染"的模式。组件顶层统一声明六组状态变量(Tab/弹窗、古迹业务、Map Kit、POI 搜索、ArkWeb、WebP 元数据),七个 Tab 各自通过 @Builder 方法独立渲染,实现状态管理与视图渲染的解耦。build() 根方法的关键设计是"地图/百科全高不套 Scroll,其余走 Scroll 分支",避免 MapComponentWeb 组件的高度坍缩和手势冲突。

在 Map Kit 集成上,setupMapCallback 方法完成了地图初始化四步链:mapCallback 回调装配 → getEventManager 获取事件管理器 → addMarker 批量标注 6 处古迹 → onMarkerLongClick / onPoiLongClick 注册双长按监听。两个长按监听是 6.1.1 新特性接口,分别接收 map.MarkermapCommon.Poi 对象,提取 id/name/position 信息构造 EventLog 日志。toggleMarkerListen / togglePoiListen 方法通过 offXxxLongClick() 不传参清除全部订阅实现动态开关,开关状态变化也记录到事件日志。

在 ArkWeb 集成上,setupDownloadDelegate 方法绑定四回调到 webControlleronBeforeDownload 调用 start() 提供沙箱路径(否则任务停在 PENDING)、onDownloadUpdated 刷新进度条、onDownloadFailed 兜底失败、onDownloadFinish 读取 getOriginalUrl()getReferrerUrl() 双溯源字段。triggerDownload 方法通过 webController.startDownload(url) 实现应用侧主动下载。双 URL 溯源让每个下载文件可追溯到原始下载地址和触发下载的引用页,满足文博资源的版权溯源需求。

在 Image Kit 集成上,genWebpFile 方法完成"像素画→WebP→沙箱"四步链:pickTextureColor 按五种纹理算法着色 → image.createPixelMap 创建像素图 → ImagePacker.packToData 编码 WebP → fileIo 写入沙箱。readMeta 使用 readImageMetadataByType + WEBP_METADATA 类型化读取五字段,undefined 用 -1 占位。writeMeta 用字面量构造 WebPMetadata 写回,verifyRead 重建 ImageSource 回读校验。四步全链路通过 MetaOpLog 操作日志可视化呈现,让用户清晰看到每一步的执行结果。

在交互设计上,工坊 Tab 的四区块通过序号 ①②③④ 鎏金色标记形成操作流程引导,用户从上到下依次完成"生成→读取→写入→回读"。搜索 Tab 的 reliabilityScore 函数将 POI 可靠性分数映射为三档标签和颜色,配合线性进度条双重传达匹配程度。下载 Tab 的双 URL 溯源卡明确标注了 getOriginalUrl()getReferrerUrl() 两个 API 方法名,让用户理解溯源数据的来源。弹窗系统通过 modalOverlayfieldRow 两个通用 Builder 复用,三个面板(新增/编辑/删除)各自条件渲染,编辑弹窗的"留空保持原值"设计降低了用户修改成本。

展望未来,本平台可在以下方向深化:接入 AR 实景叠加能力实现古迹遗址的沉浸式现场还原;引入 AI 图像识别实现古建照片的自动年代鉴定和构件标注;通过分布式能力实现多设备协同的团队考察记录同步;利用 HarmonyOS 的传感器融合采集环境温湿度数据丰富遗产保护维度;结合 LBS 推送能力实现"走近古迹自动讲述历史"的智能导览。HarmonyOS 的 Map Kit、ArkWeb 和 Image Kit 三大能力的持续演进,为文博文旅的数字化探索提供了坚实的技术底座。

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

更多推荐