121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Circle } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface ActiveNowBadgeProps {
|
|
activeHours: { weekday: string[]; weekend: string[] } | null | undefined;
|
|
className?: string;
|
|
showNextLabel?: boolean;
|
|
}
|
|
|
|
interface ActiveState {
|
|
active: boolean;
|
|
nextLabel: string | null;
|
|
}
|
|
|
|
/**
|
|
* 根据当前时间判断 Bot 是否处于活跃时段
|
|
* 注意:依赖客户端时钟,会在挂载后立即计算并随后每分钟刷新
|
|
*/
|
|
function computeState(
|
|
activeHours: ActiveNowBadgeProps["activeHours"],
|
|
now: Date
|
|
): ActiveState {
|
|
if (!activeHours) return { active: false, nextLabel: null };
|
|
|
|
const day = now.getDay();
|
|
const isWeekend = day === 0 || day === 6;
|
|
const ranges = isWeekend ? activeHours.weekend : activeHours.weekday;
|
|
if (!ranges || ranges.length === 0) return { active: false, nextLabel: null };
|
|
|
|
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
|
|
for (const range of ranges) {
|
|
if (range === "全天" || range === "全天活跃") return { active: true, nextLabel: null };
|
|
const m = range.match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/);
|
|
if (!m) continue;
|
|
const start = parseInt(m[1]) * 60 + parseInt(m[2]);
|
|
const end = parseInt(m[3]) * 60 + parseInt(m[4]);
|
|
if (start === end) continue;
|
|
if (start < end) {
|
|
if (currentMinutes >= start && currentMinutes < end) {
|
|
return { active: true, nextLabel: null };
|
|
}
|
|
} else {
|
|
if (currentMinutes >= start || currentMinutes < end) {
|
|
return { active: true, nextLabel: null };
|
|
}
|
|
}
|
|
}
|
|
|
|
// 不在活跃时段,计算下次活跃时间
|
|
const tomorrowRanges = isWeekend ? activeHours.weekday : activeHours.weekend;
|
|
const findNext = (rs: string[], label: string) => {
|
|
for (const r of rs) {
|
|
if (r === "全天" || r === "全天活跃") return `${label}全天`;
|
|
const m = r.match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/);
|
|
if (!m) continue;
|
|
const start = parseInt(m[1]) * 60 + parseInt(m[2]);
|
|
if (label === "今天" && start <= currentMinutes) continue;
|
|
return `${label} ${m[1]}:${m[2]}`;
|
|
}
|
|
return null;
|
|
};
|
|
return {
|
|
active: false,
|
|
nextLabel: findNext(ranges, "今天") || findNext(tomorrowRanges, "明天"),
|
|
};
|
|
}
|
|
|
|
export default function ActiveNowBadge({ activeHours, className, showNextLabel = false }: ActiveNowBadgeProps) {
|
|
const [state, setState] = useState<ActiveState>({ active: false, nextLabel: null });
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
const tick = () => setState(computeState(activeHours, new Date()));
|
|
tick();
|
|
// 每分钟刷新一次
|
|
const id = setInterval(tick, 60_000);
|
|
return () => clearInterval(id);
|
|
}, [activeHours]);
|
|
|
|
if (!mounted) {
|
|
// 避免 hydration mismatch,先渲染一个不可见占位
|
|
return <span className={cn("inline-block h-4 w-12", className)} aria-hidden />;
|
|
}
|
|
|
|
if (state.active) {
|
|
return (
|
|
<span
|
|
className={cn(
|
|
"inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-200/60",
|
|
className
|
|
)}
|
|
title="现在正在活跃"
|
|
>
|
|
<Circle className="h-1.5 w-1.5 fill-emerald-500 text-emerald-500 animate-pulse" />
|
|
活跃中
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (showNextLabel && state.nextLabel) {
|
|
return (
|
|
<span
|
|
className={cn(
|
|
"inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-muted/40 text-muted-foreground border border-border/40",
|
|
className
|
|
)}
|
|
title={`下次活跃:${state.nextLabel}`}
|
|
>
|
|
{state.nextLabel} 上线
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|