实习日志 / 中文 / 界面重构 / 登录以后,用户去了哪里:一次报案端前端的重构
登录以后,用户去了哪里:一次报案端前端的重构
中文 | 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 接通证据、检索、数据库和第三方服务;最后三条验证路径(车险、房屋、财产)并行验证,不等车险做完再扩展。
这份规格后来成了员工工作台重构的参照。我给工作台留了一条约束:不照搬报案端的三栏布局,先按员工处理案件的顺序定功能和信息层级,再决定外壳长什么样。
整件事是从一个"登录后跳错页"的小毛病开始的。挖下去才发现,前端看起来是界面问题,背后是一个没被定义清楚的状态模型;而状态模型一旦要定义清楚,就会一路牵动到认证、会话和后端契约。