feat(web): 定时拉取优化——静默刷新不闪loading + AdminTable稳定排序/键控不重排
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- useAsyncData: dataRef跟踪,首次setLoading(骨架),已有data时不设loading(静默后台拉),成功setData/失败保留旧data
- AdminTable: rows 按 rowKey 升序稳定排序(useMemo,同批数据顺序稳定不跳),稳定key diff复用DOM不整页重绘,error&&!hasRows才显示错误(保留旧表)
- 受益: 所有useAsyncData/AdminTable面板页(会话/记忆/渠道/IoT/插件/人格/用户/RAG/LLM/自动化/可观测/仪表盘)
- npm run build绿(83模块)
This commit is contained in:
Aska committed 2026-09-07 16:27:49 +08:00
1 parent eaca8dc8cc
commit e9df928072
2 files changed
+32 -8

No files matched your search

+21 -5
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from "react";
import { useMemo, type ReactNode } from "react";
import { EmptyState, Tag, type TagTone } from "../components/primitives";
import { Icon } from "../components/Icon";
@@ -79,7 +79,13 @@ export interface AdminTableProps<T> {
}
/** A strict, generic data table. Loading/error/empty states are handled here so
* every module has a complete surface without duplicating state rendering. */
* every module has a complete surface without duplicating state rendering.
*
* Rows are sorted by `rowKey` so the order is deterministic no matter how the
* backend returns them — the same data always renders in the same order (no
* jumping on refresh). Stable keys let React reuse the row DOM and only update
* changed cells. An error is shown only when there is no data to keep on
* screen, so a failed silent refresh keeps the stale list visible. */
export function AdminTable<T>({
columns,
rows,
@@ -89,13 +95,23 @@ export function AdminTable<T>({
rowKey,
onRowClick,
}: AdminTableProps<T>): ReactNode {
const sorted = useMemo(() => {
if (!rows) return rows;
return [...rows].sort((a, b) => {
const ka = rowKey(a);
const kb = rowKey(b);
return ka < kb ? -1 : ka > kb ? 1 : 0;
});
}, [rows, rowKey]);
if (loading) {
return <div className="px-4 py-10 text-center text-xs text-[var(--mu-text-lo)]">加载中…</div>;
}
if (error) {
const hasRows = sorted != null && sorted.length > 0;
if (error && !hasRows) {
return <div className="px-4 py-10 text-center text-xs text-[var(--mu-err)]">{error}</div>;
}
if (!rows || rows.length === 0) {
if (!hasRows) {
return <EmptyState icon={<Icon name="folder" size={32} />} title={emptyTitle} />;
}
return (
@@ -114,7 +130,7 @@ export function AdminTable<T>({
</tr>
</thead>
<tbody>
{rows.map((row) => (
{sorted.map((row) => (
<tr
key={rowKey(row)}
onClick={onRowClick ? () => onRowClick(row) : undefined}