:产品载体模型:从固定商品到自定义载体)
一、载体不是一张背景图而是一组稳定契约在纹样创作工坊里陶瓷杯、包装盒、丝巾看起来只是几张可点击卡片真正进入生成与导出链路后它们却分别决定画布比例、预览方式、可选格式和三维资源。若页面只保存一个中文名称后续模块就只能继续写字符串分支新增手机壳或自定义照片时选择页、预览页、导出页都要同步修改遗漏任意一处都会产生“能选中但不能生成”的半成品状态。因此载体模型要回答四个问题用什么稳定 ID 跨页面传递预览画布采用什么尺寸允许导出哪些格式固定载体和用户照片如何进入同一条下游链路。界面只消费这个契约不负责猜测业务能力。契约字段固定载体示例自定义载体示例下游用途idcup、box、silkcustom选中态、接续快照、组件标识canvasW/canvasH杯体 400×500默认 400×400预览比例与导出尺寸exportFormatsPNG、STL、OBJ、FBXPNG、JPG、SVG、PDF过滤格式芯片和选择生成器glbPath指向对应三维资源空字符串判断是否有固定三维模型二、用统一 Product 契约收口固定项和自定义项固定载体适合放在只读目录中自定义载体则作为一项具有特殊输入来源的领域对象。两者共享Product接口页面和导出服务因此可以继续调用getProductById()只有涉及图片 URI 或渲染实现时才通过isCustomCarrier()进入专用分支。export type ProductId | cup | box | silk | badge | bookmark | case | fabric | cloisonne | helmet | custom export interface Product { id: string name: string desc: string canvasW: number canvasH: number exportFormats: string[] glbPath: string } export const PRODUCTS: Product[] [ { id: cup, name: 陶瓷杯, desc: 马克杯 11oz / 15oz, canvasW: 400, canvasH: 500, exportFormats: [PNG, STL, OBJ, FBX], glbPath: gltf/carrier_cup.glb }, { id: box, name: 包装盒, desc: 天地盖盒 / 天地盖, canvasW: 400, canvasH: 400, exportFormats: [PNG, PDF, AI, SVG], glbPath: gltf/carrier_box.glb } ] export const CUSTOM_CARRIER: Product { id: custom, name: 自定义载体, desc: 拍照或选择物品照片, canvasW: 400, canvasH: 400, exportFormats: [PNG, JPG, SVG, PDF], glbPath: } export function getProductById(id: string): Product | undefined { return id custom ? CUSTOM_CARRIER : PRODUCTS.find((item: Product) item.id id) }id是跨模块契约不应使用数组下标。目录排序变化后下标会指向另一项稳定 ID 则能安全进入页面状态、接续参数和导出历史。exportFormats也不应在选择页复制一份因为界面展示和服务白名单一旦分离用户就可能选到服务无法处理的格式。三、选择组件只表达意图不直接推进业务阶段载体卡片需要区分“单击选中”和“确认进入下一步”。单击只改变selectedId让边框、背景色和确认按钮跟着状态刷新确认事件再交给父级页面决定是否推进流程。自定义入口额外发出onCustomCarrier但仍然复用同一个onSelect(custom)避免出现视觉选中项与领域状态不一致。Component export struct ProductSelector { Prop selectedId: string cup Prop hasCustomCarrier: boolean false onSelect: (id: string) void () {} onConfirm: (id: string) void () {} onCustomCarrier: () void () {} Builder ProductCard(product: Product) { Column() { Text(product.name) Text(画布 ${product.canvasW}×${product.canvasH}) if (this.selectedId product.id) { Button(确认载体) .onClick(() this.onConfirm(product.id)) } } .border({ width: this.selectedId product.id ? 2 : 1, color: this.selectedId product.id ? #8A3B1F : #DED6C9 }) .onClick(() this.onSelect(product.id)) } Builder CustomButton() { Row() { Text(自定义载体) if (this.selectedId custom this.hasCustomCarrier) { Text(已选择) } } .onClick(() { this.onSelect(custom) this.onCustomCarrier() }) } }这种事件边界让组件保持可复用它不知道工坊当前处于第几步也不负责清空导出格式。手机布局可用两列网格平板布局可增加列数和卡片高度而选择语义不随视觉结构变化。四、自定义载体必须经历“取得 URI—预览—确认”三态自定义载体不能在系统选择器返回前直接进入生成步骤。安全的状态顺序是初始态展示“拍照”和“从相册选择”取得非空 URI 后展示预览、名称、重选和确认只有用户点击确认父级页面才把产品阶段标记为完成。Component export struct CustomCarrierPanel { Link customCarrierUri: string Link customCarrierName: string onConfirm: (uri: string, name: string) void () {} private async openGallery(): Promisevoid { try { const options: photoPicker.PhotoSelectOptions { MIMEType: photoPicker.PhotoViewMIMETypes.IMAGE_TYPE, maxSelectNumber: 1 } const picker new photoPicker.PhotoViewPicker() const result: photoPicker.PhotoSelectResult await picker.select(options) if (result.photoUris.length 0) { return } this.customCarrierUri result.photoUris[0] this.customCarrierName 自定义_${this.formatTime()} } catch (error) { this.customCarrierUri this.customCarrierName this.showPickerError(String(error)) } } private confirm(): void { if (this.customCarrierUri.length 0) { return } this.onConfirm(this.customCarrierUri, this.customCarrierName) } }系统 Picker 的返回值是 URI不应先转换为不可跨会话复用的显示路径。华为开发者文档的使用 Picker 选择媒体库资源说明选择接口可由 UIAbility 拉起返回 URI 用于后续读取这也意味着组件需要保存 URI而不是只保存展示名称。取消选择、返回空数组和抛出异常都应保持在当前步骤并提供再次选择的入口。五、父级页面负责推进状态机和清理派生状态选择组件发出事件后页面把产品 ID 写入WorkshopStateModel。固定载体可以直接确认自定义载体在 URI 为空时只展开面板不允许跳到生成页。确认成功后还要清空旧的exportFormat因为上一种载体支持的格式不一定属于新载体。ProductSelector({ selectedId: this.workshop.productId, hasCustomCarrier: this.customCarrierUri.length 0, onSelect: (id: string) { this.workshop.selectProduct(id) this.syncContinuationSnapshot() }, onConfirm: (id: string) { if (id custom this.customCarrierUri.length 0) { this.showCustomCarrierPanel true return } this.workshop.selectProduct(id) this.workshop.confirmProductStep() this.exportFormat this.syncContinuationSnapshot() }, onCustomCarrier: () { this.showCustomCarrierPanel true } }) if (this.showCustomCarrierPanel) { CustomCarrierPanel({ customCarrierUri: $customCarrierUri, customCarrierName: $customCarrierName, onConfirm: (uri: string, name: string) { if (uri.length 0 || name.length 0) { return } this.showCustomCarrierPanel false this.workshop.selectProduct(custom) this.workshop.confirmProductStep() this.exportFormat this.syncContinuationSnapshot() } }) }selectProduct()与confirmProductStep()分开很重要前者表示“用户正在比较”后者表示“输入已经完整可以消费”。接续快照可以在选择变化后更新但恢复时仍要校验自定义 URI 是否可读只恢复productIdcustom而没有 URI会得到一个没有载体图的空预览。六、下游统一消费 Product渲染层只保留必要分支固定载体和自定义载体进入生成页后格式列表都来自getProductById(productId)。渲染差异只发生在素材来源固定项使用资源映射自定义项使用用户选出的 URI。组件标识继续由纹样 ID 和载体 ID 构成导出快照服务无需知道选择动作来自目录还是系统 Picker。const product getProductById(this.workshop.productId) if (product undefined) { this.showError(载体不存在请重新选择) return } this.availableFormats product.exportFormats if (!this.availableFormats.includes(this.exportFormat)) { this.exportFormat } if (isCustomCarrier(product.id)) { CustomCarrierRenderer({ carrierImageUri: this.customCarrierUri, patternId: this.workshop.patternId, aiImageUrl: this.aiImageUrl }) } else { CanvasRenderer({ patternId: this.workshop.patternId, productId: product.id, aiImageUrl: this.aiImageUrl }) }自定义项的glbPath为空所以不能假装拥有固定三维模型。若产品要求用户照片也进入三维材质流程需要额外定义模型模板、纹理坐标和裁切策略把二维照片直接标记为“支持 3D”会让能力标签与运行结果冲突。七、失败语义要落在可观察状态上故障点页面应保持的状态用户可执行动作禁止结果Picker 被取消仍停留在载体步骤再次拍照或选择自动进入生成页Picker 抛出异常清空临时 URI显示提示重试或改选固定载体吞掉异常后显示空预览自定义 URI 失效标记载体不可用重新选择图片继续导出零内容文件固定 ID 不存在不渲染详情返回载体目录使用陶瓷杯静默兜底切换载体后旧格式失效清空格式选中态重新选可用格式沿用上一载体的格式恢复快照缺少 URI回到自定义选择态补选图片只恢复custom字符串连续点击确认只推进一次阶段等待生成页稳定重复写历史或重复导出固定载体缺失时静默回退到陶瓷杯看似友好实际会掩盖数据错误用户选择包装盒最终却导出杯体模板。领域查询返回undefined页面展示可恢复错误能更早暴露目录、接续参数或历史记录中的非法 ID。八、用一张矩阵验收目录、自定义输入和下游消费场景操作预期 UI状态与数据检查固定载体选中点击包装盒卡片高亮包装盒并显示确认按钮productId为box仍停留在载体步骤固定载体确认点击确认载体进入生成步骤phase至少为RENDERED格式为空自定义入口点击自定义载体展开拍照和相册入口productId可为custom但未确认取消系统选择拉起 Picker 后返回仍显示空态面板URI 和名称均为空选择图片从相册选一张图片显示图片、名称、重选和确认URI 非空尚未推进步骤确认自定义载体点击确认载体进入自定义预览productId为customURI 可供渲染切换载体从陶瓷杯改为丝巾格式选中态被清空格式集合改为丝巾白名单非法 ID 恢复注入不存在的产品 ID显示重新选择提示不创建预览和导出任务返回后再进入完成一次选择后返回目录与已确认状态一致固定项恢复 ID自定义项同时校验 URI验收要同时观察卡片选中态、步骤指示器、预览内容和格式列表。只看到卡片变色不能证明确认阶段正确只看到图片预览也不能证明格式白名单已经切换。四个信号一致才说明产品模型真正贯通了 UI、状态机与服务。九、总结产品载体模型的关键不是把名称集中到一个数组而是建立可跨页面消费的能力契约。固定载体用稳定 ID、画布、格式和资源路径描述自定义载体复用同一接口同时补充用户选择的 URI。选择组件只表达意图父级页面控制确认和阶段推进渲染与导出服务按统一 Product 查询结果工作。这样新增载体时目录项、预览能力和导出白名单能在同一个模型里完成校验失败也会停在用户可以恢复的位置。