TTeamDesk 学习手册
本章目录 展开导航 ↓

17 · nuqs URL 状态:让工作台可以刷新、返回和分享#

小陈把工作台筛成“紧急、分配给我、第 2 页”,打开详情后希望回来仍是这组条件,也希望能复制链接。把筛选只放进 useState,刷新就会丢;全塞进 Zustand 又不方便分享。当前项目把这类浏览意图放进 URL,由 nuqs 管理。

1. URL 是可序列化的浏览条件#

概念示例,不是真实团队链接:

text
/?team=<团队UUID>&priority=URGENT&assignment=mine&page=2&pageSize=10&q=登录

URL 保存条件,不保存查到的工单,也不保存未发送回复或认证令牌。复制给别人时,对方仍要登录并通过授权,结果受对方身份影响。

尤其 assignment=mine 表示“当前请求人的成员关系”。同一个链接给另一位客服,不保证看到相同工单;分享的是筛选意图,不是数据快照。

2. parser 如何把字符串变成组件可用状态#

url-state.ts 集中定义:

参数 解析后值 默认或无效输入行为
page 1~100000 整数 hook 使用默认 1
pageSize 1~50 整数 hook 使用默认 10
status 工单状态枚举 无有效枚举时为 null
priority 优先级枚举 无有效枚举时为 null
assignment all / mine / unassigned 默认 all
q 不超过 100 字符的字符串 默认空字符串

自定义数字 parser 先用 /^\d+$/ 限制字符串形状,再解析和检查范围。所以 1.5、-1、2abc 不会被当成合法页码。

parse() 本身在失败时返回 null,.withDefault(...) 让 hook 使用缺省值;并不是测试直接调用 .parse('-1') 就会得到 1。枚举没有设置默认状态 OPEN,无有效状态表示不附加状态筛选。

解析回落不等于地址栏上的每个非法参数都会立刻被自动清理。先检查组件实际使用的值,再区分原始 URL 和序列化后的 URL。

3. useQueryStates 如何连接 UI 与查询#

TicketList 使用:

tsx
// 源码节选
const [filters, setFilters] = useQueryStates(ticketSearchParsers, {
  history: 'push',
  shallow: true,
});

filters 是解析后的状态,setFilters 用来更新这组参数。当前 team 在 TeamWorkspace 用单独的 useQueryState('team', parseAsString) 管理;它不在 ticketSearchParsers 内。teamParser 虽被导出,当前工作区并没有直接使用这个导出。

providers.tsx 的 NuqsAdapter 将 nuqs 接到 Next.js App Router。页面仍是当前框架的客户端交互组件,不能绕过安装版本的路由与渲染约定。

  • history: 'push' 让这类条件更新进入浏览历史,支持后退、前进恢复条件。
  • shallow: true 让这些更新侧重客户端 URL 状态,不要求为此重跑 Server Component 数据读取。

“shallow”不等于“不会发网络请求”。解析值变了,组件会给 TanStack Query 传新输入;查询键变化后,它仍可按缓存策略发起 tRPC 请求。

4. 筛选更新为什么总是配合 page: 1#

tsx
// 源码节选:状态筛选
onChange={(value) => {
  void setFilters({ status: value ?? null, page: 1 });
}}

从第 8 页筛成只有 1 页结果的条件,继续保留页码会显示空页。useQueryStates 让相关参数作为一次逻辑更新提交,避免组件先使用新筛选与旧页码这个中间组合。

页大小变化也回到第一页;普通翻页则保留当前页大小。setFilters(null) 重置这组工单参数,不会因为 team 恰好也在 URL 中就把它一起清空。

搜索使用 Input.Search.onSearch 提交条件,不是每输入一个字符就立即查询。组件以 key={q} 配合 defaultValue={q},让前进后退恢复 q 时输入框也重新反映 URL 值。

5. Query 与服务端怎样继续处理条件#

列表查询输入包括当前团队、分页、状态、优先级、assignment 和 q。可空状态转为 undefined,表示未指定筛选;不是给数据库传一个“null 状态”。

服务端过滤器 在团队和客户归属条件之外继续叠加:标题包含搜索、优先级、状态和负责人条件。当前搜索只查标题,并使用大小写不敏感匹配,不会自动搜索描述或所有回复。

mine 对应当前 Membership.id,unassigned 对应空负责人。客户界面隐藏分配筛选,但即使手改 URL,服务端创建者和团队边界仍在,不能通过筛选扩大可见范围。

6. 详情链接与返回链接怎样保留上下文#

列表行用 useSearchParams() 读取当前参数,转成新 URLSearchParams,带到详情链接;详情的返回列表链接再带回这些参数。这里是读取快照后构造新对象,不是直接修改 useSearchParams 返回的只读对象。

操作路径 当前保留的条件 原因
列表行 → 已有工单详情 → 返回列表 当前查询参数及团队 链接显式传递
创建工单成功 → 新详情 团队 创建成功导航只带 team
切换团队 → 工作台 新团队 工作区导航到根路径,不承诺保留旧筛选
详情里的回复分页 不在 URL Conversation 自己的局部 state

因此“所有动作都会无损恢复所有筛选”并不是当前实现。要分享某张已有工单并保留列表条件,应从当前列表行打开详情,而不是假设所有跳转都拼接同一套参数。

拓展阅读URL 校验为什么不能替代接口校验?

nuqs parser 让浏览器在坏链接下仍能得到合理状态;Zod 再保护真正的 HTTP 输入。外部脚本不需要经过页面 parser,合法 UUID 也不代表有团队资格。URL、schema、授权分别处理交互、输入合同和业务访问,三层都要存在。

7. Suspense 与当前 Next.js 边界#

项目附带的 useSearchParams 指南说明它属于客户端只读查询参数 Hook;页面在预渲染场景下还要考虑 Suspense 边界。

首页 与 详情页 都使用 Suspense。它们不是让每个 Query loading 自动消失的魔法:工作台仍显式处理加载、错误、空结果。遇到构建提示 URL 数据缺少边界,应按当前安装版本文档修复,不要直接搬旧版路由的 shallow 写法。

8. 本章练习#

在有数据的本地工作台切换筛选、翻页,复制链接刷新;打开列表里的详情,再返回;然后用浏览器后退和前进观察 q、下拉框与表格输入是否一致。再手工输入 page=-1 或非法状态,确认解析回落且接口没有读取超量数据。

练习:把同一条 assignment=mine 链接发给另一客服,为什么结果可能不同?

动手练习查看参考答案

参考答案:URL 保存“分配给当前我”的相对条件;服务根据对方会话与团队成员关系计算 mine,还要执行权限过滤。它既不是负责人 ID 的固定快照,也不是访问授权。

完整链路是:URL → nuqs 解析 → UI 与 Query 输入 → tRPC / Zod → 成员权限和筛选条件 → 数据库 → Query 缓存 → 表格;用户操作再经 setFilters 写回 URL。

把知识变成自己的理解

完成本章练习,再继续下一站。

TEAMDESK HANDBOOK · 从一张工单,走进全栈开发

搜索教程

输入关键词,搜索全部章节。

阅读设置

外观
正文字号

从一张工单出发,读懂数据如何流转。

设置与进度仅保存在当前浏览器。