Files
art-design/docs/superpowers/specs/2026-08-06-stocking-visibility-toggle-design.md
T
mzl92 76baee2fe5 docs: 备货单/终版列表「全部/我的」视图切换设计
为 user_id=73 在 stocking_list 与 final_list 增加「全部/我的」切换,
view_scope 语义化传参,后端按 token+73 鉴权,默认「我的」。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 11:34:33 +08:00

5.2 KiB
Raw Blame History

备货单 / 终版列表「全部 / 我的」视图切换 设计

  • 日期:2026-08-06
  • 分支:feature/crm_pc
  • 涉及页面:src/views/crmpc/stocking/stocking_list.vue、src/views/crmpc/stocking/final_list.vue
  • 涉及接口:GET /crm/stock/order/list

1. 背景

备货单列表(stocking_list,is_final=0)与终版列表(final_list,is_final=1)当前共用同一个列表接口 GET /crm/stock/order/list。后端依据请求 token 中的用户 id 过滤数据,即每个登录用户只能看到自己作为业务员的备货单 / 终版。前端请求参数只有 current / size / is_final / keyword,无任何范围控制。

业务上存在一个固定特权用户(user_id = 73),需要在「看自己」之外,额外能查看「全公司所有备货单 / 终版」。

2. 需求

  • 给 user_id = 73 这一个用户,在两个列表页增加「全部 / 我的」视图切换。
  • 选「全部」时,该用户看到全公司数据;选「我的」时,行为同现状(只看自己)。
  • 其他用户(非 73)完全不受影响:不显示切换控件,行为同现状。
  • 默认视图为「我的」。

3. 设计

3.1 UI

  • 在两个列表页的工具栏增加一组互斥切换控件「全部 / 我的」(el-radio-group button 样式,二选一)。
  • 控件位置:工具栏右侧、搜索框左边,两个页面位置保持一致(final_list 现有 toolbar-left 被注释,不在那里放,统一放右侧,改动更小)。
  • 渲染条件:useUserStore().getUserInfo.userId === 73。getUserInfo 是 userStore.info 的 computed,类型 Partial<UserInfo>,userId 可能为 undefined,undefined === 73 为 false,判断安全,无需额外判空。

3.2 前端逻辑

  • 新增本地状态 viewScope = ref<'all' | 'self'>('self'),初值 'self'(即默认「我的」)。
  • 切换控件绑定 viewScope;@change 时:
    1. pagination.currentPage = 1(切回第一页,避免越界空页);
    2. 调用 getList()。
  • getList() 构造参数时,当 userId === 73 把 viewScope.value 作为 view_scope 一并传入:
    • 「全部」→ view_scope: 'all'
    • 「我的」→ view_scope: 'self'
  • 非 73 用户:不渲染控件,viewScope 恒为 'self',传 view_scope: 'self'(或不传,见 3.3)。为简单起见统一传,后端对非特权用户会忽略该值。

3.3 请求参数

列表请求参数由

{ "current": 1, "size": 10, "is_final": 0, "keyword": "" }

扩展为(73 用户)

{ "current": 1, "size": 10, "is_final": 0, "keyword": "", "view_scope": "all" }
  • 参数名 view_scope 沿用现有 snake_case 风格(与 is_final / keyword 一致)。
  • 取值为语义化的 'all' | 'self',不传用户 id。

3.4 后端配合

GET /crm/stock/order/list 增加可选参数 view_scope,处理逻辑:

请求情况 后端行为
不传 / view_scope=self 现状逻辑:按 token 用户过滤,返回自己的数据
view_scope=all 且 token 用户 id ∈ {73} 返回全公司全量数据
view_scope=all 且 token 用户 id ∉ {73} 降级:当作 self 处理,返回该用户自己的数据(忽略 all)

后端「可看全部」白名单暂写死为 {73},与前端判断一致。

3.5 默认值

73 用户打开页面,viewScope 初值 'self',即默认「我的」。需看全部时手动切换。

4. 安全考量

  • 后端只认 token:是否返回全量数据,由后端依据 token 中的用户 id 判断,绝不信任前端传入的任何用户标识。前端 userId 存于 localStorage,可被篡改。
  • 前端写死 userId === 73 仅用于控件显隐的便捷实现,不是安全边界。即便有人改前端强制显示按钮并传 view_scope=all,后端鉴权不通过仍只返回其自己的数据。
  • 结论:语义化参数 + 后端 token 鉴权,可兜住水平越权(IDOR)风险。

5. 改动清单

端 文件 / 接口 改动
前端 stocking_list.vue 工具栏加「全部/我的」el-radio-group(仅 73 显示);getList 传 view_scope;切换重置页码并重新请求
前端 final_list.vue 同上(is_final=1)
后端 GET /crm/stock/order/list 接收 view_scope;token 用户 ∈ {73} 且 =all 时返回全量,否则按现状过滤

6. 验收标准

  1. 以非 73 账号登录两个列表页:不出现「全部/我的」控件,数据范围与改动前一致。
  2. 以 73 账号登录:
    • 默认显示「我的」选中态,列表为 73 自己的数据。
    • 切到「全部」,列表变为全公司数据;切回「我的」恢复。
    • 切换时分页重置到第 1 页。
  3. 用任意账号在浏览器手动把请求改成 view_scope=all:后端只返回该账号自己的数据(非 73 不越权)。

7. 未做项 / 后续 TODO

  • 前端 userId === 73 为硬编码,加人需改前端发版。后续建议:getUserInfo 下发权限标记(如 stock@view_all / final_stock@view_all),前端用现有 hasAuth(...) 控制显隐;后端白名单同步改为权限点判断。本次保留此扩展点,先不动。
  • view_scope 选择不持久化(每次进页面回到默认「我的」)。