茶器艺科智造HarmonyOS应用实战-23-六个标签和六个presetKey靠索引对齐,新增杯型为何容易串位:改成类型化配置表

先设想一个尚未在当前工程执行的扩展场景:产品计划在“方盏”和“高足杯”之间加入“水波杯”。如果开发者只在标签数组插入一个中文名,界面会多出按钮;点击它却可能套用原高足杯位置的参数,后面的项目也可能拿到错误的 presetKey。重启后表现还可能变化,因为草稿保存的是索引,Web 端又用另一套 key 分支选择轮廓。这个场景用于暴露索引耦合风险,不代表当前源码已经加入水波杯或完成过运行验证。

指定工程现在有六个 PRESET_LABELS 和六个 CUP_PRESET_KEYS,注释要求“一一对应”。HSP 的按钮、applyPreset(i)、深链 key 反查、对话关键词、订单名称和 3D 同步都依赖这个数字 i;ArkWeb 内嵌 HTML 再根据 presetKey 走自己的轮廓分支。任何一处插入、重排或漏改,类型系统都看不见数组之间的关系。

用类型化配置表固定杯型身份

本文将完成四项工程收口:

  1. 沿源码还原 label、key、默认参数、对话与 Web 轮廓的真实依赖链。
  2. 用一个 CupPresetSpec 同时承载稳定 key、标签、默认参数和几何类型。
  3. 让 UI、深链、草稿与 Web 都按 key 查表,不再传递裸索引。
  4. 单独建立工艺参数证据边界:配置一致不等于尺寸与轮廓已经获得工艺认可。

一、两组数组的等长只是约定,不是类型关系

libraryhar/src/main/ets/model/ChaqiModels.ets:56 与 :72 当前分别声明:

export const PRESET_LABELS: string[] = [
  '罗汉杯', '悟空杯', '方盏', '高足杯', '螺旋瓶', '竹节杯'
];

export type CupPresetKey =
  'luohan' | 'wukong' | 'fang' | 'zhujie' | 'spiral' | 'generic';

export const CUP_PRESET_KEYS: CupPresetKey[] = [
  'luohan', 'wukong', 'fang', 'generic', 'spiral', 'zhujie'
];

第四项最能说明问题:标签是“高足杯”,key 却是 generic。这不一定是错误。源码注释明确说高足杯、悟空杯和螺旋瓶走通用轮廓;它们可通过不同默认尺寸形成不同外观。真正风险是这种语义只存在于数组位置和注释中。编译器能保证 key 属于联合类型,却不能保证 PRESET_LABELS[3] 就应配 CUP_PRESET_KEYS[3]。

数组长度相等也不足以证明对齐。两边各六项但顺序不同,程序仍能编译;若只加标签不加 key,取值还可能成为 undefined,再被 Web 的 p.presetKey || 'luohan' 回退成罗汉杯。

二、索引从按钮一路传播到参数、订单和深链

HSP 的 ChaqiExperiencePage.ets 让 ForEach(PRESET_LABELS) 把 index 交给 applyPreset(index)。该函数用 if/else 按 0–5 写入五个尺寸:

private applyPreset(i: number): void {
  this.presetIndex = i;
  if (i === 0) {
    this.heightMm = 70; this.diameterMm = 85;
    this.mouthMm = 85; this.bottomMm = 41; this.waistPct = 45;
  } else if (i === 1) {
    this.heightMm = 88; this.diameterMm = 76;
    this.mouthMm = 68; this.bottomMm = 34; this.waistPct = 58;
  }
  // 2 方盏、3 高足、4 螺旋瓶、5 竹节杯继续分支
  this.syncCupLatheFromUi();
}

syncCupLatheFromUi 又执行 const k = CUP_PRESET_KEYS[this.presetIndex]。切片下单使用 PRESET_LABELS[this.presetIndex] 拼订单名;深链 presetKey 通过遍历 CUP_PRESET_KEYS 反查 index;对话关键词则直接写 applyPreset(4)、applyPreset(5) 等数字。

这条链共有六个隐式索引消费者:按钮显示、参数赋值、模型 key、订单名称、深链反查、对话快捷规则。新增杯型不是改两个数组,而是必须同步审查所有消费者。

标签索引穿过页面、路由、草稿和Web的当前链路

三、Web 分支再次解释 key,形成第二份映射

ArkTS 的 CupLatheSilhouette.buildLatheProfilePoints 对 zhujie/luohan/fang 走专用函数,其他 key 走 generic。libraryhsp/.../rawfile/cup3d/index.html:342-364 也有一份近似逻辑,另外保留了 round:

function generateCupProfileMm(p) {
  var key = p.presetKey || 'luohan';
  if (key === 'zhujie') return generateZhujieProfile(/* ... */);
  if (key === 'luohan') return generateLuohanProfile(/* ... */);
  if (key === 'round') return generateWaterWaveProfile(/* ... */);
  if (key === 'fang') return generateFangProfile(/* ... */);
  return generateGenericProfile(/* ... */);
}

因此“key 被正确传到 Web”仍不等于“Web 知道新 key 的几何含义”。新增 waterwave 后,若只改 ArkTS 联合类型和数组,Web 会落到 generic;若只在 Web 加 round,HAR 联合类型与深链白名单又可能不承认它。

更稳的做法是区分杯型身份与几何算法类型。高足、悟空、螺旋瓶可以拥有不同稳定 key,但共同声明 profileKind: 'generic'。Web 接收经过白名单化的 profileKind,不再自行猜业务 key 对应哪种几何。

四、类型化配置表把同一杯型的字段放在同一行

建议在 HAR 定义一个配置接口。默认尺寸和算法类型与 key 同处一个对象,插入新杯型时不会让平行数组错位:

export type CupProfileKind = 'luohan' | 'fang' | 'zhujie' | 'generic';

export interface CupPresetDefaults {
  heightMm: number;
  diameterMm: number;
  mouthMm: number;
  bottomMm: number;
  waistPct: number;
}

export interface CupPresetSpec {
  readonly key: CupPresetKey;
  readonly label: string;
  readonly profileKind: CupProfileKind;
  readonly defaults: CupPresetDefaults;
  readonly aliases: readonly string[];
}

配置表示例可按当前源码值迁移,而不是趁重构改数字:

export const CUP_PRESETS: readonly CupPresetSpec[] = [
  {
    key: 'luohan', label: '罗汉杯', profileKind: 'luohan',
    defaults: {
      heightMm: 70, diameterMm: 85, mouthMm: 85,
      bottomMm: 41, waistPct: 45
    },
    aliases: ['罗汉杯', '罗汉']
  },
  {
    key: 'generic', label: '高足杯', profileKind: 'generic',
    defaults: {
      heightMm: 88, diameterMm: 72, mouthMm: 64,
      bottomMm: 36, waistPct: 52
    },
    aliases: ['高足杯', '高足']
  }
];

这里暴露了当前模型里的身份问题:generic 被用作高足杯 key,同时注释又称它为“其他通用杯型”。若未来需要多个通用轮廓预设,稳定身份应新增 gaozu,profileKind 仍为 generic,并为旧草稿做迁移。不要直接把现有 key 改名后让持久化数据失联。

五、页面按 spec 应用,不再维护数字 if/else

查找函数集中处理 key,UI 与路由都复用它:

export function presetByKey(key: CupPresetKey): CupPresetSpec | null {
  for (let i = 0; i < CUP_PRESETS.length; i++) {
    if (CUP_PRESETS[i].key === key) {
      return CUP_PRESETS[i];
    }
  }
  return null;
}

private applyPreset(spec: CupPresetSpec): void {
  this.selectedPresetKey = spec.key;
  this.heightMm = spec.defaults.heightMm;
  this.diameterMm = spec.defaults.diameterMm;
  this.mouthMm = spec.defaults.mouthMm;
  this.bottomMm = spec.defaults.bottomMm;
  this.waistPct = spec.defaults.waistPct;
  this.syncCupLatheFromUi();
}

按钮遍历 CUP_PRESETS,点击直接传 spec;订单名使用当前 spec.label;深链使用 presetByKey;对话匹配 aliases 后传 spec。数字 index 只可作为 UI 遍历位置,不能再充当持久身份。

ForEach(CUP_PRESETS, (spec: CupPresetSpec) => {
  Button(spec.label)
    .onClick(() => this.applyPreset(spec));
}, (spec: CupPresetSpec) => spec.key)

稳定 key 也适合做 ForEach 身份键。新增或重排显示顺序不会让组件身份跟着数组位置漂移。

六、草稿应保存 key,旧 presetIndex 需要显式迁移

当前 ChaqiDraftState 保存 presetIndex,load/save 都把它夹在 0–5。即使运行时配置表完全正确,重排数组仍会改变旧索引含义。建议新 schema 保存 presetKey,读取旧版本时用冻结的历史映射迁移:

const V1_PRESET_KEYS: readonly CupPresetKey[] = [
  'luohan', 'wukong', 'fang', 'generic', 'spiral', 'zhujie'
];

function migratePresetIndex(index: number): CupPresetKey {
  if (index >= 0 && index < V1_PRESET_KEYS.length) {
    return V1_PRESET_KEYS[index];
  }
  return 'luohan';
}

历史映射必须固定,不能引用会继续变化的 CUP_PRESETS。迁移完成后写新 schema;若遇到未知 key,记录问题并采用受控默认。第 06 篇已经讨论 schemaVersion 读取缺口,这里只强调预设身份不要继续依赖位置。

七、把 profileKind 明确传给 Web,减少两端分支分叉

CupWeb3d 当前把 presetKey 传入 rawfile。建议由 native 配置表解析出 profileKind,同时传 key 用于诊断与身份,Web 只对白名单 geometry 分派:

const spec = presetByKey(this.selectedPresetKey);
const command: CupWebCommand = {
  presetKey: this.selectedPresetKey,
  profileKind: spec?.profileKind ?? 'generic',
  heightMm: this.heightMm,
  diameterMm: this.diameterMm,
  mouthMm: this.mouthMm,
  bottomMm: this.bottomMm,
  waistPct: this.waistPct,
  waveLevel: this.waveLevel
};
function generateCupProfileMm(p) {
  var kind = normalizeProfileKind(p.profileKind);
  if (kind === 'zhujie') return generateZhujieProfile(/* ... */);
  if (kind === 'luohan') return generateLuohanProfile(/* ... */);
  if (kind === 'fang') return generateFangProfile(/* ... */);
  return generateGenericProfile(/* ... */);
}

Web 仍要对 profileKind 做白名单化,因为桥接消息是运行时输入。ArkTS 切片轮廓也应使用同一个 profileKindForPreset 结果,确保 3D 与切片分派一致。若确实要保留 round,应把它提升为双方正式的 CupProfileKind,而不是只留在 HTML 的隐藏分支。

类型化预设表驱动UI、路由、草稿与Web几何

八、结构一致不等于工艺参数正确

类型化表能证明“罗汉杯标签与 luohan key、当前默认参数和 luohan 算法被绑在一起”,却不能证明 heightMm=70、diameterMm=85 或腹部公式符合真实制陶工艺。当前源码注释使用“阔腹底稳”“细高”“黄金档案最优参数”等描述,但项目内没有看到参数来源、测量样本、陶艺师签审、打印收缩率、材料批次或烧成结果记录。

因此每个 spec 还需要一份独立证据账本,而不是在代码注释里写“已验证”:

证据项可接受材料当前源码能否证明
尺寸来源实物测量记录、设计图、版本号不能
轮廓合理性专家复核、采样点对照不能
可打印性切片器参数与结果文件不能
材料收缩材料批次、打印与烧成前后测量不能
UI/切片/3D一致同输入点集或截图对照只能看到同源意图,未运行

配置重构应先原样迁移现有数值,避免把“修接线”和“改工艺”混成一次变更。后续若证据支持调参,应单独提交 spec 数值变化并绑定证据编号。

九、回归矩阵、故障排查与证据边界

it('keeps every preset key unique', 0, () => {
  const seen: string[] = [];
  CUP_PRESETS.forEach((spec: CupPresetSpec) => {
    expect(seen.includes(spec.key)).assertFalse();
    seen.push(spec.key);
    expect(spec.label.length > 0).assertTrue();
  });
});

it('resolves every route key to one spec', 0, () => {
  CUP_PRESETS.forEach((spec: CupPresetSpec) => {
    expect(presetByKey(spec.key)?.label).assertEqual(spec.label);
  });
});
场景静态/单元预期集成观察
新增杯型一个 spec 完成 key/label/default/profile按钮、订单、深链同名
重排显示顺序key 与默认参数不变旧草稿仍选同一 key
高足杯身份 key 与 generic profile 分离3D/切片均走 generic
未知深链 key查表失败并留问题码不切到错误预设
未知 Web kind白名单回退并记录不执行任意分支
旧 presetIndex用 V1 固定表迁移重启前后身份一致
现象首查常见根因修正
按钮名对、形状错spec.profileKind 与桥接消息Web 仍按旧 key 猜显式传 geometry 类型
新杯型后旧草稿变杯型是否仍存 index显示顺序成为身份迁移并保存稳定 key
对话选错杯型是否还调用 applyPreset(数字)硬编码索引未清aliases 查 spec
切片与 3D 分叉两端算法分派Web/ArkTS 各维护 key switch共享 profileKind 语义
编译通过但标签串位是否仍有平行数组类型只约束单数组一行一个 CupPresetSpec
参数被称为工艺最优是否有外部证据编号把注释当验证单独建立工艺证据账本

当前源码能确认六组标签、六组 key、按索引的 applyPreset、深链反查、对话数字索引、presetIndex 草稿和 Web/ArkTS 分支;也能确认高足等 key 会走 generic 轮廓。本文的 CUP_PRESETS、稳定 key 迁移、profileKind 桥接和测试是建议实现,没有修改只读项目。本文未运行单测、HAP 构建、ArkWeb、切片、真机或打印流程,也没有陶艺参数来源材料;因此不能声称新增配置已生效,更不能把现有尺寸称为经工艺验证的最优值。

Logo

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

更多推荐