Files
zhuiguang-ai/src/components/forum/ActiveNowBadge.tsx
T

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;
}