chore(web): 修复失效的类型检查命令(tsconfig 是 solution 文件, sc --noEmit 空跑=假绿)→ 新增 typecheck 脚本用 tsc -b;并收尾前端契约修复(路由守卫/分页/查询竞态/类型补全)
This commit is contained in:
parent
f80b3b634c
commit
6b562ae286
@ -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",
|
||||
|
||||
@ -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() {
|
||||
<Route path="/charge-records" element={<ChargeRecords />} />
|
||||
<Route path="/device-logs" element={<DeviceLogs />} />
|
||||
<Route path="/energy" element={<Energy />} />
|
||||
<Route path="/settings/users" element={<Users />} />
|
||||
<Route path="/settings/users" element={<Guard code="user:view"><Users /></Guard>} />
|
||||
<Route path="/settings/roles" element={<Guard code="role:view"><Roles /></Guard>} />
|
||||
<Route path="/settings/operation-logs" element={<Guard code="operation_log:view"><OperationLogs /></Guard>} />
|
||||
<Route path="/settings/feedback" element={<Guard code="user:view"><Feedback /></Guard>} />
|
||||
<Route path="/settings/feedback" element={<Guard code="operation_log:view"><Feedback /></Guard>} />
|
||||
<Route path="/batteries" element={<Batteries />} />
|
||||
<Route path="/charge-orders" element={<ChargeOrders />} />
|
||||
</Route>
|
||||
|
||||
@ -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<number, string> = {
|
||||
3: 'orange',
|
||||
};
|
||||
|
||||
/** 已应用的查询条件(页码 + 筛选条件) */
|
||||
interface BatteryQuery {
|
||||
page: number;
|
||||
status?: number;
|
||||
batteryId: string;
|
||||
}
|
||||
|
||||
export default function Batteries() {
|
||||
const [data, setData] = useState<Battery[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [statusFilter, setStatusFilter] = useState<number | undefined>();
|
||||
const [batteryIdFilter, setBatteryIdFilter] = useState('');
|
||||
/** 电池ID输入框(点「搜索」时才应用到查询条件,避免每敲一个字符都发请求) */
|
||||
const [batteryIdInput, setBatteryIdInput] = useState('');
|
||||
/**
|
||||
* 已应用的查询条件 —— **唯一的请求触发器**
|
||||
*
|
||||
* 页码与筛选条件放在同一个 state 里:点「搜索」时一次性写入 `{ page: 1, ...条件 }`,
|
||||
* 由 `useEffect` 触发**一次**请求。这样就不会出现「先 setPage(1) 又用旧 page 发请求」
|
||||
* 的重复请求 / 停在错页问题(历史 bug)。
|
||||
*/
|
||||
const [query, setQuery] = useState<BatteryQuery>({ 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() {
|
||||
<Form.Item label="电池ID">
|
||||
<Input
|
||||
placeholder="搜索电池ID"
|
||||
value={batteryIdFilter}
|
||||
onChange={setBatteryIdFilter}
|
||||
value={batteryIdInput}
|
||||
onChange={setBatteryIdInput}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="状态">
|
||||
<Select
|
||||
placeholder="全部"
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
value={query.status}
|
||||
onChange={handleStatusChange}
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
>
|
||||
@ -177,11 +183,11 @@ export default function Batteries() {
|
||||
data={data}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
current: query.page,
|
||||
pageSize,
|
||||
total,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p) => setPage(p),
|
||||
onChange: (p) => setQuery((q) => ({ ...q, page: p })),
|
||||
}}
|
||||
border
|
||||
stripe
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* - 搜索过滤:柜子、电池ID、状态、时间范围
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
@ -29,36 +29,50 @@ const STATUS_COLOR: Record<number, string> = {
|
||||
2: 'red',
|
||||
};
|
||||
|
||||
/** 已应用的查询条件(页码 + 筛选条件) */
|
||||
interface OrderQuery {
|
||||
page: number;
|
||||
cabinetId: string;
|
||||
batteryId: string;
|
||||
status?: number;
|
||||
dateRange: string[];
|
||||
}
|
||||
|
||||
export default function ChargeOrders() {
|
||||
const [data, setData] = useState<ChargeOrder[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cabinetId, setCabinetId] = useState<string>('');
|
||||
const [batteryId, setBatteryId] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<number | undefined>();
|
||||
const [dateRange, setDateRange] = useState<string[]>([]);
|
||||
/** 电池ID输入框(点「搜索」时才应用,避免每敲一个字符都发请求) */
|
||||
const [batteryIdInput, setBatteryIdInput] = useState('');
|
||||
/** 时间范围选择(点「搜索」时才应用) */
|
||||
const [dateRangeInput, setDateRangeInput] = useState<string[]>([]);
|
||||
/**
|
||||
* 已应用的查询条件 —— **唯一的请求触发器**
|
||||
*
|
||||
* 页码与筛选条件放在同一个 state 里:点「搜索」时一次性写入 `{ page: 1, ...条件 }`,
|
||||
* 由 `useEffect` 触发**一次**请求,不会出现「先 setPage(1) 又用旧 page 发请求」的
|
||||
* 重复请求 / 停在错页问题(历史 bug)。
|
||||
*/
|
||||
const [query, setQuery] = useState<OrderQuery>({
|
||||
page: 1,
|
||||
cabinetId: '',
|
||||
batteryId: '',
|
||||
dateRange: [],
|
||||
});
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
/**
|
||||
* 加载列表
|
||||
* 显式接收分页/筛选参数:避免在 `setState` 尚未生效时用旧值发请求
|
||||
* (历史 bug:点「查询」先 `setPage(1)` 又立刻用旧 `page` 发请求)
|
||||
*/
|
||||
const loadData = async (
|
||||
p: number,
|
||||
filters: { cabinetId: string; batteryId: string; status: number | undefined; dateRange: string[] },
|
||||
) => {
|
||||
/** 加载列表(条件由 `query` 显式传入,不读组件内的其它 state) */
|
||||
const loadData = useCallback(async (q: OrderQuery) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await chargeOrderApi.list({
|
||||
cabinet_id: filters.cabinetId ? Number(filters.cabinetId) : undefined,
|
||||
battery_id: filters.batteryId || undefined,
|
||||
status: filters.status,
|
||||
start_time: filters.dateRange[0] || undefined,
|
||||
end_time: filters.dateRange[1] || undefined,
|
||||
page: p,
|
||||
cabinet_id: q.cabinetId ? Number(q.cabinetId) : undefined,
|
||||
battery_id: q.batteryId || undefined,
|
||||
status: q.status,
|
||||
start_time: q.dateRange[0] || undefined,
|
||||
end_time: q.dateRange[1] || undefined,
|
||||
page: q.page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
setData(res.data.list || []);
|
||||
@ -68,36 +82,23 @@ export default function ChargeOrders() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** 当前筛选条件快照 */
|
||||
const currentFilters = { cabinetId, batteryId, status: statusFilter, dateRange };
|
||||
|
||||
// 分页 / 柜子 / 状态变化时自动重查(电池ID、时间范围为手动查询条件)
|
||||
// 查询条件(含页码)变化 → 重查一次
|
||||
useEffect(() => {
|
||||
loadData(page, { cabinetId, batteryId, status: statusFilter, dateRange });
|
||||
}, [page, cabinetId, statusFilter]);
|
||||
loadData(query);
|
||||
}, [loadData, query]);
|
||||
|
||||
/** 查询:页码已变化时交给 useEffect 重查,否则直接查第 1 页 */
|
||||
/** 搜索:应用输入框条件并回到第 1 页(新对象必然触发一次请求) */
|
||||
const handleSearch = () => {
|
||||
if (page === 1) {
|
||||
loadData(1, currentFilters);
|
||||
} else {
|
||||
setPage(1);
|
||||
}
|
||||
setQuery((q) => ({ ...q, page: 1, batteryId: batteryIdInput, dateRange: dateRangeInput }));
|
||||
};
|
||||
|
||||
/** 重置:清空筛选条件并回到第 1 页(用重置后的条件直接重查,不依赖 setState 时序) */
|
||||
/** 重置:清空筛选条件并回到第 1 页 */
|
||||
const handleReset = () => {
|
||||
setCabinetId('');
|
||||
setBatteryId('');
|
||||
setStatusFilter(undefined);
|
||||
setDateRange([]);
|
||||
if (page === 1 && !cabinetId && statusFilter === undefined) {
|
||||
loadData(1, { cabinetId: '', batteryId: '', status: undefined, dateRange: [] });
|
||||
} else {
|
||||
setPage(1);
|
||||
}
|
||||
setBatteryIdInput('');
|
||||
setDateRangeInput([]);
|
||||
setQuery({ page: 1, cabinetId: '', batteryId: '', dateRange: [] });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
@ -159,23 +160,23 @@ export default function ChargeOrders() {
|
||||
<Form.Item label="柜子ID">
|
||||
<Input
|
||||
placeholder="柜子ID"
|
||||
value={cabinetId}
|
||||
onChange={(v) => setCabinetId(typeof v === 'string' && v ? v : '')}
|
||||
value={query.cabinetId}
|
||||
onChange={(v) => setQuery((q) => ({ ...q, cabinetId: typeof v === 'string' && v ? v : '' }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="电池ID">
|
||||
<Input
|
||||
placeholder="搜索电池ID"
|
||||
value={batteryId}
|
||||
onChange={setBatteryId}
|
||||
value={batteryIdInput}
|
||||
onChange={setBatteryIdInput}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="状态">
|
||||
<Select
|
||||
placeholder="全部"
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
value={query.status}
|
||||
onChange={(v) => setQuery((q) => ({ ...q, status: v }))}
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
>
|
||||
@ -186,8 +187,8 @@ export default function ChargeOrders() {
|
||||
</Form.Item>
|
||||
<Form.Item label="时间范围">
|
||||
<RangePicker
|
||||
value={dateRange.length > 0 ? [dateRange[0], dateRange[1]] : undefined}
|
||||
onChange={(vals) => setDateRange(vals || [])}
|
||||
value={dateRangeInput.length > 0 ? [dateRangeInput[0], dateRangeInput[1]] : undefined}
|
||||
onChange={(vals) => setDateRangeInput(vals || [])}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
@ -209,11 +210,11 @@ export default function ChargeOrders() {
|
||||
data={data}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
current: query.page,
|
||||
pageSize,
|
||||
total,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p) => setPage(p),
|
||||
onChange: (p) => setQuery((q) => ({ ...q, page: p })),
|
||||
}}
|
||||
border
|
||||
stripe
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user