从一个 loading 到完整状态机:多模型图片编辑流程的前端建模

做一个最小图片生成页面时,const [loading, setLoading] = useState(false) 看起来完全够用:点击按钮设为 true,请求结束再设回 false

当同一入口开始承载背景处理、对象清理、增强、扩图、修复和风格转换后,这个布尔值很快失去表达能力。文件可能仍在解码,选项可能刚因模型切换而失效,任务可能已提交但响应丢失,结果可能已返回却仍等待人工复核。这些状态都不是简单的"加载中"。

先把可用能力当作服务端契约

多模型产品最容易出现的前端债务,是把规则写成大量条件分支:

ts 复制代码
if (model === "a") hide4K();
if (mode === "fusion") requireMoreImages();
if (!user) disablePremiumMode();

这些判断会逐渐与后端真实能力漂移。更合适的做法是让服务端返回版本化能力文档:

ts 复制代码
type Capability = {
  version: string;
  models: Array<{
    id: string;
    modes: Array<{
      id: string;
      minSources: number;
      maxSources: number;
      mimeTypes: string[];
      resolutions: string[];
      aspectRatios: string[];
      requiresAuth: boolean;
      creditCost: number;
    }>;
  }>;
};

界面根据当前"模型 + 模式"分支生成控件。切换父级选项后,仍合法的选择继续保留;失效的分辨率、宽高比或输入数量被替换,同时给出原因。这样既减少硬编码,也不会无声清空用户已经写好的提示词。

用可区分联合类型表达流程

状态至少需要覆盖输入校验、提交、处理、复核和终态:

ts 复制代码
type EditorState =
  | { type: "idle" }
  | { type: "validating_sources"; files: File[] }
  | { type: "ready"; draft: EditDraft }
  | { type: "submitting"; request: EditRequest }
  | { type: "processing"; jobId: string; request: EditRequest }
  | { type: "reviewing"; jobId: string; resultUrl: string; request: EditRequest }
  | { type: "completed"; resultUrl: string }
  | { type: "failed"; stage: string; recoverable: boolean; message: string };

这比多个互相独立的布尔值更安全。组件在 processing 状态一定能拿到 jobId,在 reviewing 状态一定同时拥有结果与原请求,类型系统也能提醒我们处理遗漏的分支。

提交前冻结请求快照

不要把仍可编辑的表单对象直接交给异步任务。点击生成时复制不可变快照,包括能力版本、源资源 ID、提示词、模型、模式、分辨率、宽高比和用户确认时的成本。

json 复制代码
{
  "capabilityVersion": "2026-08-22.3",
  "sourceAssetIds": ["asset_7f31"],
  "prompt": "移除线缆,保留桌面纹理和阴影",
  "model": "general-edit-v2",
  "mode": "single_edit",
  "resolution": "hd",
  "aspectRatio": "original",
  "quotedCreditCost": 2
}

上传与生成分离后,失败重试可以引用已完成解码和登记的资源。提交请求再带上幂等键,即使服务端创建任务后响应丢失,重试也不会产生第二个任务。

校验为什么必须做两次

客户端预检负责尽快反馈:图片数量、声明类型、实际解码、压缩大小、像素总量与内存风险。服务端负责最终可信:能力版本是否有效、模式是否存在、输入数量是否匹配、输出选项是否属于同一分支、身份是否满足要求、成本是否需要重新确认。

前端传来的报价不能作为扣费依据。它只适合用于发现"用户确认后规则发生变化"这一冲突。

失败状态不要只放一条 message

不同失败需要不同恢复策略:

  • 能力更新:刷新契约,保留图片和提示词,只标记失效字段;
  • 图片解码失败:把错误绑定到具体文件;
  • 登录或积分不足:派发任务前停止,恢复草稿后重新校验;
  • 模型服务超时:保留任务 ID,区分未知与确定失败;
  • 结果地址过期:刷新已有资源的访问权限,不重新生成;
  • 结果质量不合格:保留原请求,让用户修改提示词或模式。

recoverable 也不应该只是装饰字段,它决定界面展示"修改输入""继续查询""重新授权"还是"创建新尝试"。

reviewing 是必要状态,不是成功页动画

背景处理需要检查头发、边缘和阴影;对象清理需要检查重复纹理;增强需要检查人脸、文字和光晕;扩图需要检查透视、光线与重复物体;修复需要检查身份、服装、标识和日期。

后端完成只说明输出存在。因此在 reviewing 中应同时显示原图、提示词、选项快照和生成结果。用户确认后才进入 completed,否则从原请求创建一次有上下文的新尝试。

边界比功能数量更重要

这套状态建模适合意图驱动的在线编辑,但不等于替代精确蒙版、分层合成、像素级修饰和批量生产流程。模型与选项会变化,高分辨率和多图输入不是全局能力,部分操作可能需要登录或积分,文件格式和大小存在限制,所有结果都需要人工复核。

我参与的 AI Photo Editor 是本文的具体实践背景。真正可复用的不是某个页面,而是这组建模原则:能力由契约驱动,异步过程由状态机表达,用户投入由恢复策略保护,结果由复核状态收口。

相关推荐
JaydenAI2 小时前
[TypeScript学习笔记-10]一个特殊且重要的类型:Symbol
typescript·symbol
To_OC13 小时前
啃完 TS 工具类型我发现:Pick 和 Omit 原来就是一层窗户纸
前端·面试·typescript
JaydenAI15 小时前
[TypeScript学习笔记-07]根据现有类型创建新类型的N种方式
typescript
东风破_20 小时前
TypeScript 高级类型进阶:keyof、Exclude、Record 与类型组合思想
前端·后端·typescript
晓说前端1 天前
TypeScript 核心语法应用 —— Vue 3 中的使用(下)
前端·javascript·typescript·类型系统
东风破_1 天前
(拼多多考题)TypeScript 工具类型:Pick、Omit、Partial、Required
typescript
rimydu1974art1 天前
opencheck解读:拆解 OpenCheck 的歧视性条款识别与澄清问询机制
人工智能·typescript
触底反弹2 天前
🔥 从「为什么」到「怎么用」:TypeScript 类型约束与泛型完全指南
后端·面试·typescript
Asize2 天前
2 道大厂面试题:TS 工具类型我懂了,CSS 3 列布局把我问住了
前端·css·typescript