05a1d06e61
review 发现 4 处仍用 new Date(v).toLocaleString 渲染后端 UTC 口径时间戳—— 正是 lib/format.ts 头注释明令消灭的反模式(SQLite 本地/非中国浏览器会差 8h, 生产靠 +00:00 偏移侥幸正确)。统一改用 formatUtcTime 按北京显示: - admins: last_login_at - audit-logs: created_at - devices: completed_at - dashboard/HomeStatsConfig: ops_stat_config.updated_at 均为 server_default now()/datetime.now(utc) 的 UTC 口径字段。 HomeStatsConfig 的「下个更新时刻」用 epoch ms + 强制 Asia/Shanghai,正确,不动。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: OuYingJun1024 <1034284404@qq.com> Reviewed-on: #10 Co-authored-by: ouzhou <ouzhou@wonderable.ai> Co-committed-by: ouzhou <ouzhou@wonderable.ai>
76 lines
2.2 KiB
TypeScript
76 lines
2.2 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { api } from './api';
|
|
import type { CursorPage } from './types';
|
|
|
|
/**
|
|
* 页码分页列表 hook(对接后端 offset 分页 + total)。
|
|
* 与 useCursorList(「加载更多」)并存:需要页码/跳页/共 N 条的主列表页用本 hook。
|
|
*
|
|
* 约定:后端 cursor 即 offset,本 hook 按 (page-1)*pageSize 计算;CursorPage.total 为符合
|
|
* 筛选条件的总条数(供 antd Table pagination 渲染)。filters 变化自动回第 1 页重载。
|
|
*/
|
|
export function usePagedList<T>(
|
|
url: string,
|
|
filters: Record<string, unknown>,
|
|
initialPageSize = 20,
|
|
) {
|
|
const [items, setItems] = useState<T[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
const [loading, setLoading] = useState(false);
|
|
const requestSeq = useRef(0);
|
|
|
|
const filtersKey = JSON.stringify(filters);
|
|
|
|
const load = useCallback(
|
|
async (p: number, ps: number) => {
|
|
const seq = requestSeq.current + 1;
|
|
requestSeq.current = seq;
|
|
setLoading(true);
|
|
try {
|
|
const params = { ...JSON.parse(filtersKey), limit: ps, cursor: (p - 1) * ps };
|
|
const { data } = await api.get<CursorPage<T>>(url, { params });
|
|
if (requestSeq.current !== seq) return; // 丢弃过期响应(快速切页/筛选)
|
|
setItems(data.items);
|
|
setTotal(data.total ?? 0);
|
|
} finally {
|
|
if (requestSeq.current === seq) setLoading(false);
|
|
}
|
|
},
|
|
[url, filtersKey],
|
|
);
|
|
|
|
// 筛选变化:回第 1 页重载
|
|
useEffect(() => {
|
|
setPage(1);
|
|
load(1, pageSize);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [load]);
|
|
|
|
// antd Table pagination.onChange(page, pageSize):页变或每页条数变都走这里
|
|
const onChange = (p: number, ps: number) => {
|
|
if (ps !== pageSize) {
|
|
// 每页条数变 → 回第 1 页,避免越界空页
|
|
setPageSize(ps);
|
|
setPage(1);
|
|
load(1, ps);
|
|
} else {
|
|
setPage(p);
|
|
load(p, ps);
|
|
}
|
|
};
|
|
|
|
return {
|
|
items,
|
|
total,
|
|
page,
|
|
pageSize,
|
|
loading,
|
|
onChange,
|
|
reload: () => load(page, pageSize), // 写操作后刷新当前页
|
|
};
|
|
}
|