#背景
后台控制台有 20 多个列表页,最初的写法是每页各自 reactive 一套 filters、pagination、load()。三个月后的问题清单很典型:
- 从列表点进详情再返回,筛选条件全丢,用户要重新选一遍;
- 分页页码在刷新后回到第 1 页;
- 窄屏下抽屉宽度写死像素,右侧被切掉;
- 提交失败的错误字段回填逻辑,每个页面写法都不一样。
这些都不是"某个页面的 bug",而是缺少公共能力。
#做法:按能力切
composables/
├── useViewState.js # 列表页的筛选 + 分页记忆(sessionStorage)
├── useViewport.js # 视口判断(窄屏下的抽屉/表格策略)
└── useForm.js # 提交、错误回填、脏标记#1. useViewState:状态要能"记得住"
const { filters, pagination, reset, clearViewState } = useViewState('cases', {
filters: { keyword: '', status: '' },
pagination: { page: 1, per_page: 20 },
})用 sessionStorage 而不是 localStorage:关掉标签页即清,不会让三天前的筛选条件阴魂不散地影响今天的判断。
一个必须处理的细节:从外部带参数跳转进来时以 URL 为准(比如从仪表盘点"待审核评论"跳到评论列表),此时要 clearViewState() 再应用 URL 参数,否则记忆值会盖掉 URL 参数,用户点了却看不到想看的。
#2. useViewport:窄屏判断集中一处
const { isMobile } = useViewport()宽度用 matchMedia 而不是监听 resize:前者不会在拖拽窗口时触发几十次回调,也不需要手动清理。
抽屉宽度是它的第一个用武之地:isMobile ? '100%' : 720 —— 固定像素在手机上会让抽屉超出屏幕,而 fixed 定位的溢出不会出现在 scrollWidth 里,常规的溢出检查抓不到。
#3. useForm:错误回填统一
const { saving, errors, submit, serverFeedback } = useForm(api.saveCase)后端返回的字段级错误({"title": "标题不能为空"})直接映射到表单上,前端校验规则与服务端规则共享同一份字段名。这个约定省掉了"前端显示的位置不对"这类来回改。
#命名与边界
三条约定:
use前缀 + 名词性能力名(useViewState、useViewport),不要用动词(useLoadList)——能力应该能描述"它给你什么",而不是"它做了什么";- 返回值只暴露需要的:内部
ref不要整个 return,避免调用方误改内部状态; - 一个组合式函数只解决一件事:
useViewState不管请求,useForm不管分页。需要组合时在页面里组合。
#结果
- 列表页从"每页 40 行重复逻辑"降到"每页 8 行调用";
- 筛选/分页记忆、窄屏适配、错误回填在全站一次性统一;
- 新增列表页不再需要抄代码——抄代码会抄错,调用能力不会。
#取舍
- 不引入 Pinia 存页面状态:页面级状态(筛选、分页)放在组合式函数 + sessionStorage 里足够;全局状态(当前用户、权限)才进 store。把页面状态塞进全局 store 会让"谁能改这个值"变得难以追踪。
- 不做通用 CRUD 生成器:抽象层次越高,例外情况越难处理。宁可多写 8 行调用,也不要一个"配置驱动的万能表格"——那种东西改一次需求就要改生成器,最终没人敢动。
评论
还没有评论,来说点什么吧。