ysoseri.us

实习日志 / 中文 / 界面重构 / 登录以后,用户去了哪里:一次报案端前端的重构

登录以后,用户去了哪里:一次报案端前端的重构

中文 | English

9 月 2 日,我在报案端走了一遍最普通的流程:打开页面,登录,开始报案。走完以后发现,登录这一步本身就把人弄丢了。

一个小毛病

当时登录或注册成功后,页面会跳到一个 Your account 账户页,上面有一个 Back to claims 按钮。点它,回到的却是入口页,刚才那个聊天工作区不见了。

用户在这里会连着困惑三次:不知道登录到底成功没有;以为自己还卡在登录注册流程里;找不到刚刚开始的那单报案和之前的对话。

反过来也有问题。已经在工作区里聊天的用户,点左下角的个人资料,有时页面什么都不变。原因是账户页的渲染条件写的是"还没有开始报案",一旦开始了报案,点个人资料只改了状态,页面上看不出任何变化。

两个毛病指向同一件事:页面显示什么,是靠一个叫 hasStarted 的布尔值隐式推出来的。入口、登录、注册、工作区、账户这几个视图,没有一个被明确地定义成状态。

往下挖,是整个前端的结构问题

顺着这个线头看下去,问题比登录大得多。旧的报案端是一路加出来的:营销式的入口页、固定的网页表单、分步骤的车险引导流程、轮询刷新、开发演示用的控制按钮、聊天工作区,全都堆在一个 HTML 和一套页面逻辑里。结果是:

  • Agent 没能成为主入口,用户要先猜系统预设了哪条流程;
  • 组件、样式和业务状态缠在一起,改一个按钮可能弄坏另一个页面;
  • 页面状态靠局部变量和浏览器的后退键维持,刷新或后退就可能进到对不上的页面;
  • delivery、audience、retry reason 这类内部字段直接漏到了用户界面上;
  • 静态的 fixture 数据和旧布局,把真实 API、登录、证据上传和人工交接这几条链路上的问题都盖住了。

所以这次重构的目标定得很明确:不在旧页面上继续加功能,把报案端改成一个围绕真实用户旅程、真实后端状态、可以反复验证的前端。

先把旅程画清楚

我先定了几条原则。Agent 是主视觉,也是唯一的主要入口,用户用自己的话开始,不需要懂保险术语,也不需要按固定问卷的顺序走。右侧的动态表单是后端状态机和字段注册表的投影,用户能看到当前记下了什么,但不会被一张固定的问题清单牵着走。网页表单、找人工、上传文件、语音和无障碍能力都保留,作为辅助入口,不和 Agent 抢主入口。

关于登录,原则是:没登录的人不能被挡住。他们可以发消息、上传文件、请求人工协助,缺的只是长期的个人资料、历史记录和文件的长期保存。

按这个原则,目标旅程是这样的:

已登录用户:打开页面 → 直接进入 Agent 工作区
未登录用户:打开页面 → 可以直接聊天
未登录用户发出消息 → 留在 Agent 工作区
未登录用户点左下角个人资料 → 显示 "Log in to save your chat"
点登录 → 弹层或独立的认证视图
认证成功 → 回到当前工作区,并把当前的临时会话关联到账户(如果有)

Your account 不再是登录后的中转页。账户设置只能从个人资料进入,返回时回到进来之前的那个视图。只有真的检测到一单可以恢复、还没完成的报案时,才显示 Resume a claim。

这套旅程落到实现上,有三处要讲清楚。

匿名会话。 未登录的用户发出第一条消息时,前端生成一个随机的匿名会话,同时创建报案。匿名身份放在请求头 X-Northwind-Anonymous-Session 里,不发一个空的 bearer token 冒充登录。

登录后的转移。 如果登录时手上有一单匿名报案,认证成功后调用 promotion 接口,把报案、会话、消息、证据和相关记录一起转到这个账户名下。转移失败时,当前的对话不能悄悄丢掉。

登录状态要能恢复。 登录状态不能只存在一个 JavaScript 模块变量里,否则刷新一下就没了。用户再打开页面时,应该像用 ChatGPT 和 Claude 网页版一样,不用每次重新登录。前端因此要处理三件事:恢复会话、会话失效、主动退出。

让 URL 和页面状态说同一件事

hasStarted 那个毛病的根治办法,是让每个页面由 URL 和 React 状态共同决定:

URL页面允许进入的条件
/入口,匿名开始没有进行中的报案,或者用户主动回到入口
/auth/login登录还没认证;可以带着匿名报案的上下文
/auth/register注册同上
/claims/{claim_id}对话工作区内存里的当前报案和 URL 里的 claim_id 一致
/account个人资料已认证;在工作区里作为一个工具视图打开,不用弹窗
/how-it-works教学内容还没开始报案,或者由明确的辅助动作打开

光有路由表还不够,跳转本身也要设防:

  • setPage 必须同时更新 React 的页面状态和浏览器的 history,不能只改地址栏。
  • 浏览器的 popstate 先经过权限和上下文校验,再决定接不接受;不合法的路径立刻 replace 成当前应在的路由。
  • 没认证就访问 /account,转去登录页;已经认证还访问登录或注册页,转回聊天主页面。
  • 报案开始以后,用户不能靠后退键退到一个会覆盖当前工作区的入口状态。
  • 退出登录是一个明确的生命周期操作:清空账户、报案、会话、消息、字段、附件、交接状态和工具视图,然后 replace 到 /。
  • 认证页、入口页、工作区和工具页用互斥的渲染分支,背景层不会跨页面残留。
  • 刷新页面时,不能把 URL 当成"业务状态已经加载好了"。如果内存和后端都没有对应的上下文,就回到安全的入口,或者重新加载明确的报案投影,绝不渲染半个页面。

拆组件,也拆掉旧东西

旧代码里有一批东西要么删掉,要么被取代:

旧东西处理原因换成什么
登录后进入 Your account删掉打断主任务,登录状态不明确直接进入工作区
Back to claims 回到入口页删掉回错了地方,丢掉了工作区的上下文回到进入账户页之前的视图
绿色渐变的营销式入口删掉和定下的暖色调性冲突暖色层级加单一的鼠尾草绿强调色
车险分步引导入口删掉和 Agent 主导的自适应对话冲突一个自然语言输入框,由 Agent 推进
三张理赔类型卡片删掉把报案变成了一道选择题输入框旁一个非必选的理赔类型下拉
文件、语音、无障碍的 Unicode 占位按钮取代占位符不能当功能用真实的组件和状态;没实现的显示真实的禁用原因
平铺的字段列表取代表达不出动态表单的业务层级按分组展示,带来源、状态、新增标记和编辑能力

前端改成 Node、Vite、React 的结构,不再把 JSX、业务逻辑、CSS 和路由塞回一个静态 HTML。组件按职责拆开:负责输入的 MessageComposer(文字、附件、语音、理赔类型、模型选择、发送状态),负责第三方服务的 ExternalServiceAction(服务说明、同意、请求、结果和限制),以及对话、字段面板、对话记录、认证页、工具页几块。每个组件只管自己的显示和事件接口;报案归属、版本号、鉴权、动作执行和持久化,留在 API 和服务层。

输入栏是一个例子。文字区、附件卡片和工具栏属于同一个面,不各自套框,中间也没有多余的分隔线。初始高度约两行,随文字和附件自动长高,有一个最大高度。工具栏左边是圆形的附件按钮、语音、可选的理赔类型、模型选择,右边是发送。没登录的用户,语音、朗读、发送和上传一个都不禁用,只有需要长期保存的能力才提示他登录。

token 要能执行

视觉规范之前写在设计文档里,是一张张色值表。这次重构给它加了一个要求:token 必须是代码里可执行的唯一来源。报案端和员工端共用一套基础 token,信息密度和布局可以不同,但不能各自复制一份。文件放在两个前端都能导入的位置:

frontend/shared/design-tokens.css
frontend/shared/design-tokens.js   # 需要在 JS 里读取时再提供

几条约束:

  • 组件的 CSS 里不许直接出现十六进制色值;
  • 同一个语义只能有一个 token,不能同时留着几套"成功绿";
  • token 按用途命名,--color-action-primary 可以,--green-button-1 不行;
  • 状态类 token 要同时定义文字、背景、边框和聚焦、悬停时的表现;
  • 状态语义统一成一组:confirmed、proposed、pending、new、incomplete、handoff、error、success、disabled;
  • 删掉旧 token 和硬编码以后,要留下 lint 或测试规则,防止它们再回来。

落进代码时,几个值也从设计稿里调了一下:强调色从 #5F8F80 调浅到 #7A9E8E,新增了一层 #FFFDF9 的卡片表面色,补了警告色 #A3682F;字号定成最小 14px、正文 16px、消息 18px、分区标题 20px、页面标题 24px、首屏 28px。

前端的改动,牵动的是后端契约

这次重构不只是换皮。它改的是路由状态、认证接口、报案和会话的契约,以及字段展示需要的元数据。前端要靠后端提供这些能力:

  • 浏览器能恢复的认证会话,并且会话失效时能明确告诉前端;
  • 匿名报案和会话在登录后能关联到用户;
  • 报案历史能返回标题、摘要、更新时间、是否可以恢复;
  • 每个字段带上分组、来源、状态、是否新增或改动、能不能编辑,以及当前处在哪个分支;
  • Agent 确认进入某个理赔分支以后,前端能知道这个分支已经锁定;
  • 交接、第三方服务这类状态,以消息或状态卡片的形式由后端给出,前端不去猜。

数据库那边,相应地要有持久会话和它的失效时间、匿名会话与用户的关联状态、报案的 can_resume 和最后活动时间、字段的元数据。这些字段不能为了前端好看而临时拼出来。后端没有可靠来源的,前端就明确显示不可用,不伪造一个完成的状态。

测试跟着旅程走

旧测试大多还在测车险分步表单,和新的主路径对不上,要删或者迁到明确的历史 fixture 范围里。新加的测试按旅程写,例如:

  • 没登录直接发消息,进入工作区并创建临时会话;
  • 同一次浏览里登录,匿名报案被关联,消息还在;
  • 登录成功不会进入 Your account;
  • 从个人资料打开账户页,返回后回到原来的工作区;
  • 刷新后恢复登录状态,失效后回到未登录状态;
  • 只有存在可恢复的报案时才显示 Resume a claim;
  • 刷新、前进、后退、直接访问 URL,都不会出现错页或背景层残留;
  • 报案人看不到 audience、delivery 这些内部字段。

顺序和下一步

动手的顺序也按依赖排:先稳住路由、认证、匿名报案的转移和会话的生命周期;再拆组件;然后把颜色、间距、状态和按钮全部迁到共享 token;再按真实 API 接通证据、检索、数据库和第三方服务;最后三条验证路径(车险、房屋、财产)并行验证,不等车险做完再扩展。

这份规格后来成了员工工作台重构的参照。我给工作台留了一条约束:不照搬报案端的三栏布局,先按员工处理案件的顺序定功能和信息层级,再决定外壳长什么样。

整件事是从一个"登录后跳错页"的小毛病开始的。挖下去才发现,前端看起来是界面问题,背后是一个没被定义清楚的状态模型;而状态模型一旦要定义清楚,就会一路牵动到认证、会话和后端契约。