mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-05-06 13:58:43 +00:00
feat: add scheduled theme mode and domain translations
This commit is contained in:
@@ -1,15 +1,11 @@
|
|||||||
// src/components/DomainStatus.tsx (最终完整版)
|
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { getDomains, Domain } from '@/api/domain';
|
import { getDomains, Domain } from '@/api/domain';
|
||||||
import { CalendarDays, DollarSign } from 'lucide-react';
|
import { CalendarDays, DollarSign } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import RemainPercentBar from "./RemainPercentBar";
|
import RemainPercentBar from "./RemainPercentBar";
|
||||||
|
|
||||||
// =======================================================
|
|
||||||
// 彩色备注标签组件
|
|
||||||
// =======================================================
|
|
||||||
const DomainNoteTags = ({ notes }: { notes?: string }) => {
|
const DomainNoteTags = ({ notes }: { notes?: string }) => {
|
||||||
if (!notes) {
|
if (!notes) {
|
||||||
return null;
|
return null;
|
||||||
@@ -42,11 +38,8 @@ const DomainNoteTags = ({ notes }: { notes?: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// =======================================================
|
|
||||||
// 行内模式卡片 (Inline Mode Card)
|
|
||||||
// =======================================================
|
|
||||||
const DomainCardInline = ({ domain }: { domain: Domain }) => {
|
const DomainCardInline = ({ domain }: { domain: Domain }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const expiresIn = domain.expires_in_days;
|
const expiresIn = domain.expires_in_days;
|
||||||
const billingData = domain.BillingData || {};
|
const billingData = domain.BillingData || {};
|
||||||
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
|
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
|
||||||
@@ -75,13 +68,13 @@ const DomainCardInline = ({ domain }: { domain: Domain }) => {
|
|||||||
<span className="w-24 truncate">{billingData.registrar || 'N/A'}</span>
|
<span className="w-24 truncate">{billingData.registrar || 'N/A'}</span>
|
||||||
<div className="flex items-center gap-1.5 w-28">
|
<div className="flex items-center gap-1.5 w-28">
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
<CalendarDays className="h-3.5 w-3.5" />
|
||||||
<span>到期: {billingData.endDate ? new Date(billingData.endDate).toLocaleDateString() : 'N/A'}</span>
|
<span>{t('domain.expiryPrefix')}: {billingData.endDate ? new Date(billingData.endDate).toLocaleDateString() : 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 w-24">
|
<div className="flex items-center gap-1.5 w-24">
|
||||||
<DollarSign className="h-3.5 w-3.5" />
|
<DollarSign className="h-3.5 w-3.5" />
|
||||||
<span>{billingData.renewalPrice || 'N/A'}</span>
|
<span>{billingData.renewalPrice || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold w-24">{expiresIn !== undefined ? `${expiresIn} 天` : 'N/A'}</span>
|
<span className="font-semibold w-24">{expiresIn !== undefined ? `${expiresIn} ${t('domain.days')}` : 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DomainNoteTags notes={billingData.notes} />
|
<DomainNoteTags notes={billingData.notes} />
|
||||||
@@ -90,17 +83,12 @@ const DomainCardInline = ({ domain }: { domain: Domain }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// =======================================================
|
|
||||||
// 卡片模式 (Card Mode)
|
|
||||||
// =======================================================
|
|
||||||
const DomainCard = ({ domain }: { domain: Domain }) => {
|
const DomainCard = ({ domain }: { domain: Domain }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const expiresIn = domain.expires_in_days;
|
const expiresIn = domain.expires_in_days;
|
||||||
const billingData = domain.BillingData || {};
|
const billingData = domain.BillingData || {};
|
||||||
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
|
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={`https://${domain.Domain}`} target="_blank" rel="noopener noreferrer" className="block h-full">
|
<a href={`https://${domain.Domain}`} target="_blank" rel="noopener noreferrer" className="block h-full">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
@@ -111,7 +99,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
|
|||||||
<h4 className="font-semibold font-mono tracking-tight">{domain.Domain}</h4>
|
<h4 className="font-semibold font-mono tracking-tight">{domain.Domain}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||||
<span className="font-medium">{billingData.registrar || '未知注册商'}</span>
|
<span className="font-medium">{billingData.registrar || t('domain.unknownRegistrar')}</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<CalendarDays className="h-3 w-3" />
|
<CalendarDays className="h-3 w-3" />
|
||||||
<span>{billingData.endDate ? new Date(billingData.endDate).toLocaleDateString() : 'N/A'}</span>
|
<span>{billingData.endDate ? new Date(billingData.endDate).toLocaleDateString() : 'N/A'}</span>
|
||||||
@@ -125,7 +113,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<RemainPercentBar value={expiresIn ? Math.max(0, Math.min(100, (expiresIn / 365) * 100)) : 100} className="w-full h-1.5" />
|
<RemainPercentBar value={expiresIn ? Math.max(0, Math.min(100, (expiresIn / 365) * 100)) : 100} className="w-full h-1.5" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-muted-foreground w-12 text-right">{expiresIn !== undefined ? `${expiresIn}天` : 'N/A'}</span>
|
<span className="font-medium text-muted-foreground w-12 text-right">{expiresIn !== undefined ? `${expiresIn}${t('domain.days')}` : 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<DomainNoteTags notes={billingData.notes} />
|
<DomainNoteTags notes={billingData.notes} />
|
||||||
@@ -135,10 +123,6 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// =======================================================
|
|
||||||
// 主组件 (Main Component)
|
|
||||||
// =======================================================
|
|
||||||
export const DomainStatus = () => {
|
export const DomainStatus = () => {
|
||||||
const { data: domains, isLoading, error } = useQuery({
|
const { data: domains, isLoading, error } = useQuery({
|
||||||
queryKey: ['domains'],
|
queryKey: ['domains'],
|
||||||
@@ -196,4 +180,4 @@ export const DomainStatus = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export default function ServerOverview({
|
|||||||
>
|
>
|
||||||
<CardContent className="flex h-full items-center px-6 py-3">
|
<CardContent className="flex h-full items-center px-6 py-3">
|
||||||
<section className="flex flex-col gap-1">
|
<section className="flex flex-col gap-1">
|
||||||
<p className="text-sm font-medium md:text-base">总域名数</p>
|
<p className="text-sm font-medium md:text-base">{t("serverOverview.totalDomains")}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||||
<div className="text-lg font-semibold">{totalDomains}</div>
|
<div className="text-lg font-semibold">{totalDomains}</div>
|
||||||
@@ -195,7 +195,7 @@ export default function ServerOverview({
|
|||||||
</section>
|
</section>
|
||||||
{!disableAnimatedMan && (
|
{!disableAnimatedMan && (
|
||||||
<img
|
<img
|
||||||
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
|
className="absolute right-2 top-[-72px] z-50 w-24 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
|
||||||
alt={"animated-man"}
|
alt={"animated-man"}
|
||||||
src={customIllustration}
|
src={customIllustration}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createContext, type ReactNode, useEffect, useState } from "react";
|
import { createContext, type ReactNode, useEffect, useState } from "react";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
export type Theme = "dark" | "light" | "system";
|
export type Theme = "dark" | "light" | "system" | "scheduled";
|
||||||
|
|
||||||
type ThemeProviderProps = {
|
type ThemeProviderProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -29,13 +29,25 @@ export function ThemeProvider({
|
|||||||
() => (localStorage.getItem(storageKey) as Theme) || "system",
|
() => (localStorage.getItem(storageKey) as Theme) || "system",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isSystemDark, setIsSystemDark] = useState(
|
||||||
|
() => window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||||
|
);
|
||||||
|
|
||||||
const [hour, setHour] = useState(() => DateTime.now().hour);
|
const [hour, setHour] = useState(() => DateTime.now().hour);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setHour(DateTime.now().hour);
|
setHour(DateTime.now().hour);
|
||||||
}, 60000);
|
}, 60000);
|
||||||
return () => clearInterval(timer);
|
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const handler = (e: MediaQueryListEvent) => setIsSystemDark(e.matches);
|
||||||
|
mediaQuery.addEventListener("change", handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
mediaQuery.removeEventListener("change", handler);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -46,6 +58,8 @@ export function ThemeProvider({
|
|||||||
|
|
||||||
let effectiveTheme = theme;
|
let effectiveTheme = theme;
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
|
effectiveTheme = isSystemDark ? "dark" : "light";
|
||||||
|
} else if (theme === "scheduled") {
|
||||||
// Time-based theme: 18:00 - 06:00 is dark
|
// Time-based theme: 18:00 - 06:00 is dark
|
||||||
const isNight = hour >= 18 || hour < 6;
|
const isNight = hour >= 18 || hour < 6;
|
||||||
effectiveTheme = isNight ? "dark" : "light";
|
effectiveTheme = isNight ? "dark" : "light";
|
||||||
@@ -62,7 +76,7 @@ export function ThemeProvider({
|
|||||||
root.classList.remove("disable-transitions");
|
root.classList.remove("disable-transitions");
|
||||||
}, 0);
|
}, 0);
|
||||||
return () => window.clearTimeout(timeoutId);
|
return () => window.clearTimeout(timeoutId);
|
||||||
}, [theme, hour]);
|
}, [theme, hour, isSystemDark]);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
theme,
|
theme,
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ export function ModeToggle() {
|
|||||||
{t("theme.system")}
|
{t("theme.system")}
|
||||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={cn("rounded-[5px] text-xs", {
|
||||||
|
"gap-3 bg-muted font-semibold": theme === "scheduled",
|
||||||
|
})}
|
||||||
|
onSelect={(e) => handleSelect(e, "scheduled")}
|
||||||
|
>
|
||||||
|
{t("theme.scheduled")}
|
||||||
|
{theme === "scheduled" && <CheckCircleIcon className="size-4" />}
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"offlineServers": "Offline Servers",
|
"offlineServers": "Offline Servers",
|
||||||
"totalBandwidth": "Total Bandwidth",
|
"totalBandwidth": "Total Bandwidth",
|
||||||
"speed": "Speed",
|
"speed": "Speed",
|
||||||
"network": "Network"
|
"network": "Network",
|
||||||
|
"totalDomains": "Total Domains"
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"Distributions": "Servers are distributed in",
|
"Distributions": "Servers are distributed in",
|
||||||
@@ -100,7 +101,8 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"system": "System"
|
"system": "System",
|
||||||
|
"scheduled": "Scheduled"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"pageNotFound": "Page not found",
|
"pageNotFound": "Page not found",
|
||||||
@@ -144,5 +146,12 @@
|
|||||||
"ToggleLightMode": "Toggle Light Mode",
|
"ToggleLightMode": "Toggle Light Mode",
|
||||||
"ToggleDarkMode": "Toggle Dark Mode",
|
"ToggleDarkMode": "Toggle Dark Mode",
|
||||||
"ToggleSystemMode": "Toggle System Mode",
|
"ToggleSystemMode": "Toggle System Mode",
|
||||||
"Home": "Home"
|
"Home": "Home",
|
||||||
|
"domain": {
|
||||||
|
"registrar": "Registrar",
|
||||||
|
"expiryDate": "Expiry Date",
|
||||||
|
"expiryPrefix": "Expires",
|
||||||
|
"days": "Days",
|
||||||
|
"unknownRegistrar": "Unknown Registrar"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"offlineServers": "离线服务器",
|
"offlineServers": "离线服务器",
|
||||||
"totalBandwidth": "总流量",
|
"totalBandwidth": "总流量",
|
||||||
"speed": "速率",
|
"speed": "速率",
|
||||||
"network": "网络"
|
"network": "网络",
|
||||||
|
"totalDomains": "总域名数"
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"Distributions": "服务器分布在",
|
"Distributions": "服务器分布在",
|
||||||
@@ -101,7 +102,8 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"light": "亮色",
|
"light": "亮色",
|
||||||
"dark": "暗色",
|
"dark": "暗色",
|
||||||
"system": "跟随系统"
|
"system": "跟随系统",
|
||||||
|
"scheduled": "定时切换"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"pageNotFound": "页面不存在",
|
"pageNotFound": "页面不存在",
|
||||||
@@ -145,5 +147,12 @@
|
|||||||
"ToggleLightMode": "切换亮色模式",
|
"ToggleLightMode": "切换亮色模式",
|
||||||
"ToggleDarkMode": "切换暗色模式",
|
"ToggleDarkMode": "切换暗色模式",
|
||||||
"ToggleSystemMode": "切换系统模式",
|
"ToggleSystemMode": "切换系统模式",
|
||||||
"Home": "首页"
|
"Home": "首页",
|
||||||
|
"domain": {
|
||||||
|
"registrar": "注册商",
|
||||||
|
"expiryDate": "到期日期",
|
||||||
|
"expiryPrefix": "到期",
|
||||||
|
"days": "天",
|
||||||
|
"unknownRegistrar": "未知注册商"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"offlineServers": "離線伺服器",
|
"offlineServers": "離線伺服器",
|
||||||
"totalBandwidth": "總帶寬",
|
"totalBandwidth": "總帶寬",
|
||||||
"speed": "速率",
|
"speed": "速率",
|
||||||
"network": "網路"
|
"network": "網路",
|
||||||
|
"totalDomains": "總域名數"
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"Distributions": "伺服器分布在",
|
"Distributions": "伺服器分布在",
|
||||||
@@ -94,7 +95,8 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"light": "亮色",
|
"light": "亮色",
|
||||||
"dark": "暗色",
|
"dark": "暗色",
|
||||||
"system": "跟隨系統"
|
"system": "跟隨系統",
|
||||||
|
"scheduled": "定時切換"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"pageNotFound": "頁面不存在",
|
"pageNotFound": "頁面不存在",
|
||||||
@@ -136,6 +138,13 @@
|
|||||||
"ToggleDarkMode": "切換暗色模式",
|
"ToggleDarkMode": "切換暗色模式",
|
||||||
"ToggleSystemMode": "切換系統模式",
|
"ToggleSystemMode": "切換系統模式",
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
|
"domain": {
|
||||||
|
"registrar": "註冊商",
|
||||||
|
"expiryDate": "到期日期",
|
||||||
|
"expiryPrefix": "到期",
|
||||||
|
"days": "天",
|
||||||
|
"unknownRegistrar": "未知註冊商"
|
||||||
|
},
|
||||||
"pwa": {
|
"pwa": {
|
||||||
"offlineReady": "可離線使用之應用程式",
|
"offlineReady": "可離線使用之應用程式",
|
||||||
"newContent": "有新内容可用",
|
"newContent": "有新内容可用",
|
||||||
|
|||||||
Reference in New Issue
Block a user