feat: add scheduled theme mode and domain translations

This commit is contained in:
Bot
2026-05-01 13:54:37 +08:00
parent 37ba1f05cb
commit d45ae46fb4
7 changed files with 71 additions and 37 deletions
+8 -24
View File
@@ -1,15 +1,11 @@
// src/components/DomainStatus.tsx (最终完整版)
import { useQuery } from '@tanstack/react-query';
import { getDomains, Domain } from '@/api/domain';
import { CalendarDays, DollarSign } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import RemainPercentBar from "./RemainPercentBar";
// =======================================================
// 彩色备注标签组件
// =======================================================
const DomainNoteTags = ({ notes }: { notes?: string }) => {
if (!notes) {
return null;
@@ -42,11 +38,8 @@ const DomainNoteTags = ({ notes }: { notes?: string }) => {
);
};
// =======================================================
// 行内模式卡片 (Inline Mode Card)
// =======================================================
const DomainCardInline = ({ domain }: { domain: Domain }) => {
const { t } = useTranslation();
const expiresIn = domain.expires_in_days;
const billingData = domain.BillingData || {};
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>
<div className="flex items-center gap-1.5 w-28">
<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 className="flex items-center gap-1.5 w-24">
<DollarSign className="h-3.5 w-3.5" />
<span>{billingData.renewalPrice || 'N/A'}</span>
</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>
<DomainNoteTags notes={billingData.notes} />
@@ -90,17 +83,12 @@ const DomainCardInline = ({ domain }: { domain: Domain }) => {
);
};
// =======================================================
// 卡片模式 (Card Mode)
// =======================================================
const DomainCard = ({ domain }: { domain: Domain }) => {
const { t } = useTranslation();
const expiresIn = domain.expires_in_days;
const billingData = domain.BillingData || {};
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
return (
<a href={`https://${domain.Domain}`} target="_blank" rel="noopener noreferrer" className="block h-full">
<div className={cn(
@@ -111,7 +99,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
<h4 className="font-semibold font-mono tracking-tight">{domain.Domain}</h4>
</div>
<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">
<CalendarDays className="h-3 w-3" />
<span>{billingData.endDate ? new Date(billingData.endDate).toLocaleDateString() : 'N/A'}</span>
@@ -125,7 +113,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
<div className="flex-1">
<RemainPercentBar value={expiresIn ? Math.max(0, Math.min(100, (expiresIn / 365) * 100)) : 100} className="w-full h-1.5" />
</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 className="pt-1">
<DomainNoteTags notes={billingData.notes} />
@@ -135,10 +123,6 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
);
};
// =======================================================
// 主组件 (Main Component)
// =======================================================
export const DomainStatus = () => {
const { data: domains, isLoading, error } = useQuery({
queryKey: ['domains'],
@@ -196,4 +180,4 @@ export const DomainStatus = () => {
))}
</div>
);
};
};
+2 -2
View File
@@ -151,7 +151,7 @@ export default function ServerOverview({
>
<CardContent className="flex h-full items-center px-6 py-3">
<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">
<Globe className="h-4 w-4 text-muted-foreground" />
<div className="text-lg font-semibold">{totalDomains}</div>
@@ -195,7 +195,7 @@ export default function ServerOverview({
</section>
{!disableAnimatedMan && (
<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"}
src={customIllustration}
loading="eager"
+17 -3
View File
@@ -1,7 +1,7 @@
import { createContext, type ReactNode, useEffect, useState } from "react";
import { DateTime } from "luxon";
export type Theme = "dark" | "light" | "system";
export type Theme = "dark" | "light" | "system" | "scheduled";
type ThemeProviderProps = {
children: ReactNode;
@@ -29,13 +29,25 @@ export function ThemeProvider({
() => (localStorage.getItem(storageKey) as Theme) || "system",
);
const [isSystemDark, setIsSystemDark] = useState(
() => window.matchMedia("(prefers-color-scheme: dark)").matches,
);
const [hour, setHour] = useState(() => DateTime.now().hour);
useEffect(() => {
const timer = setInterval(() => {
setHour(DateTime.now().hour);
}, 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(() => {
@@ -46,6 +58,8 @@ export function ThemeProvider({
let effectiveTheme = theme;
if (theme === "system") {
effectiveTheme = isSystemDark ? "dark" : "light";
} else if (theme === "scheduled") {
// Time-based theme: 18:00 - 06:00 is dark
const isNight = hour >= 18 || hour < 6;
effectiveTheme = isNight ? "dark" : "light";
@@ -62,7 +76,7 @@ export function ThemeProvider({
root.classList.remove("disable-transitions");
}, 0);
return () => window.clearTimeout(timeoutId);
}, [theme, hour]);
}, [theme, hour, isSystemDark]);
const value = {
theme,
+9
View File
@@ -70,6 +70,15 @@ export function ModeToggle() {
{t("theme.system")}
{theme === "system" && <CheckCircleIcon className="size-4" />}
</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>
</DropdownMenu>
);