很多系统上传文件时给人的感觉都是一样的:
- 拖进去;
- 页面变慢;
- 某处转圈;
- 成了或错了再弹一下。
Odoo 的体验通常更顺一些,尤其是在 Kanban、会计看板、拖拽上传这些场景里。原因不在于某一个组件写得多复杂,而在于它前端分层做得比较清楚。
从 file_upload_service.js、overlay_service.js、notification_container.js 再加上 account 侧的上传视图实现连起来看,官方其实把上传反馈拆成了三类问题:
- 上传任务本身怎么追踪;
- 界面上哪些临时覆盖层怎么显示和移除;
- 错误或结果消息怎么提示给用户。
这三件事如果混在一起,就很容易出现“整个页面像被上传逻辑绑架”。
一、file_upload service 负责的是任务状态,不是遮罩也不是提示框
file_upload_service.js 里最核心的是:
uploads = reactive({})bus = new EventBus()upload()返回一个带progress / state / loaded / total的任务对象
也就是说,上传服务真正抽象的是:
一批可观察的上传任务。
它关心的事情包括:
- 传了多少字节;
- 当前是
pending / loading / loaded / error / abort哪种状态; - 这次上传的标题是什么;
- 什么时候触发
FILE_UPLOAD_ADDED / LOADED / ERROR事件。
但它不负责决定:
- 页面要不要灰掉;
- 是否弹一个全屏浮层;
- 错误应该是 toast、dialog 还是静默处理。
这点特别重要,因为很多二开恰恰喜欢把这些都塞进上传服务里,最后服务层越来越像 UI 组件。
二、overlay service 负责的是“临时覆盖内容的生命周期”
overlay_service.js 则是另一套完全不同的抽象。
它维护的是 overlays 这个响应式集合,并通过 add(component, props, options) 返回一个 remove 函数。
这套服务解决的问题是:
- 某个临时浮层何时加入主界面;
- 它用哪个 component 渲染;
- 移除时需不需要跑
onRemove; - 多个 overlay 的层级顺序怎么排。
也就是说,overlay 关心的是“有一层临时 UI 覆盖在界面上”,而不是“底层上传进度是多少”。
这和 file_upload service 是两个完全不同维度。
一个是任务状态,一个是展示容器。
三、Notification 更轻:它负责的是离散消息,不负责持续状态
NotificationContainer 更容易被忽略,因为看起来很简单:
- 遍历 notifications;
- 每条通知用
Notification组件渲染; - 通过
Transition做出入场动画。
但它边界非常清楚。
Notification 适合承载的是:
- 上传失败一句话;
- 某个文件处理结果提示;
- 一条需要用户知道的轻量反馈。
它不适合承载的是:
- 长时间持续变化的进度;
- 复杂交互浮层;
- 需要自己管理移除时机的大块 UI。
所以 notification 是“消息”,不是“过程”。
四、为什么这三层拆开后,拖拽上传体验会自然很多
以 account 的 FileUploadKanbanRenderer 为例,它在拖文件进来时,会把 dropzoneState.visible 置为 true,并且支持 onPaste() 直接走上传。
注意这里处理的是当前视图对拖拽行为的响应,不是底层上传传输本身。
再看 DashboardKanbanRenderer,它会在 dragenter / dragleave / drop 里维护 dashboardState.isDragging,决定卡片 dropzone 是否该显示。
这再次说明:
- 拖拽可见态,是视图层状态;
- 上传进度,是上传服务状态;
- 错误提示,是通知层状态;
- 真要弹覆盖层,则交给 overlay service。
职责一分清,视图自然不会被某个全能上传组件绑死。
五、为什么“上传失败弹通知”不代表“通知就是上传状态中心”
file_upload_service.js 里在 onError() 发生时,默认会调用 notificationService.add(...)。
很多人看到这段,会误以为 notification 就是上传服务的 UI。
其实不是。
更准确地说,是:
- 上传服务自己识别到错误;
- 默认借 notification 给用户一个轻量反馈;
- 但这只是默认消费方式,不是唯一消费方式。
源码里甚至留了 displayErrorNotification 开关,明确允许你关闭默认通知,改走更显式的错误处理。
这再次证明官方知道:
上传状态源和错误展示渠道,应该解耦。
六、二开时最容易混掉的几个边界
误区 1:把 overlay 当成进度状态仓库
结果就是关闭浮层时,上传状态也跟着丢。
误区 2:把 notification 当持续进度条
通知适合短消息,不适合承载一直跳变的进度过程。
误区 3:在上传服务里硬编码具体界面行为
这样服务就失去跨视图复用价值。
误区 4:视图层直接各自维护一套上传状态
最后列表、看板、拖拽区各有一份,行为一定会漂。
七、一个更稳的实现心智
如果你要扩展 Odoo 上传体验,最稳的思路通常是:
- 上传服务:只做任务状态、事件和请求;
- 视图层:只做拖拽区域、进度展示、局部可见态;
- overlay:处理需要覆盖界面的临时组件;
- notification:处理离散提示。
一旦按这个心智拆,很多设计题会自动变简单。
例如:
- “上传时页面是否遮罩?”——这是 overlay 问题;
- “上传失败怎么提醒?”——这是 notification 问题;
- “进度从哪来?”——这是 file_upload service 问题;
- “拖拽进来卡片是否高亮?”——这是当前 renderer 问题。
八、结论
Odoo 上传反馈体验之所以不容易乱,不是因为它有一个“万能上传组件”,而是因为它明确把三类东西拆开了:
file_uploadservice 负责任务状态;overlayservice 负责临时覆盖层生命周期;notification负责离散结果提示。
所以真正值得学的不是某个拖拽样式,而是这条前端分层原则:
持续过程、临时覆盖和瞬时消息,最好永远不要塞进同一层抽象里。
DISCUSSION
评论区