From c29e39df1369ce820523c7dfb35839db47d56e96 Mon Sep 17 00:00:00 2001 From: luorijun Date: Thu, 15 Oct 2026 17:04:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A6=96=E9=A1=B5=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(root)/(dashboard)/data.ts | 68 +++++ src/app/(root)/(dashboard)/page.tsx | 430 ++++++++++++---------------- 2 files changed, 259 insertions(+), 239 deletions(-) create mode 100644 src/app/(root)/(dashboard)/data.ts diff --git a/src/app/(root)/(dashboard)/data.ts b/src/app/(root)/(dashboard)/data.ts new file mode 100644 index 0000000..d3ef813 --- /dev/null +++ b/src/app/(root)/(dashboard)/data.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useRef, useState } from "react" +import type { ApiResponse } from "@/lib/api" + +export function useData(load: () => Promise>) { + const request = useRef(0) + const [state, setState] = useState<{ + source: typeof load + data: T | null + loading: boolean + error: string | null + updatedAt: Date | null + }>({ source: load, data: null, loading: true, error: null, updatedAt: null }) + + const reload = useCallback(async () => { + const id = ++request.current + setState(prev => ({ + source: load, + data: prev.source === load ? prev.data : null, + updatedAt: prev.source === load ? prev.updatedAt : null, + loading: true, + error: null, + })) + + try { + const resp = await load() + if (id !== request.current) return + + if (resp.success) { + setState({ + source: load, + data: resp.data, + loading: false, + error: null, + updatedAt: new Date(), + }) + } else { + setState(prev => ({ + ...prev, + loading: false, + error: resp.message || "请求失败,请重试", + })) + } + } catch { + if (id !== request.current) return + setState(prev => ({ + ...prev, + loading: false, + error: "请求失败,请重试", + })) + } + }, [load]) + + useEffect(() => { + void reload() + return () => { + request.current++ + } + }, [reload]) + + const current = state.source === load + return { + data: current ? state.data : null, + loading: !current || state.loading, + error: current ? state.error : null, + updatedAt: current ? state.updatedAt : null, + reload, + } +} diff --git a/src/app/(root)/(dashboard)/page.tsx b/src/app/(root)/(dashboard)/page.tsx index 89827bf..af2126a 100644 --- a/src/app/(root)/(dashboard)/page.tsx +++ b/src/app/(root)/(dashboard)/page.tsx @@ -1,10 +1,14 @@ "use client" -import { format, subDays } from "date-fns" +import { + differenceInCalendarDays, + format, + isValid, + parseISO, + subDays, +} from "date-fns" import { Activity, - ArrowDownRight, - ArrowUpRight, BarChart3, DoorOpenIcon, Server, @@ -13,19 +17,11 @@ import { Users, } from "lucide-react" import Link from "next/link" -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useMemo, useState } from "react" import { getExtractable, getOverview, getTrend } from "@/actions/dashboard" -import { - SimpleBarChart, - SimpleHorizontalBarChart, - SimpleLineChart, -} from "@/components/charts" +import { SimpleBarChart, SimpleLineChart } from "@/components/charts" import { Page } from "@/components/page" -import type { - DashboardExtractable, - DashboardOverview, - DashboardTrendItem, -} from "@/models/dashboard" +import { useData } from "./data" type TimeRange = "today" | "7d" | "30d" | "90d" | "custom" @@ -37,125 +33,76 @@ const timeRangeLabels: Record = { custom: "自定义", } -function getDateRange( - range: TimeRange, - customStart: string, - customEnd: string, -) { - const today = format(new Date(), "yyyy-MM-dd") - switch (range) { - case "today": - return { date_start: today, date_end: today } - case "7d": - return { - date_start: format(subDays(new Date(), 6), "yyyy-MM-dd"), - date_end: today, - } - case "30d": - return { - date_start: format(subDays(new Date(), 29), "yyyy-MM-dd"), - date_end: today, - } - case "90d": - return { - date_start: format(subDays(new Date(), 89), "yyyy-MM-dd"), - date_end: today, - } - case "custom": - return { date_start: customStart, date_end: customEnd } +function getDateRange(range: Exclude) { + const now = new Date() + const days = { today: 0, "7d": 6, "30d": 29, "90d": 89 } + return { + date_start: format(subDays(now, days[range]), "yyyy-MM-dd"), + date_end: format(now, "yyyy-MM-dd"), } } -function mulberry32(seed: number) { - let t = seed - return () => { - t |= 0 - t = (t + 0x6d2b79f5) | 0 - let n = Math.imul(t ^ (t >>> 15), 1 | t) - n = (n + Math.imul(n ^ (n >>> 7), 61 | n)) ^ n - return ((n ^ (n >>> 14)) >>> 0) / 4294967296 - } -} - -const mockUserData = (() => { - const rng = mulberry32(42) - return [ - { name: "张三", activity: Math.floor(rng() * 100) + 200 }, - { name: "李四", activity: Math.floor(rng() * 100) + 180 }, - { name: "王五", activity: Math.floor(rng() * 100) + 160 }, - { name: "赵六", activity: Math.floor(rng() * 100) + 140 }, - { name: "孙七", activity: Math.floor(rng() * 100) + 120 }, - { name: "周八", activity: Math.floor(rng() * 100) + 90 }, - { name: "吴九", activity: Math.floor(rng() * 100) + 60 }, - ] -})() - export default function DashboardPage() { - const [timeRange, setTimeRange] = useState("7d") + const [period, setPeriod] = useState(() => ({ + key: "7d" as TimeRange, + ...getDateRange("7d"), + })) const [showCustomPicker, setShowCustomPicker] = useState(false) const [startDate, setStartDate] = useState("") const [endDate, setEndDate] = useState("") + const [dateError, setDateError] = useState(null) - const [overview, setOverview] = useState(null) - const [trendData, setTrendData] = useState([]) - const [extractable, setExtractable] = useState( - null, + const overviewState = useData(getOverview) + const extractableState = useData(getExtractable) + const loadTrend = useCallback( + () => + getTrend({ date_start: period.date_start, date_end: period.date_end }), + [period.date_start, period.date_end], ) - const [lastUpdated, setLastUpdated] = useState("--") + const trendState = useData(loadTrend) + const overview = overviewState.data + const extractable = extractableState.data - const fetchData = useCallback( - async (range: TimeRange, customStart: string, customEnd: string) => { - const [overviewResp, trendResp, extractableResp] = await Promise.all([ - getOverview(), - getTrend(getDateRange(range, customStart, customEnd)), - getExtractable(), - ]) - console.log(overviewResp, "overviewResp") - console.log(trendResp, "trendResp") - console.log(extractableResp, "extractableResp") - - if (overviewResp.success) setOverview(overviewResp.data) - if (trendResp.success) setTrendData(trendResp.data) - if (extractableResp.success) setExtractable(extractableResp.data) - setLastUpdated(format(new Date(), "yyyy-MM-dd HH:mm:ss")) - }, - [], - ) - - useEffect(() => { - fetchData(timeRange, startDate, endDate) - }, [fetchData, timeRange, startDate, endDate]) - - const handleTimeRangeChange = useCallback( - (key: TimeRange) => { - if (key === "custom") { - setShowCustomPicker(true) - setTimeRange("custom") - setStartDate("") - setEndDate("") - } else { - setTimeRange(key) - setShowCustomPicker(false) - fetchData(key, "", "") - } - }, - [fetchData], - ) + const handleTimeRangeChange = (key: TimeRange) => { + setDateError(null) + if (key === "custom") { + setStartDate(period.date_start) + setEndDate(period.date_end) + setShowCustomPicker(true) + } else { + setPeriod({ key, ...getDateRange(key) }) + setShowCustomPicker(false) + } + } const handleDateConfirm = () => { - if (startDate && endDate) { - setShowCustomPicker(false) - fetchData("custom", startDate, endDate) + const start = parseISO(startDate) + const end = parseISO(endDate) + if (!isValid(start) || !isValid(end)) { + setDateError("请选择有效的开始和结束日期") + return } + const days = differenceInCalendarDays(end, start) + 1 + if (days < 1) { + setDateError("结束日期不能早于开始日期") + return + } + if (days > 366) { + setDateError("日期范围不能超过 366 天") + return + } + setDateError(null) + setPeriod({ key: "custom", date_start: startDate, date_end: endDate }) + setShowCustomPicker(false) } const handleCancel = () => { setShowCustomPicker(false) - setTimeRange("7d") - fetchData("7d", "", "") + setDateError(null) } const chartData = useMemo(() => { + const trendData = trendState.data ?? [] const proxyData = trendData.map(item => ({ date: item.date.slice(5), value: item.active_channel_count, @@ -172,19 +119,13 @@ export default function DashboardPage() { date: item.date.slice(5), value: item.extract_count, })) - return { proxyData, tradeData, customerData, extractData } - }, [trendData]) + }, [trendState.data]) return (
-
-

IP代理管理控制台

-

- 上次更新: {lastUpdated} -

-
+

IP代理管理控制台

+
(
)}
-
- -
-
-

每日活跃代理

- -
- -
-

每日交易额

- -
- -
-

每日新增客户

- -
- -
-

每日提取量

- + {dateError && ( +

+ {dateError} +

+ )} +
+ + {period.date_start} ~ {period.date_end} + +
+ + {trendState.loading || trendState.error ? ( +
+ {trendState.loading + ? "正在加载趋势数据…" + : "趋势数据加载失败,请重试"} +
+ ) : ( +
+
+

每日活跃代理

+ +
+
+

每日交易额

+ +
+
+

每日新增客户

+ +
+
+

每日提取量

+ +
+
+ )}
@@ -372,18 +332,17 @@ export default function DashboardPage() {

套餐统计

+
+ +
总可提取量 @@ -394,21 +353,6 @@ export default function DashboardPage() {
- -
-
-

最近活跃用户

-
-
- -
-
@@ -418,16 +362,12 @@ export default function DashboardPage() { function StatCard({ title, value, - change, - trend, icon: Icon, iconColor, iconBg, }: { title: string value: string - change: string - trend: "up" | "down" icon: React.ComponentType<{ className?: string }> iconColor: string iconBg: string @@ -438,19 +378,6 @@ function StatCard({

{title}

{value}

-
- {trend === "up" ? ( - - ) : ( - - )} - - {change} - - 较昨日 -
@@ -460,31 +387,56 @@ function StatCard({ ) } -function ResourceItem({ - label, - active, - total, - color, -}: { - label: string - active: string - total: string - color: string -}) { +function ResourceItem({ label, total }: { label: string; total: string }) { return ( -
-
- {label} - - {active} / {total} - -
-
-
-
+
+ {label} + {total} +
+ ) +} + +function DataStatus({ + data, + loading, + error, + updatedAt, + reload, + showStale = true, +}: { + data: unknown + loading: boolean + error: string | null + updatedAt: Date | null + reload: () => Promise + showStale?: boolean +}) { + return ( +
+ {loading ? ( + + {data && showStale ? "正在更新,当前显示上次数据…" : "正在加载…"} + + ) : error ? ( + + {error} + {data && showStale ? ",当前显示上次数据" : ""} + + ) : null} + {updatedAt && (showStale || (!loading && !error)) && ( + 更新时间:{format(updatedAt, "yyyy-MM-dd HH:mm:ss")} + )} +
) }