一、技术前言

在非物质文化遗产数字化保护与文创设计领域,传统纹样正经历从"博物馆典藏"到"数字化素材库"的深刻变革。从唐草卷纹的织锦提花到宋瓷冰裂纹的釉下彩绘,从联珠纹的金银錾刻到回纹的雕版印染,每一种纹样都承载着特定朝代的审美意趣与工艺智慧。然而,传统纹样素材平台长期面临三大技术挑战:品类占比可视化数据缺乏动态更新导致运营决策滞后、朝代与纹样双层分类的嵌套滚动体验割裂导致用户迷失层级、WebP 动画帧元数据无法在沙箱内读写导致纹理参数不可追溯。

HarmonyOS ArkUI 框架为这些挑战提供了系统级解决方案。ArkUI 的声明式 UI 范式通过 @Component 封装可复用组件、@State 管理响应式状态、@Builder 拆分复杂 UI 结构,天然适合"素材-频道-工坊-元数据"四层架构。@Observed 装饰器让数据模型字段级变化被 UI 感知,实现"纹样新增即列表刷新、元数据写入即快照更新"的流畅体验。ForEach 的键值生成器配合预计算数组,确保大列表渲染性能与筛选切换的丝滑过渡。

本平台深度融合 HarmonyOS 6.1.1(API 24)的三大前沿特性。Canvas 绘制 实现五品类馆藏占比环形图,通过 drawPie 方法绘制五扇区 ctx.arc 填色、中心镂空圆与中心文字标注,setInterval 每秒翻转 breath 布尔值驱动半径呼吸微动,globalAlpha 透明度用后立即复位确保不污染后续绘制。Tabs 嵌套滚动 引入 nestedScroll(TabsNestedScrollMode) 新特性,外层朝代频道 Tabs 嵌套内层纹样类别 Tabs,SELF_FIRST(先内后外)与 SELF_ONLY(仅内层)双模式可切换,内层滑到边缘时是否联动外层翻页由模式决定,每次翻页均通过 onChange 回调记入滑动日志时间轴。Image Kit WebP 元数据 实现 readImageMetadataByType 读取五字段快照、writeImageMetadata 写回帧延迟与循环次数、重建 ImageSource 二次读取回读校验的全链路沙箱演示,所有字段均为可选值,读取后须 ?? -1 兜底,禁止将 undefined 当作 0 处理。

二、整体架构流程图

弹窗系统三态统一

数据模型层Observed

PatternItem 纹样素材

InnerCard 子类卡片

SwipeLog 滑动日志

WebpMetaSnapshot 元数据快照

MetaOpLog 操作日志

Page1258 主组件

headerBanner 头部朱砂渐变Banner

内容区 6 Tab 切换

tabBar 底部导航

弹窗系统 add/edit/del

Tab0 素材馆
朝代chips+双列纹样卡+环形图+柱状图

Tab1 频道
朝代×纹样双层Tabs嵌套滚动

Tab2 日志
翻页事件时间轴固定行高72

Tab3 工坊
纹理五选一+WebP像素画生成

Tab4 元数据
五字段读写回读全链路

Tab5 我的
守艺人渐变大卡+任务清单

Canvas绘制
drawPie环形图呼吸微动

Tabs嵌套滚动
nestedScroll双模式切换

ImageKit
像素画编码WebP落盘

ImageKit
readImageMetadataByType

panelAdd 新建纹样收藏

panelEdit 编辑用途描述

panelDel 删除确认

架构以主组件为根,使用 Stack 容器层叠:底层 Column 纵向排列头部朱砂渐变 Banner、分割线、内容区和底部 Tab 栏,顶层是三个独立弹窗(addModal/editModal/delModal 各自条件渲染)。内容区通过 currentTab 索引在 6 个 @Builder 方法间切换,三大特性分散在素材馆(Canvas 环形图)、频道(嵌套滚动)、工坊与元数据(WebP 全链路)四个 Tab 上。状态变量统一声明在组件顶层实现跨 Tab 共享——breath 呼吸布尔值同时驱动 Canvas 环形图半径微动和柱状图柱高波动,nestedMode 嵌套模式同时影响频道 Tab 的滚动行为和日志 Tab 的徽标颜色,webpPath 沙箱路径同时串联工坊生成产物和元数据读写对象。

三、色彩体系设计

3.1 ColorPalette 接口定义

色彩体系的接口定义集中声明了页面所有颜色字段,采用宣纸米白底色搭配朱砂红主色的浅色系方案:

interface ColorPalette {
  bg: string;       // 页面底色·宣纸米白
  card: string;     // 卡片底色·纯白
  chip: string;     // 胶囊/输入底色·米杏
  title: string;    // 主标题·墨褐
  sub: string;      // 次级文字·驼褐
  text3: string;    // 弱化文字·浅驼
  red: string;      // 主题色·朱砂
  redD: string;     // 主题色深·深朱砂
  blue: string;     // 辅色·黛蓝
  gold: string;     // 辅色·鎏金
  green: string;    // 辅色·苔绿
  line: string;     // 分割线·米灰
  tabOn: string;    // Tab 激活色·朱砂
  mask: string;     // 弹窗遮罩·墨褐半透
  onMain: string;   // 深色底上的白字
  gradA: string;    // 渐变起点·朱砂(头部/会员卡)
  gradB: string;    // 渐变终点·深朱砂(统计大卡)
}

3.2 COLORS 常量逐色分析

const COLORS: ColorPalette = {
  bg: '#F7F3EC',      // 宣纸米白,非遗典藏底色,温润不刺眼
  card: '#FFFFFF',    // 纯白卡片底,与米白背景形成微弱层次
  chip: '#EFE8DC',    // 米杏胶囊底色,胶囊与输入框统一质感
  title: '#3B2F25',   // 墨褐主标题,高对比深色文字
  sub: '#8A7A64',     // 驼褐次级文字,层次柔和过渡
  text3: '#B5A88F',   // 浅驼弱化文本,辅助信息不抢视觉
  red: '#C0392B',     // 朱砂主题色,按钮与渐变起点
  redD: '#9A2C20',    // 深朱砂,渐变终点与删除操作
  blue: '#2C3E7A',    // 黛蓝,朝代徽标与生成操作
  gold: '#B8860B',    // 鎏金,优选档位与外层频道徽标
  green: '#5E8C61',   // 苔绿,已生效态与内层类别徽标
  line: '#E8E0D0',    // 米灰分割线,低对比不干扰
  tabOn: '#C0392B',   // Tab 选中色为朱砂(与主题色一致)
  mask: 'rgba(59,47,37,0.55)', // 墨褐半透遮罩
  onMain: '#FFFFFF',  // 深色底上的白字
  gradA: '#C0392B',   // 朱砂渐变起点
  gradB: '#9A2C20'    // 深朱砂渐变终点
};

色彩体系以"宣纸米白 + 朱砂红"为核心对比。米白代表宣纸的温润底色,朱砂代表中国传统印章与漆器的主题色。值得注意的是 Tab 选中色使用 red(朱砂)而非其他辅色,这是因为朱砂在米白背景上对比度最高且与主题色统一,用户视觉定位迅速。头部 Banner 使用 linearGradientgradA(朱砂)到 bg(宣纸米白)的 160 度渐变,模拟朱砂印章在宣纸上晕染开的效果。辅色系统中,黛蓝用于朝代徽标,鎏金用于优选档位与外层频道,苔绿用于已生效态与内层类别,深朱砂用于渐变终点与删除操作,五色各司其职、互不僭越。环形图五扇区配色直接取自主题色板——回纹朱砂、云纹黛蓝、方胜鎏金、冰裂苔绿、联珠深朱砂,品类与色彩形成稳定的视觉映射。

四、Tab 元数据与常量定义

4.1 底部导航 Tab 定义

底部导航采用 6 Tab 单排布局,每个 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: '我的' }
];

六个 Tab 的命名暗含业务语义链路:素材馆是浏览入口,频道是分类深挖,日志记录行为轨迹,工坊是创作工具,元数据是技术追溯,我的是用户中心。这种排列遵循"浏览→创作→追溯→个人"的用户动线设计。

4.2 嵌套频道与纹样常量

频道 Tab 的双层嵌套结构依赖两组常量定义。外层朝代频道共 5 个,采用 barMode(BarMode.Scrollable) 横滑页签:

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

const OUTER_CHANNELS: ChannelItem[] = [
  { name: '唐', icon: '🏯' },
  { name: '宋', icon: '🖌' },
  { name: '元', icon: '🏇' },
  { name: '明', icon: '🏮' },
  { name: '清', icon: '🪷' }
];

const INNER_TABS: string[] = ['回纹', '云纹', '方胜', '冰裂', '联珠'];

外层覆盖唐宋元明清五大朝代,内层覆盖回纹、云纹、方胜、冰裂、联珠五大纹样类别,5 乘 5 共 25 种组合,每种组合生成 8 条 Mock 素材卡片,总计 200 条数据充分满足"滑到边缘"的嵌套滚动演示需求。

4.3 图表数据与纹理常量

Canvas 环形图的数据定义遵循 ArkTS 类型规范——禁止 {val:number}[] 内联类型,必须先定义 interface 再声明数组:

interface PieData {
  val: number;    // 占比(%,合计 100)
  label: string;  // 品类名
}

const PIE_DATA: PieData[] = [
  { val: 30, label: '回纹' },
  { val: 25, label: '云纹' },
  { val: 18, label: '方胜' },
  { val: 15, label: '冰裂' },
  { val: 12, label: '联珠' }
];

const PIE_COLORS: string[] = [COLORS.red, COLORS.blue, COLORS.gold, COLORS.green, COLORS.redD];
const PIE_TOTAL: number = 1280;

环形图五扇区占比合计 100%,回纹占比最高(30%),联珠最低(12%),中心镂空区域展示馆藏总量 1280 件。柱状图数据为 6 个月纹样素材成交量,满刻度 640 件作为柱高换算基准。工坊纹理五选一将像素算法与纹样语义绑定——斜纹对应回纹(横竖折线连绵不断,寓意福寿绵长)、棋盘对应方胜(两菱相扣同心相连,寓意同心永结)、横带对应云纹(如意云头层层叠叠,寓意平步青云)、竖带对应冰裂(冰面裂纹织理纵横,寓意破冰新生)、同心环对应联珠(圆珠连环成带成圈,寓意珠联璧合)。

WebP 编码参数包括固定质量 90、画布边长 96 像素、帧延迟预设三档(120/200/500 毫秒)、循环次数预设四档(0 不限/1/3/5 次)。像素画五色色板直接取自应用主题辅助色——朱砂、黛蓝、鎏金、苔绿、深朱砂,确保工坊产物的色彩体系与全站统一。

五、工具函数群

工具函数群是连接常量数据与组件渲染的桥梁,共定义了 11 个函数,涵盖嵌套模式翻译、字段格式化、像素算法、品类配色和状态配色五大类。

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 ? '先内后外' : '仅内层';
}

modeLabel 生成完整文案用于日志记录的 mode 字段,modeShort 生成短文案用于头部胶囊与模式切换 chips。TabsNestedScrollMode 是 HarmonyOS 6.1.1 新增的枚举类型,SELF_FIRST 表示内层先消费滑动事件,滑到边缘后接力到外层;SELF_ONLY 表示内层独立消费滑动事件,滑到边缘不联动外层。

5.2 字段格式化函数

元数据五字段均为可选值,格式化函数统一以 -1 表示"未提供":

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

function loopText(v: number): string {
  if (v < 0) { return '未提供'; }
  if (v === 0) { return '0(不限)'; }
  return `${v}`;
}

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

这三个函数确保快照卡渲染时永远不会出现 undefinedNaNfmtField 是通用数值格式化器,loopText 特殊处理 0 为"不限"(WebP 循环次数的语义约定),sizeText 专用于像素尺寸。

5.3 像素算法与色彩转换函数

hexToRgba 将十六进制颜色字符串转为 RGBA8888 小端整数,供 Uint32Array 直写像素:

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

注意小端排布是 BGRA 字节序但以 Uint32 解读时为 RGBA,因此位移顺序为 b << 16 | g << 8 | r,最高字节 0xFF 为 Alpha 通道不透明。0xFF000000 的按位或运算确保 Alpha 始终为 255。

pixelColor 是五种纹理算法的核心分发器,根据纹理键返回对应位置的像素颜色值:

function pixelColor(row: number, col: number, key: string, palette: string[]): number {
  const n = palette.length;
  if (key === 'checker') {
    // 棋盘(方胜):8×8 分块行列块索引相加取模
    return hexToRgba(palette[(Math.floor(row / 8) + Math.floor(col / 8)) % n]);
  }
  if (key === 'horz') {
    // 横带(云纹):每 16 行换一色
    return hexToRgba(palette[Math.floor(row / 16) % n]);
  }
  if (key === 'vert') {
    // 竖带(冰裂):每 16 列换一色
    return hexToRgba(palette[Math.floor(col / 16) % n]);
  }
  if (key === 'ring') {
    // 同心环(联珠):按到画布中心的距离分环
    const dx = col - CANVAS_SIZE / 2;
    const dy = row - CANVAS_SIZE / 2;
    const dist = Math.sqrt(dx * dx + dy * dy);
    return hexToRgba(palette[Math.floor(dist / 9) % n]);
  }
  // 斜纹(回纹·默认):行列相加取模形成 45° 斜带
  return hexToRgba(palette[(row + col) % n]);
}

五种算法各有几何特征:斜纹通过 row + col 取模形成 45 度等差斜带;棋盘通过 8 像素分块的行列索引相加取模形成方格;横带通过行号除 16 取模形成水平条纹;竖带通过列号除 16 取模形成垂直条纹;同心环通过到中心欧氏距离除 9 取模形成环形。取模运算 % n(n=5 色板长度)确保颜色循环不越界。

5.4 状态与品类配色函数

function catColor(cat: string): string {
  if (cat === '回纹') { return COLORS.red; }
  if (cat === '云纹') { return COLORS.blue; }
  if (cat === '方胜') { return COLORS.gold; }
  if (cat === '冰裂') { return COLORS.green; }
  if (cat === '联珠') { return COLORS.redD; }
  return COLORS.text3;
}

function scoreBadge(score: number): string {
  if (score >= 92) { return '精选'; }
  if (score >= 88) { return '优选'; }
  return '良品';
}

function scoreColor(score: number): string {
  if (score >= 92) { return COLORS.red; }
  if (score >= 88) { return COLORS.gold; }
  return COLORS.green;
}

catColor 将纹样品类映射为稳定的色彩标识,与环形图扇区配色保持一致。scoreBadgescoreColor 将适配度评分分为三档——92 分以上为"精选"(朱砂色)、88 分以上为"优选"(鎏金色)、其余为"良品"(苔绿色),档位越高色彩越浓烈。genStateColor 根据 WebP 生成状态文案中的关键字返回配色(失败深朱砂/生成中鎏金/已生成苔绿/待生成浅驼),opColor 为元数据操作日志配色(生成黛蓝/读取苔绿/写入朱砂/回读鎏金),layerColor 为滑动日志层级徽标配色(外层朝代鎏金/内层类别苔绿)。

六、数据模型层

数据模型层定义了 5 个 @Observed 类,分别承载纹样素材、子类卡片、滑动日志、元数据快照和操作日志五大业务实体。

6.1 纹样素材模型 PatternItem

@Observed
export class PatternItem {
  name: string;    // 纹样名(如 唐草卷纹)
  dynasty: string; // 朝代(唐/宋/元/明/清)
  cat: string;     // 品类(回纹/云纹/方胜/冰裂/联珠)
  uses: string;    // 用途描述(工艺载体 · 应用场景)
  score: number;   // 适配度评分(0~100)

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

PatternItem 是素材馆双列卡片的实体类,绑定弹窗增删改操作。@Observed 装饰器使 uses 字段修改后 UI 自动刷新——编辑弹窗保存后卡片用途描述立即更新。Mock 数据 8 条覆盖唐宋元明清五朝代和五大品类,每条用途描述均为真实工艺载体与应用场景(如"汝窑天青釉口沿描银 · 茶器套装礼盒")。

6.2 子类卡片模型 InnerCard 与 Mock 生成器

@Observed
export class InnerCard {
  id: string;     // 唯一键(朝代-品类-序号)
  tag: string;    // 所属纹样类别子页签名
  title: string;  // 卡片标题(工艺载体 第 N 期)
  desc: string;   // 卡片描述(作品名 + 工艺注解)
}

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_WORKS[i - 1]}${INNER_NOTES[i - 1]}`));
  }
  return list;
}

innerMockData 是动态 Mock 生成器,根据外层朝代频道和内层纹样类别拼装 8 条卡片数据。INNER_TITLES 池提供 8 个工艺载体名(织锦提花、瓷器釉彩、金银錾刻等),INNER_WORKS 池提供 8 个作品名(《缠枝宝相》妆花缎、《青花冰梅》盖碗等),INNER_NOTES 池提供 8 句工艺注解(含针距、切厚、开片率等技术参数)。每条卡片内容均超一屏可视区域,确保"滑到边缘"的嵌套滚动行为可被用户感知。

6.3 滑动日志、元数据快照与操作日志模型

@Observed
export class SwipeLog {
  layer: string;    // 层级(外层朝代/内层类别)
  tabName: string;  // 翻到的页签名
  fromIdx: number;  // 起始索引
  toIdx: number;    // 目标索引
  mode: string;     // 触发时的嵌套模式
  time: string;     // 记录时间
}

@Observed
export class WebpMetaSnapshot {
  canvasWidth: number;         // 画布宽(px),-1=未提供
  canvasHeight: number;        // 画布高(px),-1=未提供
  delayTime: number;           // 帧延迟(钳制后 ms),-1=未提供
  unclampedDelayTime: number;  // 帧延迟(未钳制 ms),-1=未提供
  loopCount: number;           // 循环次数,-1=未提供(0=不限)
}

@Observed
export class MetaOpLog {
  op: string;      // 操作名
  detail: string;  // 结果明细(含错误码)
  time: string;    // 记录时间
}

SwipeLog 记录每次翻页的层级、页签名、起止索引、事发模式和记录时间,构造函数中自动生成 HH:MM:SS 格式时间戳。WebpMetaSnapshot 是 WebP 元数据五字段的镜像快照,-1 作为"未提供"的统一占位符,读取快照和回读校验快照共用同一模型但渲染时用不同边框色区分。MetaOpLog 记录元数据操作日志,detail 字段含错误码和结果明细,opLogs 数组限制 30 条、swipeLogs 限制 40 条,超过后 pop() 尾部元素。

七、组件主体与生命周期

7.1 主组件状态声明

主组件继承 @Entry @Component,状态变量按业务域分组声明:

@Entry
@Component
struct Page1258 {
  /************* 基础 UI 状态 *************/
  @State currentTab: number = 0;      // 当前 Tab 索引
  @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 inputText: string = '';

  /************* 素材馆业务状态 *************/
  @State patternList: PatternItem[] = PATTERN_LIST;
  @State activeDynasty: string = '全部';

  /************* 特性 A 状态(Canvas 绘制) *************/
  private pieCtx: CanvasRenderingContext2D =
    new CanvasRenderingContext2D(new RenderingContextSettings(true));

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

  /************* 特性 C 状态(WebP 元数据) *************/
  @State textureIdx: number = 0;
  @State pixelMap?: image.PixelMap = undefined;
  @State webpPath: string = '';
  @State genState: string = '待生成';
  @State metaSnapshot?: WebpMetaSnapshot = undefined;
  @State writeDelay: number = 120;
  @State writeLoop: number = 3;
  @State verifySnapshot?: WebpMetaSnapshot = undefined;
  @State opLogs: MetaOpLog[] = [];
}

状态声明有三个值得注意的设计决策。其一,pieCtx 声明为 private 而非 @State——Canvas 上下文不参与响应式渲染,RenderingContextSettings(true) 开启抗锯齿。其二,breath 布尔值是跨 Tab 共享的呼吸驱动源,同时驱动 Canvas 环形图半径(r = 66 + (this.breath ? 4 : 0))和柱状图柱高(wave = (i % 2 === 0) === this.breath ? 1.06 : 0.94)。其三,弹窗状态三态统一管理——addModal/editModal/delModal 三个布尔值互斥,editIdx/delIdx/inputText 三个辅助状态被三个弹窗共享,closeAllModals 统一复位。

7.2 生命周期与呼吸动画

aboutToAppear() {
  this.timer = setInterval(() => {
    this.breath = !this.breath;
    this.drawPie();  // Canvas 不随 @State 自动重绘,须手动调用绘制方法
  }, 1000);
}

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

aboutToAppear 中以 1000 毫秒间隔启动定时器,每秒翻转 breath 布尔值并手动调用 drawPie 重绘 Canvas。这里有一个关键技术细节:Canvas 组件不随 @State 变化自动重绘,必须通过 drawPie 方法显式调用 ctx.clearRect + 重绘扇区来实现动画效果。aboutToDisappear 中清理定时器避免内存泄漏,timer 重置为 -1 作为"已清理"标志位。

7.3 根构建 build 方法

build() {
  Stack({ alignContent: Alignment.Center }) {
    Column() {
      this.headerBanner()
      Divider().strokeWidth(1).color(COLORS.line)
      Column() {
        if (this.currentTab === 0) {
          this.tabPatterns()
        } else if (this.currentTab === 1) {
          this.tabNested()
        } else if (this.currentTab === 2) {
          this.tabLogs()
        } else if (this.currentTab === 3) {
          this.tabStudio()
        } else if (this.currentTab === 4) {
          this.tabMeta()
        } else {
          this.tabMine()
        }
      }.layoutWeight(1).width('100%')
      this.tabBar()
    }.width('100%').height('100%')
    if (this.addModal || this.editModal || this.delModal) {
      this.modalOverlay(() => { this.closeAllModals(); })
    }
  }.width('100%').height('100%').backgroundColor(COLORS.bg)
}

Stack 容器层叠布局是弹窗系统的结构基础。底层 Column 纵向排列头部 Banner、分割线、内容区和底部 Tab 栏,内容区通过 if-else 链在 6 个 @Builder 方法间切换。顶层弹窗遮罩通过三个布尔值的或运算条件渲染——任一弹窗激活时渲染 modalOverlay,点击空白区调用 closeAllModals 统一关闭。StackalignContent: Alignment.Center 确保弹窗面板在遮罩层中水平居中,而面板内部通过 justifyContent(FlexAlign.End) 使面板贴底弹出。

八、头部 Banner 详解

头部 Banner 是三特性状态的汇聚展示区,通过朱砂渐变背景和白字实现高对比视觉锚点:

@Builder
headerBanner() {
  Column({ space: 10 }) {
    Row() {
      Column({ space: 4 }) {
        Text('纹章集 · 非遗纹样素材平台').fontSize(20).fontWeight(FontWeight.Bold)
          .fontColor(COLORS.onMain)
        Text(this.currentTab === 0 ? `素材馆 · 馆藏 ${this.patternList.length}`
          : this.currentTab === 1 ? '频道 · 朝代×纹样双层 Tabs'
            : this.currentTab === 2 ? '滑动日志 · nestedScroll 时间线'
              : this.currentTab === 3 ? 'WebP 纹理样图工坊'
                : this.currentTab === 4 ? '元数据读写 · 五字段'
                  : '守艺人中心').fontSize(11).fontColor(COLORS.onMain).opacity(0.85)
      }.alignItems(HorizontalAlign.Start).layoutWeight(1)

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

    Row({ space: 8 }) {
      // 馆藏胶囊
      Row({ space: 6 }) {
        Text('🏛️').fontSize(10)
        Text(`馆藏 ${PIE_TOTAL}`).fontSize(10).fontColor(COLORS.sub)
      }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
      .borderRadius(12).backgroundColor(COLORS.chip)

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

      // 特性 C 状态胶囊(WebP 生成状态)
      Row({ space: 4 }) {
        Circle({ width: 6, height: 6 }).fill(genStateColor(this.genState))
        Text(`WebP ${this.genState}`).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.bg, 1]]
  })
}

头部 Banner 分上下两行。上行左侧是平台标题和 Tab 联动副标题——副标题通过六元三元表达式链根据 currentTab 显示当前 Tab 的业务描述,右侧是呼吸圆点,透明度随 breath 在 0.9 与 0.45 间交替。下行是三个状态胶囊:馆藏胶囊显示总量 1280 件;嵌套模式胶囊的圆点颜色随 nestedMode 变化(SELF_FIRST 苔绿/SELF_ONLY 鎏金),文案随模式切换;WebP 状态胶囊的圆点颜色由 genStateColor 根据 genState 文案关键字决定,文案超长时 maxLines(1) 配合 textOverflow 省略号截断。Banner 背景使用 160 度 linearGradient 从朱砂到宣纸米白的渐变,模拟朱砂印章在宣纸上的晕染效果。

九、各 Tab 内容详解

9.1 Tab0 素材馆:朝代筛选与双列卡片

素材馆 Tab 是浏览入口,纵向 Scroll 包含五个区段:标题行、朝代筛选横滚 chips、双列纹样卡片、Canvas 环形图卡和月度柱状图卡。

朝代筛选 chips 采用横滚 Scroll + Row 布局,6 个 chips(全部/唐/宋/元/明/清)通过 activeDynasty 状态控制选中态——选中时朱砂底白字,未选中时米杏底驼褐字。筛选逻辑通过预计算方法实现:

filteredPatterns(): PatternItem[] {
  if (this.activeDynasty === '全部') { return this.patternList; }
  const result: PatternItem[] = [];
  for (const item of this.patternList) {
    if (item.dynasty === this.activeDynasty) {
      result.push(item);
    }
  }
  return result;
}

patternPairs(): PatternItem[][] {
  const src = this.filteredPatterns();
  const pairs: PatternItem[][] = [];
  for (let i = 0; i < src.length; i += 2) {
    const pair: PatternItem[] = [];
    pair.push(src[i]);
    if (i + 1 < src.length) {
      pair.push(src[i + 1]);
    }
    pairs.push(pair);
  }
  return pairs;
}

filteredPatternspatternPairs 是预计算方法而非 ForEach 内联 filter 调用,避免每次渲染重复执行筛选逻辑。patternPairs 将筛选结果按两条一组切块为二维数组,ForEach 外层遍历行、内层遍历列实现双列卡片布局。indexOfPattern 方法通过引用比对(this.patternList[i] === item)求条目在完整列表中的真实索引,解决筛选列表与源列表索引不对齐的问题。

纹样卡片 Builder 包含纹样名、朝代徽标、品类色徽、适配度、用途描述和编辑/删除操作五个信息区:

@Builder
patternCard(item: PatternItem) {
  Column({ space: 8 }) {
    Row({ space: 6 }) {
      Text(item.name).fontSize(13).fontWeight(FontWeight.Bold)
        .fontColor(COLORS.title).maxLines(1).layoutWeight(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Text(item.dynasty).fontSize(9).fontWeight(FontWeight.Bold)
        .fontColor(COLORS.onMain)
        .padding({ left: 7, right: 7, top: 3, bottom: 3 })
        .borderRadius(8).backgroundColor(COLORS.blue)
    }.width('100%')

    Row({ space: 6 }) {
      Text(item.cat).fontSize(10).fontColor(COLORS.onMain)
        .padding({ left: 7, right: 7, top: 3, bottom: 3 })
        .borderRadius(8).backgroundColor(catColor(item.cat))
      Text(`${scoreBadge(item.score)} ${item.score}`).fontSize(10)
        .fontColor(scoreColor(item.score)).fontFamily('monospace')
    }.width('100%')

    Text(item.uses).fontSize(10).fontColor(COLORS.sub).maxLines(2).width('100%')
      .textOverflow({ overflow: TextOverflow.Ellipsis })

    Row({ space: 12 }) {
      Blank()
      Text('编辑').fontSize(10).fontColor(COLORS.blue)
        .onClick(() => { this.openEdit(this.indexOfPattern(item)); })
      Text('删除').fontSize(10).fontColor(COLORS.redD)
        .onClick(() => { this.openDel(this.indexOfPattern(item)); })
    }.width('100%')
  }.padding(12).borderRadius(12).backgroundColor(COLORS.card)
  .layoutWeight(1).alignItems(HorizontalAlign.Start)
}

卡片设计有三层信息密度。第一行纹样名配朝代徽标——朝代使用黛蓝底白字徽标,纹样名超长省略截断。第二行品类色徽配适配度——品类色由 catColor 返回稳定的五色映射,适配度档位由 scoreBadgescoreColor 双函数返回文案与配色。第三行用途描述两行截断。第四行行内操作通过 indexOfPattern 精确定位源列表索引,编辑调用 openEdit、删除调用 openDel

9.2 Tab1 频道:双层 Tabs 嵌套滚动

频道 Tab 是特性 B 的核心演示区,结构为模式切换 chips + 双层位置说明行 + 外层朝代 Tabs 嵌套内层纹样 Tabs。

外层 Tabs 宿主 5 个朝代频道,barMode(BarMode.Scrollable) 使页签横滑,onChange 回调记录翻页日志:

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%')

内层 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() {
              // 卡片内容
            }
          }, (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)   // ★ nestedScroll 挂载点
  .layoutWeight(1).width('100%')
}

.nestedScroll(this.nestedMode) 是 API 24 新特性的挂载点,只有被嵌套的内层 Tabs 调用此方法才能启用嵌套滚动行为。当模式为 SELF_FIRST 时,内层 List 滑到底部边缘后继续滑动会接力触发外层 Tabs 的 onChange;当模式为 SELF_ONLY 时,内层滑到边缘即停止,不联动外层。模式切换通过顶部 chips 即时生效,this.nestedMode@State 变量,赋值后内层 Tabs 的 nestedScroll 参数自动更新。

9.3 Tab2 日志:翻页事件时间轴

日志 Tab 以固定行高 72 的时间轴展示翻页事件记录:

if (this.swipeLogs.length === 0) {
  // 空态占位卡
} else {
  List() {
    ForEach(this.swipeLogs, (log: SwipeLog) => {
      ListItem() {
        Row() {
          // 时间列(固定宽 48)
          Column({ space: 4 }) {
            Text(log.time).fontSize(10).fontWeight(FontWeight.Bold)
              .fontColor(layerColor(log.layer)).fontFamily('monospace')
            Text(log.layer === '外层朝代' ? '朝代' : '类别').fontSize(8)
              .fontColor(COLORS.text3)
          }.width(48).height('100%').justifyContent(FlexAlign.Center)

          // 竖线轨道列(圆点徽标 + 竖线在固定行高内填满)
          Column() {
            Circle({ width: 10, height: 10 }).fill(layerColor(log.layer))
            Column().width(2).layoutWeight(1).backgroundColor(COLORS.line)
          }.width(14).height('100%').alignItems(HorizontalAlign.Center)

          // 事件卡片(固定行高等高)
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(log.tabName).fontSize(12).fontWeight(FontWeight.Bold)
                .fontColor(COLORS.title)
              Text(`${log.fromIdx + 1}${log.toIdx + 1}`).fontSize(10)
                .fontColor(COLORS.sub).fontFamily('monospace')
            }.width('100%')
            Text(`事发模式:${log.mode}`).fontSize(9).fontColor(COLORS.text3)
              .maxLines(1).width('100%')
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Text(log.layer === '外层朝代' ? '外层朝代频道 TabContent 切换' : '内层纹样类别 TabContent 切换')
              .fontSize(9).fontColor(COLORS.text3)
          }.layoutWeight(1).height('100%').justifyContent(FlexAlign.Center)
          .padding({ left: 10, right: 10, top: 8, bottom: 8 })
          .borderRadius(10).backgroundColor(COLORS.card)
        }.width('100%').height(72).margin({ bottom: 6 })
      }
    }, (log: SwipeLog) => `${log.time}_${log.tabName}_${log.toIdx}`)
  }.scrollBar(BarState.Off).width('100%').layoutWeight(1)
}

时间轴采用三列布局:时间列固定宽 48 像素展示时间和层级短名,竖线轨道列宽 14 像素包含圆点徽标和 layoutWeight(1) 填满行高的竖线,事件卡片列占据剩余空间。固定行高 .height(72) 是时间轴视觉对齐的关键——竖线在行高内填满确保相邻条目的竖线无缝衔接,形成连续的时间轴视觉。层级徽标颜色由 layerColor 区分(外层朝代鎏金/内层类别苔绿),swipeLogs 使用 unshift 倒序排列使最新事件置顶。

9.4 Tab3 工坊:纹理五选一与 WebP 生成

工坊 Tab 是特性 C 的上游环节,将像素算法编码为 WebP 文件落盘沙箱。生成方法 genWebpFile 是全链路核心:

async genWebpFile() {
  this.genState = '生成中…';
  const hostCtx = this.getUIContext().getHostContext();
  const dir = hostCtx ? hostCtx.filesDir : '';
  if (dir === '') {
    this.genState = '生成失败(无沙箱)';
    return;
  }
  try {
    // 1. 按所选纹理算法逐像素织纹
    const texture = TEXTURES[this.textureIdx];
    const total = CANVAS_SIZE * CANVAS_SIZE;
    const buf = new ArrayBuffer(total * 4);
    const pixels = new Uint32Array(buf);
    for (let i = 0; i < total; i++) {
      const row = Math.floor(i / CANVAS_SIZE);
      const col = i % CANVAS_SIZE;
      pixels[i] = pixelColor(row, col, texture.key, WEBP_PALETTE);
    }
    // 2. ArrayBuffer → PixelMap
    const opts: image.InitializationOptions = {
      size: { width: CANVAS_SIZE, height: CANVAS_SIZE },
      pixelFormat: image.PixelMapFormat.RGBA_8888
    };
    const pm = await image.createPixelMap(buf, opts);
    this.pixelMap = pm;
    // 3. PixelMap → WebP 编码
    const packer = image.createImagePacker();
    const webpBuf = await packer.packToData(pm, { format: 'image/webp', quality: WEBP_QUALITY });
    await packer.release();
    // 4. 落盘沙箱 filesDir
    const path = `${dir}/pattern_sample.webp`;
    const file = fileIo.openSync(path,
      fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
    fileIo.writeSync(file.fd, webpBuf);
    fileIo.closeSync(file);
    this.webpPath = path;
    this.genState = `已生成 ${(webpBuf.byteLength / 1024).toFixed(1)}KB`;
  } catch (e) {
    const err = e as BusinessError;
    this.genState = `生成失败(${err.code})`;
  }
}

生成链路分四步。第一步逐像素织纹——96 乘 96 共 9216 个像素,每个像素通过 pixelColor(row, col, texture.key, WEBP_PALETTE) 计算颜色值写入 Uint32ArrayArrayBuffer 总大小为 9216 乘 4 字节(RGBA8888)。第二步 createPixelMapArrayBuffer 转为 PixelMap,像素格式指定 RGBA_8888。第三步 createImagePacker 编码为 WebP 格式,质量固定 90,编码后 packer.release() 释放资源。第四步 fileIo.openSyncREAD_WRITE|CREATE|TRUNC 模式打开沙箱文件写入 WebP 字节流,TRUNC 标志确保每次生成覆盖旧文件。webpPath 保存路径供元数据 Tab 读写使用,genState 显示生成状态和文件大小。

工坊 UI 包含参数说明卡(画布/质量/纹理三格)、纹理五选一横滚 chips、PixelMap 预览区(判空 + 非空断言)、生成按钮和沙箱落盘状态卡。预览区使用 if (this.pixelMap !== undefined) 判空渲染——非空时 Image(this.pixelMap!) 展示像素画,空时展示占位卡。

9.5 Tab4 元数据:五字段读写回读全链路

元数据 Tab 是特性 C 的下游环节,对工坊生成的 WebP 文件执行读取→写入→回读校验三步操作。

读取方法 readMeta 调用 readImageMetadataByType 获取五字段快照:

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

readImageMetadataByType 的第一个参数是元数据类型数组(此处仅 WEBP_METADATA),第二个参数 index 是帧索引(静态 WebP 取 0)。返回的 meta.webPMetadata 的五个字段全部可选,因此每个字段都通过 ?? -1 兜底为"未提供"标志值。ImageSource 用后必须 release(),文件句柄必须 closeSync()

写入方法 writeMeta 构造 WebPMetadata 对象字面量并 as 断言后写回:

async writeMeta() {
  const file = fileIo.openSync(this.webpPath, fileIo.OpenMode.READ_WRITE);
  const source = image.createImageSource(file.fd);
  const webpMeta = {
    canvasWidth: CANVAS_SIZE,
    canvasHeight: CANVAS_SIZE,
    delayTime: this.writeDelay,
    unclampedDelayTime: this.writeDelay,
    loopCount: this.writeLoop
  } as image.WebPMetadata;
  const meta: image.ImageMetadata = { webPMetadata: webpMeta };
  await source.writeImageMetadata(meta);
  await source.release();
  fileIo.closeSync(file);
  // 写入完成后立即回读校验
  await this.verifyRead();
}

写入采用官方样例的对象字面量 as 断言模式——canvasWidthcanvasHeight 固定为 96,delayTimeunclampedDelayTime 均取 writeDelay 值,loopCountwriteLoop 值。ImageMetadata 是外层包装类型,webPMetadata 字段指向 WebPMetadata 对象。写入完成后立即调用 verifyRead 执行回读校验。

回读校验方法 verifyRead 重新创建 ImageSource 二次读取并比对:

async verifyRead() {
  const file = fileIo.openSync(this.webpPath, fileIo.OpenMode.READ_WRITE);
  const source = image.createImageSource(file.fd);
  const meta = await source.readImageMetadataByType([image.MetadataType.WEBP_METADATA], 0);
  const webp = meta.webPMetadata;
  this.verifySnapshot = new WebpMetaSnapshot(
    webp?.canvasWidth ?? -1, webp?.canvasHeight ?? -1,
    webp?.delayTime ?? -1, webp?.unclampedDelayTime ?? -1,
    webp?.loopCount ?? -1);
  await source.release();
  fileIo.closeSync(file);
  const ok = this.verifySnapshot!.delayTime === this.writeDelay
    && this.verifySnapshot!.loopCount === this.writeLoop;
}

回读校验重建 ImageSource(而非复用写入时的 source),确保读取的是写入后的文件状态。比对逻辑检查 delayTimeloopCount 是否与写入值一致——ok 为真时日志记录"已生效",为假时记录差异值。回读快照卡使用苔绿描边高亮(highlight=true)与读取快照卡的默认描边区分。

元数据 UI 包含读取区(标题 + 读取按钮)、读取快照卡(判空渲染)、写入控制台(帧延迟 chips + 循环 chips + 写入按钮)、回读校验卡(高亮描边)和操作日志流。metaCard Builder 接受 highlight 参数控制描边颜色——true 为苔绿(回读校验卡),false 为米灰(读取快照卡)。五字段速查卡列出 canvasWidth/canvasHeight/delayTime/unclampedDelayTime/loopCount 的语义说明。

9.6 Tab5 我的:守艺人中心

我的 Tab 展示用户个人中心,包含渐变会员大卡、创作任务清单和版本信息三段。

会员大卡使用 135 度 linearGradient 从朱砂到深朱砂的渐变背景,白字呈现高对比。卡内包含守艺人等级(LV6 金纹匠)、三列战绩(收藏纹样 38 套/累计下载 1260 次/守艺工分 8920 分)和年度共建任务进度。三列战绩通过 statBig Builder 渲染——每格深蓝底(blueD)白字,数值使用 monospace 等宽字体,右侧呼吸圆点随 breath 透明度交替。创作任务清单 6 行,已完成绿徽/待办金徽通过 task.done 布尔值区分。

十、图表卡片详解

10.1 Canvas 环形图绘制

drawPie 方法是 Canvas 绘制的核心,实现五扇区环形图、中心镂空和中心文字:

drawPie() {
  const ctx = this.pieCtx;
  const size = 210;
  const cx = size / 2;
  const cy = size / 2;
  const r = 66 + (this.breath ? 4 : 0);  // 呼吸微动半径
  ctx.clearRect(0, 0, size, size);
  // 外圈呼吸描边
  ctx.globalAlpha = 0.16;
  ctx.beginPath();
  ctx.arc(cx, cy, r + 10, 0, Math.PI * 2);
  ctx.strokeStyle = COLORS.red;
  ctx.lineWidth = 2;
  ctx.stroke();
  ctx.globalAlpha = 1;
  // 五扇区(从 12 点方向顺时针)
  let start = -Math.PI / 2;
  for (let i = 0; i < PIE_DATA.length; i++) {
    const angle = (PIE_DATA[i].val / 100) * Math.PI * 2;
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.arc(cx, cy, r, start, start + angle);
    ctx.fillStyle = PIE_COLORS[i];
    ctx.fill();
    // 扇区百分比标注
    const mid = start + angle / 2;
    ctx.fillStyle = COLORS.onMain;
    ctx.font = 'bold 10px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(`${PIE_DATA[i].val}%`, cx + Math.cos(mid) * r * 0.72, cy + Math.sin(mid) * r * 0.72 + 3);
    start += angle;
  }
  // 中心镂空(环形图)
  ctx.beginPath();
  ctx.arc(cx, cy, r * 0.58, 0, Math.PI * 2);
  ctx.fillStyle = COLORS.card;
  ctx.fill();
  // 中心文字(馆藏总量)
  ctx.fillStyle = COLORS.title;
  ctx.font = 'bold 20px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(`${PIE_TOTAL}`, cx, cy + 1);
  ctx.fillStyle = COLORS.sub;
  ctx.font = '10px sans-serif';
  ctx.fillText('馆藏纹样件', cx, cy + 17);
}

绘制流程分四步。第一步清底重画 clearRect,然后绘制外圈呼吸描边——透明度设为 0.16 后绘制 r + 10 半径的描边圆,绘制后立即 globalAlpha = 1 复位,确保不污染后续扇区绘制。第二步五扇区从 12 点方向(-Math.PI / 2)顺时针绘制,每个扇区先 moveTo(cx, cy) 移到圆心再 arc 画弧,fill() 填充扇区颜色,然后在扇区中线方向 r * 0.72 处用 fillText 标注百分比。第三步中心镂空——绘制半径 r * 0.58 的填充圆,颜色取 COLORS.card(纯白),覆盖扇区中心形成环形图。第四步中心文字——馆藏总量 1280 用 20px 粗体墨褐色居中显示,下方"馆藏纹样件"用 10px 驼褐色。

呼吸微动通过 r = 66 + (this.breath ? 4 : 0) 实现——breath 为真时半径增大 4 像素,扇区、描边圆和镂空圆同步缩放,形成每秒一次的呼吸效果。Canvas 组件通过 .onReady(() => { this.drawPie(); }) 首绘,之后由 setInterval 每秒调用 drawPie 重绘。

10.2 月度柱状图

柱状图采用 Column + ForEach 传统实现,不使用 Canvas:

@Builder
chartCard() {
  Column({ space: 10 }) {
    Row({ space: 6 }) {
      ForEach(MONTH_IDX, (i: number) => {
        Column({ space: 4 }) {
          Text(`${DOWNLOAD_VAL[i]}`).fontSize(8).fontColor(COLORS.text3)
            .fontFamily('monospace')
          Column()
            .width('64%')
            .height(this.barHeight(i))
            .borderRadius(4)
            .linearGradient({
              angle: 180,
              colors: [[COLORS.red, 0], [COLORS.redD, 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)
  }
}

每根柱子由三部分组成:顶部数值(8px 等宽字体)、柱体(Column 宽 64% 高度由 barHeight 计算)、底部月份标签(9px 驼褐色)。柱体使用 180 度 linearGradient 从朱砂到深朱砂的垂直渐变,模拟漆器质感。barHeight 方法实现呼吸波动:

barHeight(i: number): number {
  const base = DOWNLOAD_VAL[i] / BAR_MAX * 96;
  const wave = (i % 2 === 0) === this.breath ? 1.06 : 0.94;
  return Math.max(8, Math.round(base * wave));
}

柱高基准值为 DOWNLOAD_VAL[i] / 640 * 96(满刻度 640 件对应 96 像素高度),波动系数 wave 根据 i % 2 === 0breath 的异或关系在 1.06 和 0.94 间交替——偶数柱在 breath 为真时放大 6%,奇数柱反之,形成奇偶交替的波浪效果。Math.max(8, ...) 确保最小高度 8 像素。ForEach 的键值包含 this.breath——bar_${i}_${this.breath}breath 翻转后键值变化触发 ForEach 重新渲染柱体高度。

十一、底部 Tab 栏

底部导航栏采用自绘单排布局:

@Builder
tabBar() {
  Row() {
    ForEach(TAB_LIST, (tab: TabMeta, index: number) => {
      Column({ space: 3 }) {
        Text(tab.icon).fontSize(17)
        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)
  .border({ width: { top: 1 }, color: COLORS.line })
}

6 个 Tab 各占 layoutWeight(1) 等宽分布,选中态由 currentTab === index 判断——选中时标签朱砂色(tabOn),未选中时浅驼色(text3)。图标统一 17px,标签 9px。Tab 栏背景为纯白卡片色,顶部 1 像素米灰分割线。onClick 直接赋值 currentTab 触发内容区 if-else 链切换,无过渡动画。

十二、弹窗系统

弹窗系统采用"全屏遮罩 + 底部面板"的统一模式,三态弹窗共用一个 modalOverlay 外壳。

12.1 弹窗遮罩层

@Builder
modalOverlay(onClose: () => void) {
  Column() {
    // 空白遮罩区(点击关闭弹窗)
    Column().width('100%').layoutWeight(1)
      .onClick(() => { onClose(); })
    // 弹窗面板(按激活标志三选一)
    if (this.addModal) {
      this.panelAdd(onClose)
    } else if (this.editModal) {
      this.panelEdit(onClose)
    } else if (this.delModal) {
      this.panelDel(onClose)
    }
  }.width('100%').height('100%').backgroundColor(COLORS.mask)
  .justifyContent(FlexAlign.End)
}

遮罩层使用墨褐半透背景色(rgba(59,47,37,0.55)),Column 纵向布局将空白区 layoutWeight(1) 填满上方空间,面板贴底弹出。空白区 onClick 调用 onClose 回调关闭弹窗。面板通过 if-else 链三选一渲染,onClose 作为函数参数传入各面板供"取消"按钮调用。

12.2 三态弹窗面板

新建弹窗 panelAdd 包含标题、说明文字、纹样名输入框和取消/收藏按钮:

@Builder
panelAdd(onClose: () => void) {
  Column({ space: 12 }) {
    Text('新建纹样收藏').fontSize(15).fontWeight(FontWeight.Bold)
      .fontColor(COLORS.title)
    Text('输入纹样名后会置顶到素材列表首位,朝代跟随当前筛选,品类默认回纹')
      .fontSize(10).fontColor(COLORS.text3).width('100%')
    TextInput({ placeholder: '输入纹样名,如:北魏忍冬纹' })
      .fontSize(12).height(40)
      .fontColor(COLORS.title)
      .placeholderColor(COLORS.text3)
      .backgroundColor(COLORS.chip)
      .onChange((value: string) => { this.inputText = value; })
    Row({ space: 10 }) {
      Button('取消').fontSize(12).height(38).borderRadius(10)
        .fontColor(COLORS.sub).backgroundColor(COLORS.chip)
        .layoutWeight(1)
        .onClick(() => { onClose(); })
      Button('收藏').fontSize(12).height(38).borderRadius(10)
        .fontColor(COLORS.onMain).backgroundColor(COLORS.red)
        .layoutWeight(1)
        .onClick(() => { this.confirmAdd(); })
    }.width('100%')
  }.padding(16).borderRadius({ topLeft: 16, topRight: 16 })
  .backgroundColor(COLORS.card).width('100%')
}

面板顶部圆角 borderRadius({ topLeft: 16, topRight: 16 }) 使面板从底部弹出时视觉自然过渡。confirmAdd 方法将输入的纹样名 unshift 置顶素材列表,朝代跟随当前筛选(全部时默认唐),品类默认回纹,适配度 82 分:

confirmAdd() {
  if (this.inputText.trim() !== '') {
    const dynasty = this.activeDynasty === '全部' ? '唐' : this.activeDynasty;
    this.patternList.unshift(new PatternItem(this.inputText.trim(), dynasty, '回纹',
      '新建收藏纹样 · 待匠人补全用途与适配说明', 82));
  }
  this.closeAllModals();
}

编辑弹窗 panelEdit 与新建弹窗结构类似,但 TextInput 使用 text 参数(而非 placeholder)带出当前用途描述,confirmEdit 通过 editIdx 定位源列表条目修改 uses 字段。删除确认弹窗 panelDel 不含输入框,仅显示删除提示和取消/删除按钮,删除按钮使用深朱砂色(redD)强化危险操作视觉警示,confirmDel 通过 splice(delIdx, 1) 删除指定条目。

十三、功能模块对比表

功能模块核心技术关键 API / 方法数据模型交互特性
头部 Banner渐变背景 + 状态胶囊linearGradient / Circlebreath / nestedMode / genState三特性状态汇聚展示
素材馆 Tab双列卡片 + Canvas 环形图 + 柱状图drawPie / ctx.arc / ForEachPatternItem / PieData朝代筛选 + 增删改弹窗
频道 Tab双层 Tabs 嵌套滚动nestedScroll / barModeChannelItem / InnerCardSELF_FIRST / SELF_ONLY 双模式切换
日志 Tab翻页事件时间轴List / ForEachSwipeLog固定行高 72 + 双色层徽标
工坊 Tab像素画编码 WebP 落盘createPixelMap / packToDataTextureItem / PixelMap纹理五选一 + 沙箱路径展示
元数据 Tab五字段读写回读全链路readImageMetadataByType / writeImageMetadataWebpMetaSnapshot / MetaOpLog读取快照 + 写入控制台 + 回读校验
我的 Tab渐变会员卡 + 任务清单linearGradient / ForEachMineTask三列战绩 + 呼吸圆点
弹窗系统全屏遮罩 + 底部面板modalOverlay / panelAdd / panelEdit / panelDelinputText / editIdx / delIdx三态统一 + 空白关闭
Canvas 绘制环形图呼吸微动setInterval / drawPie / globalAlphaPIE_DATA / PIE_COLORS半径随 breath ±4 像素
柱状图Column + ForEach 传统柱状linearGradient / barHeightDOWNLOAD_VAL / MONTH_IDX奇偶柱交替 ±6% 波动

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

布局方式与数据流

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

页面根结构通常由头部、内容区和底部 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 的 Canvas 绘制、Tabs 嵌套滚动和 ImageKit WebP 元数据三大前沿特性,构建了一个从素材浏览、分类深挖、行为日志、创作工坊到元数据追溯的完整业务闭环。

在技术架构层面,平台展示了三个关键设计决策。其一,breath 布尔值作为跨 Tab 共享的呼吸驱动源,同时驱动 Canvas 环形图半径微动和柱状图柱高波动,体现了状态变量的复用思维。其二,nestedScroll 挂载点精准定位于被嵌套的内层 Tabs,通过 SELF_FIRST/SELF_ONLY 双模式切换演示了嵌套滚动的行为差异,每次翻页均通过 onChange 回调记入滑动日志时间轴,使抽象的滚动行为可视化。其三,WebP 元数据全链路采用"读取→写入→回读校验"三步法,五字段全部可选值的处理统一使用 ?? -1 兜底,重建 ImageSource 的二次读取确保校验结果可信。

在色彩体系层面,宣纸米白底色、朱砂主题色、黛蓝朝代徽标、鎏金优选档位、苔绿已生效态的五色体系,既保持了非遗典藏的视觉气质,又实现了品类与色彩的稳定映射。环形图五扇区配色直接取自主题色板,工坊像素画五色色板与应用主题统一,确保全站色彩语言的连贯性。

在工程细节层面,本平台还体现了多项值得借鉴的实践。预计算数组模式是其中之一:filteredPatternspatternPairs 两个方法将筛选逻辑和双列切块逻辑从 ForEach 渲染回调中剥离,在渲染前预计算结果数组,避免在每次列表项渲染时重复执行 filter 操作,显著降低大列表的渲染开销。indexOfPattern 方法解决了筛选列表与源列表索引对齐问题——当用户在筛选后的列表中点击编辑或删除按钮时,需要找到对应条目在原始 patternList 中的真实索引,通过引用相等性比较(this.patternList[i] === item)精确定位,确保操作作用于正确的数据实体。此外,ForEach 的键值生成器设计也颇具匠心:双列卡片以 pair_${pi}_${pair[0].name} 为键保证行级唯一性,单卡片以 card_${item.name}_${item.score} 为键包含评分字段,当编辑操作修改评分时键值变化触发卡片重渲染,实现了字段级精确更新。

展望未来,平台可在以下方向持续演进。第一,引入更复杂的纹样矢量数据模型,支持 SVG 路径绘制与缩放预览,替代当前的像素画纹理算法,使纹样在不同分辨率下保持矢量精度。第二,扩展元数据支持 EXIF 和 PNG 元数据类型,通过 MetadataType 枚举扩展实现跨格式的元数据统一读写,构建多格式兼容的纹样元数据中心。第三,接入 AI 纹样生成能力,通过用户描述自动生成纹样像素画,降低创作门槛,使非遗纹样从"复刻存档"走向"智能衍生"。第四,增加纹样溯源链路,将元数据写入扩展为区块链存证,实现非遗纹样的可信追溯与版权保护。第五,优化嵌套滚动体验,引入物理阻尼动画和边缘回弹效果,使"先内后外"的接力过程更自然流畅,同时在 onChange 回调中叠加速度检测实现快速滑动时的惯性翻页。第六,将 Canvas 环形图从静态数据扩展为实时网络数据驱动,配合 @Observed 数据模型实现馆藏占比的动态更新,使运营看板与素材库保持数据同步。随着 HarmonyOS 生态的持续演进,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 应用的功能开发。


Logo

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

更多推荐