feat: 重构 Dashboard 为卡片式分组布局
表格布局替换为按分组展示的卡片式布局,新增 group 字段配置和 TargetBoard/TargetCard 等组件。模态框详情页支持时间范围筛选和分页,SummaryCards 减为 3 个。API 端点变更:trend/history 改用 from/to 参数,history 支持分页。recentSampleCount 硬编码为 30。
This commit is contained in:
35
src/web/hooks/useHistory.ts
Normal file
35
src/web/hooks/useHistory.ts
Normal 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 };
|
||||
}
|
||||
77
src/web/hooks/useTargetDetail.ts
Normal file
77
src/web/hooks/useTargetDetail.ts
Normal 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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user