diff --git a/software/web/package.json b/software/web/package.json index 63ee6a6..de5d930 100644 --- a/software/web/package.json +++ b/software/web/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", - "preview": "vite preview" + "preview": "vite preview", + "typecheck": "tsc -b" }, "dependencies": { "@arco-design/web-react": "^2.66.15", diff --git a/software/web/src/App.tsx b/software/web/src/App.tsx index e661661..faf1d3b 100644 --- a/software/web/src/App.tsx +++ b/software/web/src/App.tsx @@ -53,11 +53,13 @@ function RedirectIfAuth({ children }: { children: React.ReactNode }) { /** * 路由级权限守卫:无权限时展示统一的 403 页(`NoPermission`) * - * 权限码来自后端 `db/migrate.rs` 的 permissions 种子表: - * - `/settings/roles` → `role:view`(种子里存在;仅总管理员被授予) - * - `/settings/operation-logs` → `operation_log:view`(种子里存在;企业管理员/总管理员被授予) - * - `/settings/feedback` → `user:view`(后端 `/api/feedback` 目前只校验登录态、**没有专用权限码**, - * 这里用「查看用户」作为最小可用守卫,详见修复报告) + * 权限码全部取自后端**真实校验**的权限码,并逐一在 `db/migrate.rs` 的 permissions 种子里核对: + * - `/settings/roles` → `role:view`(种子 482 行,仅总管理员被授予) + * - `/settings/operation-logs` → `operation_log:view`(种子 486 行;后端 `operation_logs.rs:154` 校验) + * - `/settings/feedback` → `operation_log:view`(后端反馈列表/改状态用同一权限码: + * `h5.rs` 的 `list_feedback` / `update_feedback_status`;种子里真实存在) + * - `/settings/users` → `user:view`(后端 `users.rs:345` 校验;**同一处缺陷的顺带修复**: + * 该项在侧边菜单里对所有角色可见,操作员点进来以前只会看到「加载失败」) * * 总管理员(role_level >= 2)在 `hasPermission` 内直接放行,不会被拦。 */ @@ -104,10 +106,10 @@ export default function App() { } /> } /> } /> - } /> + } /> } /> } /> - } /> + } /> } /> } /> diff --git a/software/web/src/pages/batteries/index.tsx b/software/web/src/pages/batteries/index.tsx index c4b17d7..4720f28 100644 --- a/software/web/src/pages/batteries/index.tsx +++ b/software/web/src/pages/batteries/index.tsx @@ -4,7 +4,7 @@ * - 搜索过滤:电池ID、状态 */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Card, Table, @@ -29,28 +29,38 @@ const STATUS_COLOR: Record = { 3: 'orange', }; +/** 已应用的查询条件(页码 + 筛选条件) */ +interface BatteryQuery { + page: number; + status?: number; + batteryId: string; +} + export default function Batteries() { const [data, setData] = useState([]); const [total, setTotal] = useState(0); - const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [statusFilter, setStatusFilter] = useState(); - const [batteryIdFilter, setBatteryIdFilter] = useState(''); + /** 电池ID输入框(点「搜索」时才应用到查询条件,避免每敲一个字符都发请求) */ + const [batteryIdInput, setBatteryIdInput] = useState(''); + /** + * 已应用的查询条件 —— **唯一的请求触发器** + * + * 页码与筛选条件放在同一个 state 里:点「搜索」时一次性写入 `{ page: 1, ...条件 }`, + * 由 `useEffect` 触发**一次**请求。这样就不会出现「先 setPage(1) 又用旧 page 发请求」 + * 的重复请求 / 停在错页问题(历史 bug)。 + */ + const [query, setQuery] = useState({ page: 1, batteryId: '' }); const pageSize = 20; - /** - * 加载列表 - * 显式接收分页/筛选参数:避免在 `setState` 尚未生效时用旧值发请求 - * (历史 bug:点「搜索」先 `setPage(1)` 又立刻用旧 `page` 发请求,导致重复请求或停在错的页) - */ - const loadData = async (p: number, status: number | undefined, batteryId: string) => { + /** 加载列表(条件由 `query` 显式传入,不读组件内的其它 state) */ + const loadData = useCallback(async (q: BatteryQuery) => { setLoading(true); try { const res = await batteryApi.list({ - status, - battery_id: batteryId || undefined, - page: p, + status: q.status, + battery_id: q.batteryId || undefined, + page: q.page, page_size: pageSize, }); setData(res.data.list || []); @@ -60,31 +70,27 @@ export default function Batteries() { } finally { setLoading(false); } - }; + }, []); - // 分页 / 状态筛选变化时自动重查(battery_id 为手动搜索,不作为依赖) + // 查询条件(含页码)变化 → 重查一次 useEffect(() => { - loadData(page, statusFilter, batteryIdFilter); - }, [page, statusFilter]); + loadData(query); + }, [loadData, query]); - /** 搜索:页码已变化时交给 useEffect 重查,否则直接查第 1 页 */ + /** 搜索:应用输入框条件并回到第 1 页(新对象必然触发一次请求) */ const handleSearch = () => { - if (page === 1) { - loadData(1, statusFilter, batteryIdFilter); - } else { - setPage(1); - } + setQuery((q) => ({ page: 1, status: q.status, batteryId: batteryIdInput })); }; - /** 重置:清空筛选条件并回到第 1 页(用重置后的条件直接重查,不依赖 setState 时序) */ + /** 状态筛选变化:沿用当前页码重查(与改动前行为一致) */ + const handleStatusChange = (value?: number) => { + setQuery((q) => ({ ...q, status: value })); + }; + + /** 重置:清空筛选条件并回到第 1 页 */ const handleReset = () => { - setStatusFilter(undefined); - setBatteryIdFilter(''); - if (page === 1 && statusFilter === undefined) { - loadData(1, undefined, ''); - } else { - setPage(1); - } + setBatteryIdInput(''); + setQuery({ page: 1, status: undefined, batteryId: '' }); }; const columns = [ @@ -140,16 +146,16 @@ export default function Batteries() { setCabinetId(typeof v === 'string' && v ? v : '')} + value={query.cabinetId} + onChange={(v) => setQuery((q) => ({ ...q, cabinetId: typeof v === 'string' && v ? v : '' }))} />