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

100 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 备货单 / 终版列表「全部 / 我的」视图切换 设计
- 日期: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 请求参数
列表请求参数由
```jsonc
{ "current": 1, "size": 10, "is_final": 0, "keyword": "" }
```
扩展为(73 用户)
```jsonc
{ "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` 选择不持久化(每次进页面回到默认「我的」)。