1
0

feat: 重构 Dashboard 为卡片式分组布局

表格布局替换为按分组展示的卡片式布局,新增 group 字段配置和 TargetBoard/TargetCard 等组件。模态框详情页支持时间范围筛选和分页,SummaryCards 减为 3 个。API 端点变更:trend/history 改用 from/to 参数,history 支持分页。recentSampleCount 硬编码为 30。
This commit is contained in:
2026-05-11 08:54:21 +08:00
parent b8810f1182
commit 548b44d28e
44 changed files with 1676 additions and 557 deletions

View File

@@ -0,0 +1,35 @@
import { useCallback, 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: 15 });
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const fetchHistory = useCallback(
async (from: string, to: string, page = 1, pageSize = 15) => {
if (targetId === null) return;
setLoading(true);
setError(null);
try {
const response = await fetch(
`/api/targets/${targetId}/history?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&page=${page}&pageSize=${pageSize}`,
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = (await response.json()) as HistoryResponse;
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "请求失败");
} finally {
setLoading(false);
}
},
[targetId],
);
return { data, error, loading, fetchHistory };
}

View File

@@ -0,0 +1,77 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { TargetStatus } from "../../shared/api";
import { useTrend } from "./useTrend";
import { useHistory } from "./useHistory";
export function useTargetDetail() {
const [selectedTarget, setSelectedTarget] = useState<TargetStatus | null>(null);
const [timeFrom, setTimeFrom] = useState<string>("");
const [timeTo, setTimeTo] = useState<string>("");
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 openModal = useCallback((target: TargetStatus) => {
setSelectedTarget(target);
const now = new Date();
const from = subtractHours(now, 24);
setTimeFrom(from.toISOString());
setTimeTo(now.toISOString());
initialFetchRef.current = false;
}, []);
const closeModal = useCallback(() => {
setSelectedTarget(null);
initialFetchRef.current = false;
}, []);
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],
);
return {
selectedTarget,
trendData,
trendLoading,
historyData,
historyLoading,
timeFrom,
timeTo,
openModal,
closeModal,
handleTimeChange,
handlePageChange,
};
}
function subtractHours(date: Date, hours: number): Date {
const result = new Date(date);
result.setTime(result.getTime() - hours * 60 * 60 * 1000);
return result;
}

View File

@@ -6,25 +6,30 @@ export function useTrend(targetId: number | null) {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const fetchTrend = useCallback(async () => {
if (targetId === null) return;
const fetchTrend = useCallback(
async (from: string, to: string) => {
if (targetId === null) return;
setLoading(true);
setError(null);
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/targets/${targetId}/trend?hours=24`);
try {
const response = await fetch(
`/api/targets/${targetId}/trend?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = (await response.json()) as TrendPoint[];
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "请求失败");
} finally {
setLoading(false);
}
}, [targetId]);
const result = (await response.json()) as TrendPoint[];
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "请求失败");
} finally {
setLoading(false);
}
},
[targetId],
);
return { data, error, loading, fetchTrend };
}