茶器艺科智造HarmonyOS应用实战-23-六个标签和六个presetKey靠索引对齐,新增杯型为何容易串位:改成类型化配置表
茶器艺科智造HarmonyOS应用实战-23-六个标签和六个presetKey靠索引对齐,新增杯型为何容易串位:改成类型化配置表
先设想一个尚未在当前工程执行的扩展场景:产品计划在“方盏”和“高足杯”之间加入“水波杯”。如果开发者只在标签数组插入一个中文名,界面会多出按钮;点击它却可能套用原高足杯位置的参数,后面的项目也可能拿到错误的 presetKey。重启后表现还可能变化,因为草稿保存的是索引,Web 端又用另一套 key 分支选择轮廓。这个场景用于暴露索引耦合风险,不代表当前源码已经加入水波杯或完成过运行验证。
指定工程现在有六个 PRESET_LABELS 和六个 CUP_PRESET_KEYS,注释要求“一一对应”。HSP 的按钮、applyPreset(i)、深链 key 反查、对话关键词、订单名称和 3D 同步都依赖这个数字 i;ArkWeb 内嵌 HTML 再根据 presetKey 走自己的轮廓分支。任何一处插入、重排或漏改,类型系统都看不见数组之间的关系。

本文将完成四项工程收口:
- 沿源码还原 label、key、默认参数、对话与 Web 轮廓的真实依赖链。
- 用一个
CupPresetSpec同时承载稳定 key、标签、默认参数和几何类型。 - 让 UI、深链、草稿与 Web 都按 key 查表,不再传递裸索引。
- 单独建立工艺参数证据边界:配置一致不等于尺寸与轮廓已经获得工艺认可。
一、两组数组的等长只是约定,不是类型关系
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 分支再次解释 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 的隐藏分支。

八、结构一致不等于工艺参数正确
类型化表能证明“罗汉杯标签与 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、切片、真机或打印流程,也没有陶艺参数来源材料;因此不能声称新增配置已生效,更不能把现有尺寸称为经工艺验证的最优值。
更多推荐



所有评论(0)