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() {