前几天,我在做一个智能投标管理系统的编辑器优化。
用户反馈说:"全局样式设置了,但没有生效。"
我第一反应是:应该是哪里漏传了参数。翻代码,找到对应的 store,逻辑完全正确,状态也存进去了。但就是没生效。
然后我意识到:系统在说谎。
什么叫系统说谎?
就是代码逻辑没有问题,测试也能通过,但实际运行的结果和你以为的完全不一样。这种情况往往不是 bug,而是架构层面的裂缝。
这个项目的背景是这样的:系统有一个标书编制页面,用户可以选择章节、编辑内容、设置排版样式。排版样式(字体、字号、行距)存在全局 store 里,理论上所有章节打开时都应该自动应用。
但用户说没有生效。
我开始追代码。发现了一件有意思的事情:
这个系统里存在两套编辑器。
一套是 BidCompose.tsx,是最早写的独立页面编辑器。另一套是 SectionEditor.tsx,是后来在工作流模块里新加的章节编辑器。排版面板、全局样式的逻辑,只在 SectionEditor 里实现了。而用户实际使用的入口,是 BidCompose。
所以全局样式存进去了,但压根没有人去读它、应用它。
这就是系统在说谎的方式:功能看起来存在,实际上是孤岛。
我把这种情况叫做「功能分裂」。它是一种比 bug 更危险的问题,因为它不会报错,不会崩溃,只会悄悄让用户的期望落空。
找到问题根因之后,我没有急着修。
我先问了自己一个问题:为什么会有两套编辑器?
这不是代码层面的问题,而是一个决策问题。
回溯历史,大概率是这样的:一开始有一个简单的编辑器(BidCompose)。后来做工作流功能时,需要一个更强大的章节编辑器,于是新建了 SectionEditor,把排版、字数统计、AI 生成等功能都堆进去了。两套并行,互不感知。
这是产品快速迭代时非常常见的模式:在已有系统旁边新建,而不是改造已有系统。
短期看这样很快,不会破坏现有功能。长期看,系统会逐渐分裂成多个"平行世界",每个世界有自己的逻辑,彼此不同步。
修复这种裂缝,比修一个 bug 要难得多——因为你不只是在改代码,你是在弥合两个架构决策之间的鸿沟。
我的选择是:不合并两套编辑器(代价太大),而是在 BidCompose 里补上缺失的那一半逻辑,让它也能读取和应用全局样式,并加上排版面板。同时把 BidCompose 作为唯一的主入口,让 SectionEditor 继续服务工作流场景。
这个决策的本质是:承认分裂,但设定边界。
第二个问题更有意思。
用户反馈:「编辑内容很长时,顶部的工具栏会随着滚动消失,想用某个功能还得往回滚。」
这是一个经典的「工具栏 sticky 失效」问题。我看了一眼代码:
.rich-text-editor {
overflow: hidden; /* ← 就是这里 */
border-radius: 8px;
}
.toolbar {
position: sticky;
top: 0;
z-index: 10;
}工具栏已经设置了 sticky top-0,但外层容器有 overflow: hidden。
很多人不知道一个 CSS 规范里的细节: 定位只在可滚动祖先容器内生效。而 会创建新的溢出上下文,直接使 失效。
这是表层原因。但我想聊聊更深的那一层。
为什么外层容器会有 overflow: hidden?
因为要实现圆角(border-radius)时,防止子元素溢出破坏外观。这是一个非常合理的样式决策,在大多数场景下都没问题。
但问题在于:这两个决策没有在同一个上下文里被考虑过。加圆角时没有想到工具栏需要 sticky,加 sticky 时没有意识到外层有 overflow 截断。
这就是我理解的「容器职责混乱」:一个容器同时承担了「视觉容器」(圆角、边框)和「滚动上下文」两种职责,而这两种职责在某些情况下是互斥的。
解法很简单:去掉 overflow: hidden,工具栏加 shadow-sm 来实现视觉分离。但这个解法背后的思维是:识别并分离容器的职责。
第三个问题,是我认为这轮迭代里最有价值的一个决策。
原来的流程图编辑器,是一个 Mermaid 代码编辑器——左边写代码,右边实时预览。对于会写 Mermaid 语法的人来说,效率很高。
但这是一个面向招投标从业者的系统,不是面向程序员的。
用户反馈:「流程图节点不能拖,不能修改,就像看着一个图,但完全没法编辑。」
这句话让我停下来想了很久。
用户在想什么?
用户在想:这个框框,我要把它移到那里;这条线,我要让它弯一点;这个菱形,我想让它大一点,文字小一点。
系统在想什么?
系统在想:你需要写 flowchart TD 然后用 --> 连接节点,用 { } 表示菱形,用 [ ] 表示矩形……
这就是用户心智模型和系统模型的错位。
用户的心智模型是「画图」,系统给的是「写代码」。两者在表达同一件事,但认知成本差了一个数量级。
我用 React Flow 重写了流程图编辑器。现在用户可以直接拖拽节点、调整大小、从蓝色连接点拖出连线、在属性面板里改颜色和字号。技术上实现起来比 textarea 复杂很多,但用户感受到的是:「哦,这就是我想要的。」
好的架构,应该让系统和用户说同一种语言。
最后一个决策,是关于「预览」和「编辑」的关系。
原来的设计是:进入页面默认是编辑模式,点「预览」会弹出一个全屏遮罩,预览完关掉继续编辑。
用户说:「我希望进来先看整篇文章的排版效果,然后点某个章节再去编辑。」
这句话改变了我对这个页面的理解。
原来我认为:编辑是主任务,预览是辅助确认。
用户告诉我:预览才是起点,编辑是后续动作。
这不只是顺序的调整,而是对用户工作流的重新理解。
一个写招标文件的人,他的工作流是这样的:先看整体,再局部调整,再看整体……这是一个反复在「全局视角」和「局部编辑」之间切换的过程。
所以我把「预览」从弹窗变成了内嵌的主视图,默认进入时展示整篇预览。顶部有两个切换按钮:「预览」和「排版编辑」。在预览里点击某个章节,可以直接跳转到对应的编辑状态。
这个改动的工作量不大,但背后的思维转变很重要:模式(mode)不是功能,是用户的认知框架。
设计「模式」时,你需要问的不是「这个功能放在哪里」,而是「用户在哪个心理状态下需要这个功能」。
回顾这三轮迭代,我发现自己做的事情,本质上只有一件:
让系统更诚实。
架构师最重要的能力,不是选择哪个框架、用哪种设计模式。而是能够持续感知系统和现实之间的偏差,然后找到代价最小的方式把它们重新对齐。
每一次对齐,都是一次架构的进化。
最后分享一个我反复验证过的判断标准:
当用户说「这个东西没用」,通常不是功能不存在,而是功能和用户的期望之间有一道看不见的墙。架构师的工作,就是找到这道墙,然后把它拆掉。
不是加功能。是拆墙。
本文基于智能投标管理系统近期迭代的真实经历,技术栈:React 18 + TypeScript + Tiptap + React Flow。