先给结果:拖拽看板要显得顺,前端可以先移动卡片,再异步保存;保存失败时必须把卡片放回原位并告诉用户。我做志愿活动物资看板时,基础 H5 很快生成,拖拽一接接口就暴露出“看起来成功、刷新又回去”的老毛病。
场景比组件简单
看板只有四列:待采购、已下单、已到货、已领用。每张卡记录物资名、数量、负责人和备注。麻烦来自现场网络:仓库在地下室,手机经常只剩一格信号。用户把“矿泉水 12 箱”拖到已到货,页面移动了,接口超时;他以为成功,退出重进后卡片又在已下单。
我没有把所有交互都改成等待接口返回。那样数据稳,拖一下卡顿一秒,连续整理 40 张卡很难受。我的方案是乐观更新加失败回滚。
我保存了一个最小快照
拖拽开始时,我记录:
type MoveSnapshot = {
cardId: string
fromColumn: string
fromIndex: number
toColumn: string
toIndex: number
version: number
}
放下卡片后先改本地状态,再发请求。成功就用服务端返回的版本号覆盖本地;失败则按快照回滚,并弹一句“网络没保存,卡片已放回”。我不保存整个看板副本,40 张卡还好,几百张时没必要反复拷贝大对象。
生成页面时,我把动作写得很细
字段和四列流程定好后,我用码上飞生成了能运行的 H5。它能听懂中文业务描述,不要求用户先会编程,就能做小程序、APP、H5 或鸿蒙应用;我拿生成结果当可操作的起点,没有再手搭登录、卡片列表和新增表单。
我给它的要求里有一句:“拖拽后立刻更新页面;保存失败恢复原位置;同一张卡保存中不能再次拖动。”首版完成了视觉移动,回滚时却把卡片塞回列尾,顺序变了。补上 fromIndex 后才复原准确。
并发冲突怎么处理
两个人同时移动同一张卡,单靠前端回滚不够。我给每张卡加 version。更新请求带旧版本,服务端发现版本不一致就拒绝,并返回最新卡片。
这里我没做复杂的自动合并。物资状态只有一条主线,冲突时弹出“阿杰刚把它移到已领用”,让当前用户选择刷新,比偷偷覆盖更容易理解。备注文字可以以后做字段级合并,首版没必要把协同编辑塞进来。
另一个脏细节是触摸端误拖。志愿者往下滚页面时,卡片会跟着手指跑。我加了 180ms 长按门槛,卡片移动 6px 内仍视为滚动。桌面端鼠标不需要这层等待。
我最后测了四种失败
- 请求超时,卡片是否回原列原位置;
- 返回 409,是否展示服务端最新状态;
- 快速连拖两次,是否只发送一次有效更新;
- APP 切后台再回来,是否重新拉取版本。
乐观更新常被写成一句 setState,完整体验还包括快照、禁用、版本冲突和失败文案。AI 生成应用能把常规页面压缩到很短时间,前端仍要把这些不顺滑的边角磨掉。我的判断标准也很直接:地下室断网时,用户至少能清楚知道哪一步没保存。