refactor: 全面重构前端 Dashboard 为 TDesign + TanStack Query 分组表格布局
- 卡片式布局改为分组 PrimaryTable,Modal 改为 Drawer - 手写 hooks 替换为 TanStack Query(轮询/缓存/条件查询) - CSS 607行精简至73行,颜色迁移至 TDesign tokens - 可用率进度条颜色按 10% 一档红→绿渐变 - 新增纯函数测试 34 项全通过(排序/筛选/色阶阈值) - 同步更新主 specs 并归档变更文档
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import type { HistoryResponse } from "../../shared/api";
|
||||
|
||||
export function useHistory(targetId: number | null) {
|
||||
const [data, setData] = useState<HistoryResponse>({ items: [], total: 0, page: 1, pageSize: 20 });
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const fetchHistory = useCallback(
|
||||
async (from: string, to: string, page = 1, pageSize = 20) => {
|
||||
if (targetId === null) return;
|
||||
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/targets/${targetId}/history?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&page=${page}&pageSize=${pageSize}`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const result = (await response.json()) as HistoryResponse;
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return;
|
||||
setError(err instanceof Error ? err.message : "请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[targetId],
|
||||
);
|
||||
|
||||
return { data, error, loading, fetchHistory };
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { SummaryResponse } from "../../shared/api";
|
||||
|
||||
export function useSummary(intervalMs = 8000) {
|
||||
const [data, setData] = useState<SummaryResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const fetchSummary = useCallback(async () => {
|
||||
try {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
const response = await fetch("/api/summary", { signal: controller.signal });
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const result = (await response.json()) as SummaryResponse;
|
||||
setData(result);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return;
|
||||
setError(err instanceof Error ? err.message : "请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
fetchSummary();
|
||||
const timer = setInterval(fetchSummary, intervalMs);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [fetchSummary, intervalMs]);
|
||||
|
||||
return { data, error, loading, refresh: fetchSummary };
|
||||
}
|
||||
@@ -1,71 +1,110 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { TargetStatus } from "../../shared/api";
|
||||
import { useTrend } from "./useTrend";
|
||||
import { useHistory } from "./useHistory";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { HistoryResponse, SummaryResponse, TargetStatus, TrendPoint } from "../../shared/api";
|
||||
import { subtractHours } from "../utils/time";
|
||||
|
||||
const queryKeys = {
|
||||
summary: () => ["summary"] as const,
|
||||
targets: () => ["targets"] as const,
|
||||
trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const,
|
||||
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
|
||||
};
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export function useSummary() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.summary(),
|
||||
queryFn: () => fetchJson<SummaryResponse>("/api/summary"),
|
||||
refetchInterval: 8000,
|
||||
refetchIntervalInBackground: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTargets() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.targets(),
|
||||
queryFn: () => fetchJson<TargetStatus[]>("/api/targets"),
|
||||
refetchInterval: 8000,
|
||||
refetchIntervalInBackground: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTargetDetail() {
|
||||
const [selectedTarget, setSelectedTarget] = useState<TargetStatus | null>(null);
|
||||
const [timeFrom, setTimeFrom] = useState<string>("");
|
||||
const [timeTo, setTimeTo] = useState<string>("");
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedTargetId, setSelectedTargetId] = useState<number | null>(null);
|
||||
const [timeFrom, setTimeFrom] = useState("");
|
||||
const [timeTo, setTimeTo] = useState("");
|
||||
const [historyPage, setHistoryPage] = useState(1);
|
||||
|
||||
const { data: trendData, loading: trendLoading, fetchTrend } = useTrend(selectedTarget?.id ?? null);
|
||||
const { data: historyData, loading: historyLoading, fetchHistory } = useHistory(selectedTarget?.id ?? null);
|
||||
const initialFetchRef = useRef(false);
|
||||
const { data: targetsData } = useTargets();
|
||||
|
||||
const openModal = useCallback((target: TargetStatus) => {
|
||||
setSelectedTarget(target);
|
||||
const selectedTarget =
|
||||
selectedTargetId !== null ? (targetsData?.find((t) => t.id === selectedTargetId) ?? null) : null;
|
||||
|
||||
const trend = useQuery({
|
||||
queryKey:
|
||||
selectedTargetId !== null && timeFrom && timeTo
|
||||
? queryKeys.trend(selectedTargetId, timeFrom, timeTo)
|
||||
: ["trend", "disabled"],
|
||||
queryFn: () =>
|
||||
fetchJson<TrendPoint[]>(
|
||||
`/api/targets/${selectedTargetId}/trend?from=${encodeURIComponent(timeFrom)}&to=${encodeURIComponent(timeTo)}`,
|
||||
),
|
||||
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo,
|
||||
});
|
||||
|
||||
const history = useQuery({
|
||||
queryKey:
|
||||
selectedTargetId !== null && timeFrom && timeTo
|
||||
? queryKeys.history(selectedTargetId, timeFrom, timeTo, historyPage)
|
||||
: ["history", "disabled"],
|
||||
queryFn: () =>
|
||||
fetchJson<HistoryResponse>(
|
||||
`/api/targets/${selectedTargetId}/history?from=${encodeURIComponent(timeFrom)}&to=${encodeURIComponent(timeTo)}&page=${historyPage}&pageSize=20`,
|
||||
),
|
||||
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo,
|
||||
});
|
||||
|
||||
const openDrawer = useCallback((target: TargetStatus) => {
|
||||
setSelectedTargetId(target.id);
|
||||
const now = new Date();
|
||||
const from = subtractHours(now, 24);
|
||||
setTimeFrom(from.toISOString());
|
||||
setTimeTo(now.toISOString());
|
||||
initialFetchRef.current = false;
|
||||
setHistoryPage(1);
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setSelectedTarget(null);
|
||||
initialFetchRef.current = false;
|
||||
const closeDrawer = useCallback(() => {
|
||||
setSelectedTargetId(null);
|
||||
queryClient.removeQueries({ queryKey: ["trend"] });
|
||||
queryClient.removeQueries({ queryKey: ["history"] });
|
||||
}, [queryClient]);
|
||||
|
||||
const handleTimeChange = useCallback((from: string, to: string) => {
|
||||
setTimeFrom(from);
|
||||
setTimeTo(to);
|
||||
setHistoryPage(1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTarget && timeFrom && timeTo && !initialFetchRef.current) {
|
||||
initialFetchRef.current = true;
|
||||
fetchTrend(timeFrom, timeTo);
|
||||
fetchHistory(timeFrom, timeTo);
|
||||
}
|
||||
}, [selectedTarget, timeFrom, timeTo, fetchTrend, fetchHistory]);
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(from: string, to: string) => {
|
||||
setTimeFrom(from);
|
||||
setTimeTo(to);
|
||||
if (selectedTarget) {
|
||||
fetchTrend(from, to);
|
||||
fetchHistory(from, to);
|
||||
}
|
||||
},
|
||||
[fetchTrend, fetchHistory, selectedTarget],
|
||||
);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(page: number) => {
|
||||
if (timeFrom && timeTo) {
|
||||
fetchHistory(timeFrom, timeTo, page);
|
||||
}
|
||||
},
|
||||
[timeFrom, timeTo, fetchHistory],
|
||||
);
|
||||
const handlePageChange = useCallback((page: number) => {
|
||||
setHistoryPage(page);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
selectedTarget,
|
||||
trendData,
|
||||
trendLoading,
|
||||
historyData,
|
||||
historyLoading,
|
||||
trendData: trend.data ?? [],
|
||||
trendLoading: trend.isLoading,
|
||||
historyData: history.data ?? { items: [], total: 0, page: 1, pageSize: 20 },
|
||||
historyLoading: history.isLoading,
|
||||
timeFrom,
|
||||
timeTo,
|
||||
openModal,
|
||||
closeModal,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
handleTimeChange,
|
||||
handlePageChange,
|
||||
};
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { TargetStatus } from "../../shared/api";
|
||||
|
||||
export function useTargets(intervalMs = 8000) {
|
||||
const [data, setData] = useState<TargetStatus[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const fetchTargets = useCallback(async () => {
|
||||
try {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
const response = await fetch("/api/targets", { signal: controller.signal });
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const result = (await response.json()) as TargetStatus[];
|
||||
setData(result);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return;
|
||||
setError(err instanceof Error ? err.message : "请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
fetchTargets();
|
||||
const timer = setInterval(fetchTargets, intervalMs);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [fetchTargets, intervalMs]);
|
||||
|
||||
return { data, error, loading, refresh: fetchTargets };
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import type { TrendPoint } from "../../shared/api";
|
||||
|
||||
export function useTrend(targetId: number | null) {
|
||||
const [data, setData] = useState<TrendPoint[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const fetchTrend = useCallback(
|
||||
async (from: string, to: string) => {
|
||||
if (targetId === null) return;
|
||||
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/targets/${targetId}/trend?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const result = (await response.json()) as TrendPoint[];
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return;
|
||||
setError(err instanceof Error ? err.message : "请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[targetId],
|
||||
);
|
||||
|
||||
return { data, error, loading, fetchTrend };
|
||||
}
|
||||
Reference in New Issue
Block a user