mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-11-04 01:00:20 +00:00
* feat(server-detail): add online history tab Render per-server online periods from GET /api/v1/server/:id/online-history: summary card (current status, online rate, total in window, last active), daily online-time bars, and the raw period list. Window selector offers 1d/7d/30d and locks options beyond the server's max_days; guests hitting the 1d cap see a login hint. i18n keys added for all locales (zh-CN/zh-TW translated, others fall back to English for Weblate). * fix(online-history): keep selector pills above the sliding indicator The active-indicator overlay sits at z-10 and intercepted clicks on the day pills; wrap pills at z-20 like the existing PeriodSelector. * feat(online-history): render daily online as 24h timeline with hover tooltips 每日在线卡片从整日占比进度条改为 0:00–24:00 整轴时间条: 每个在线时段按当日时刻定位为高亮分段,hover 显示起止时间与时长; 进行中时段以亮色高亮。卡片顶部新增共享刻度(0:00 / 12:00 / 24:00)。 * feat(online-history): regroup periods by day as expandable chip wall Rework the Online tab per the chip-wall design: periods are grouped by panel-timezone day with per-day totals, defaulting to the latest day with "Expand more" revealing up to ten more groups per click, plus an Online/Offline switch that derives offline chips as the window complement. - respond to the backend panel timezone (timezone/tz_offset): dayjs 1.11 .tz(input, zone) ignores explicit offsets, so offset-bearing timestamps are parsed first and converted afterwards; naive date strings keep the zone-parse path (panelDay) - guests falling back automatically: a rejected days>1 request locks 7d/30d and retries at 1d instead of surfacing only a login prompt - fix day-selector indicator landing on the wrong pill: useActiveIndicator refs are stored by index, pass the map index not the day value - disable Node experimental webstorage in vitest workers: Node >=25 ships a broken global localStorage that shadows the jsdom one - 14 locales: tabSwitch.Online + serverOnlineHistory.* * feat(online-history): boxed summary stats with desktop/mobile layouts * fix(online-history): align offline gaps, DST coordinates and ongoing chips - extract pure timeline/grouping helpers into src/lib/online-history.ts - clamp offline complements to day and window boundaries, so a gap can no longer run into a later day's first online session - use wall-clock coordinates and true next-midnight boundaries on DST days - label only the segment ending at the live session end as Ongoing - keep group totals equal to the visible chips for cross-day sessions - keep period chips on one line and render next-midnight as 24:00 - add regression tests for multi-day offline gaps, window_start/window_end, spring-forward/fall-back geometry and ongoing clipping * style(online-history): lighten summary metrics and shorten rate bar * feat(online-history): refine UI and unify daily history rendering --------- Co-authored-by: Sam5440 <Sam5440@users.noreply.github.com> Co-authored-by: hamster1963 <1410514192@qq.com>
139 lines
4.4 KiB
TypeScript
139 lines
4.4 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
function Bar({ className }: { className: string }) {
|
|
return (
|
|
<Skeleton
|
|
className={cn(
|
|
"rounded-[5px] bg-stone-200/70 motion-reduce:animate-none dark:bg-stone-700/60",
|
|
className,
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function Metric({ footer = true }: { footer?: boolean }) {
|
|
return (
|
|
<div className="flex min-w-0 flex-col items-center justify-center gap-2">
|
|
<div className="flex h-4 items-center">
|
|
<Bar className="h-3 w-14" />
|
|
</div>
|
|
<Bar className="h-7 w-24 max-w-full" />
|
|
{footer && (
|
|
<div className="flex h-4 max-w-full items-center">
|
|
<Bar className="h-3 w-20 max-w-full" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function OnlineHistorySkeleton({ days = 7 }: { days?: number }) {
|
|
const { t } = useTranslation();
|
|
const dayCount = Math.min(31, Math.max(1, days) + 1);
|
|
const cardClassName = cn({
|
|
"bg-card/70":
|
|
typeof window !== "undefined" && Boolean(window.CustomBackgroundImage),
|
|
});
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
aria-label={t("serverOnlineHistory.loading")}
|
|
aria-busy="true"
|
|
data-testid="online-history-loading"
|
|
>
|
|
<div aria-hidden="true" className="flex flex-col gap-4">
|
|
<div className="flex min-h-9 items-center justify-between gap-3">
|
|
<div className="flex items-center gap-2 md:px-1">
|
|
<Bar className="size-2 rounded-full" />
|
|
<Bar className="h-3 w-8" />
|
|
</div>
|
|
<Bar className="h-9 w-36 rounded-full" />
|
|
</div>
|
|
|
|
<div className="px-0 py-2 md:px-6 md:py-4">
|
|
<div className="hidden grid-cols-4 divide-x divide-stone-100 dark:divide-stone-800 md:grid">
|
|
{Array.from({ length: 4 }, (_, index) => (
|
|
<Metric key={index} />
|
|
))}
|
|
</div>
|
|
<div className="flex flex-col gap-3 md:hidden">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Metric />
|
|
<Metric />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4 border-t border-stone-100 pt-3 dark:border-stone-800">
|
|
<Metric footer={false} />
|
|
<Metric footer={false} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Card className={cardClassName}>
|
|
<CardContent className="px-4 py-3 sm:px-5">
|
|
<div className="flex min-h-8 flex-wrap items-center justify-between gap-2">
|
|
<Bar className="h-4 w-28" />
|
|
<Bar className="h-3 w-32" />
|
|
</div>
|
|
<div className="mt-3 grid grid-cols-1 gap-2 md:grid-cols-2 md:[&>*:last-child:nth-child(odd)]:col-span-2">
|
|
{Array.from({ length: dayCount }, (_, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex min-w-0 flex-col gap-2.5 rounded-lg bg-muted/40 px-2.5 py-2 dark:bg-muted/20"
|
|
>
|
|
<div className="flex h-4 items-center justify-between gap-2">
|
|
<Bar className="h-3 w-20" />
|
|
<Bar className="h-3 w-14" />
|
|
</div>
|
|
<Bar className="h-2 w-full rounded-full" />
|
|
<div className="flex h-[9px] items-center justify-between">
|
|
{Array.from({ length: 5 }, (_, tick) => (
|
|
<Bar key={tick} className="h-1 w-2" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-3 flex h-4 items-center gap-4">
|
|
<Bar className="h-2 w-16" />
|
|
<Bar className="h-2 w-16" />
|
|
<Bar className="h-2 w-20" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className={cardClassName}>
|
|
<CardContent className="px-4 py-3 sm:px-5">
|
|
<div className="flex min-h-8 items-center justify-between gap-2">
|
|
<Bar className="h-4 w-24" />
|
|
<Bar className="-mt-1 -mr-2 h-[30px] w-24 self-start rounded-[8px] sm:-mr-3" />
|
|
</div>
|
|
<div className="mt-3 flex flex-col gap-3">
|
|
{Array.from({ length: Math.min(3, dayCount) }, (_, index) => (
|
|
<div key={index}>
|
|
<div className="flex h-6 items-start justify-between gap-3">
|
|
<Bar className="h-3 w-16" />
|
|
<Bar className="h-3 w-20" />
|
|
</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
<Bar className="h-7 w-36" />
|
|
<Bar className="h-7 w-40" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
{dayCount > 3 && (
|
|
<div className="flex h-8 items-center justify-center rounded-md border border-dashed border-border">
|
|
<Bar className="h-3 w-16" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|