mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-05-06 05:48:41 +00:00
Compare commits
5 Commits
42f62f7d5b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 49d2ef3879 | |||
| 97ddc709e8 | |||
| d45ae46fb4 | |||
| 37ba1f05cb | |||
| 01ca9f155e |
Generated
+28
@@ -14,6 +14,9 @@ importers:
|
||||
'@heroicons/react':
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0(react@19.0.0)
|
||||
'@number-flow/react':
|
||||
specifier: 0.5.5
|
||||
version: 0.5.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-accordion':
|
||||
specifier: 1.2.3
|
||||
version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@@ -278,6 +281,12 @@ packages:
|
||||
'@emnapi/core': ^1.7.1
|
||||
'@emnapi/runtime': ^1.7.1
|
||||
|
||||
'@number-flow/react@0.5.5':
|
||||
resolution: {integrity: sha512-Zdju5n0osxrb+7jbcpUJ9L2VJ2+9ptwjz5+A+2wq9Q32hs3PW/noPJjHtLTrtGINM9mEw76DcDg0ac/dx6j1aA==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
react-dom: ^18 || ^19
|
||||
|
||||
'@oxc-project/types@0.122.0':
|
||||
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
|
||||
|
||||
@@ -1268,6 +1277,9 @@ packages:
|
||||
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
esm-env@1.2.2:
|
||||
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
@@ -1444,6 +1456,9 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
number-flow@0.5.3:
|
||||
resolution: {integrity: sha512-iLKyssImNWQmJ41rza9K7P5lHRZTyishi/9FarWPLQHYY2Ydtl6eiXINEjZ1fa8dHeY0O7+YOD+Py3ZsJddYkg==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1793,6 +1808,13 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@number-flow/react@0.5.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
number-flow: 0.5.3
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
'@oxc-project/types@0.122.0': {}
|
||||
|
||||
'@radix-ui/number@1.1.0': {}
|
||||
@@ -2657,6 +2679,8 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.2
|
||||
|
||||
esm-env@1.2.2: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
fast-equals@5.4.0: {}
|
||||
@@ -2776,6 +2800,10 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
number-flow@0.5.3:
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
+22
-25
@@ -10,7 +10,6 @@ import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import Footer from "./components/Footer";
|
||||
import Header, { RefreshToast } from "./components/Header";
|
||||
import { useBackground } from "./hooks/use-background";
|
||||
import { useTheme } from "./hooks/use-theme";
|
||||
import { InjectContext } from "./lib/inject";
|
||||
import { fetchSetting } from "./lib/nezha-api";
|
||||
import { cn } from "./lib/utils";
|
||||
@@ -32,19 +31,19 @@ const MainApp: React.FC = () => {
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
const { i18n } = useTranslation();
|
||||
const { setTheme } = useTheme();
|
||||
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false);
|
||||
const { backgroundImage: customBackgroundImage } = useBackground();
|
||||
|
||||
useEffect(() => {
|
||||
if (settingData?.data?.config?.custom_code) {
|
||||
InjectContext(settingData?.data?.config?.custom_code);
|
||||
const updateConfig = () => {
|
||||
const config = settingData?.data?.config;
|
||||
if (config) {
|
||||
if (config.custom_code) {
|
||||
InjectContext(config.custom_code);
|
||||
setIsCustomCodeInjected(true);
|
||||
}
|
||||
|
||||
// 同步自定义配置到全局变量
|
||||
const config = settingData?.data?.config;
|
||||
if (config) {
|
||||
if (config.custom_logo) window.CustomLogo = config.custom_logo;
|
||||
if (config.custom_description)
|
||||
window.CustomDesc = config.custom_description;
|
||||
@@ -60,23 +59,13 @@ const MainApp: React.FC = () => {
|
||||
}
|
||||
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
|
||||
}
|
||||
};
|
||||
|
||||
updateConfig();
|
||||
const interval = setInterval(updateConfig, 60000); // Check every minute
|
||||
return () => clearInterval(interval);
|
||||
}, [settingData]);
|
||||
|
||||
// 检测是否强制指定了主题颜色
|
||||
const forceTheme =
|
||||
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem("vite-ui-theme");
|
||||
// Only auto-apply ForceTheme if the user hasn't manually picked one (or picked 'system')
|
||||
if (
|
||||
(!savedTheme || savedTheme === "system") &&
|
||||
(forceTheme === "dark" || forceTheme === "light")
|
||||
) {
|
||||
setTheme(forceTheme);
|
||||
}
|
||||
}, [forceTheme, setTheme]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorPage code={500} message={error.message} />;
|
||||
}
|
||||
@@ -107,20 +96,28 @@ const MainApp: React.FC = () => {
|
||||
{customBackgroundImage && (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75",
|
||||
"fixed inset-0 z-0 bg-cover w-screen h-screen bg-no-repeat bg-center transition-none dark:brightness-75",
|
||||
{
|
||||
"hidden sm:block": customMobileBackgroundImage,
|
||||
},
|
||||
)}
|
||||
style={{ backgroundImage: `url(${customBackgroundImage})` }}
|
||||
style={{
|
||||
backgroundImage: `url(${customBackgroundImage})`,
|
||||
backfaceVisibility: 'hidden',
|
||||
perspective: '1000px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{customMobileBackgroundImage && (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75",
|
||||
"fixed inset-0 z-0 bg-cover w-screen h-screen bg-no-repeat bg-center transition-none sm:hidden dark:brightness-75",
|
||||
)}
|
||||
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
|
||||
style={{
|
||||
backgroundImage: `url(${customMobileBackgroundImage})`,
|
||||
backfaceVisibility: 'hidden',
|
||||
perspective: '1000px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +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;
|
||||
@@ -28,40 +29,54 @@ 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);
|
||||
|
||||
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(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.add("disable-transitions");
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
let effectiveTheme = theme;
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
const themeColor =
|
||||
systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", themeColor);
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
root.classList.remove("disable-transitions");
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
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";
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||
root.classList.add(effectiveTheme);
|
||||
const themeColor =
|
||||
effectiveTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", themeColor);
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
root.classList.remove("disable-transitions");
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [theme]);
|
||||
}, [theme, hour, isSystemDark]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"offlineServers": "Offline Servers",
|
||||
"totalBandwidth": "Total Bandwidth",
|
||||
"speed": "Speed",
|
||||
"network": "Network"
|
||||
"network": "Network",
|
||||
"totalDomains": "Total Domains"
|
||||
},
|
||||
"map": {
|
||||
"Distributions": "Servers are distributed in",
|
||||
@@ -100,7 +101,8 @@
|
||||
"theme": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
"system": "System",
|
||||
"scheduled": "Scheduled"
|
||||
},
|
||||
"error": {
|
||||
"pageNotFound": "Page not found",
|
||||
@@ -144,5 +146,12 @@
|
||||
"ToggleLightMode": "Toggle Light Mode",
|
||||
"ToggleDarkMode": "Toggle Dark 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": "离线服务器",
|
||||
"totalBandwidth": "总流量",
|
||||
"speed": "速率",
|
||||
"network": "网络"
|
||||
"network": "网络",
|
||||
"totalDomains": "总域名数"
|
||||
},
|
||||
"map": {
|
||||
"Distributions": "服务器分布在",
|
||||
@@ -101,7 +102,8 @@
|
||||
"theme": {
|
||||
"light": "亮色",
|
||||
"dark": "暗色",
|
||||
"system": "跟随系统"
|
||||
"system": "跟随系统",
|
||||
"scheduled": "定时切换"
|
||||
},
|
||||
"error": {
|
||||
"pageNotFound": "页面不存在",
|
||||
@@ -145,5 +147,12 @@
|
||||
"ToggleLightMode": "切换亮色模式",
|
||||
"ToggleDarkMode": "切换暗色模式",
|
||||
"ToggleSystemMode": "切换系统模式",
|
||||
"Home": "首页"
|
||||
"Home": "首页",
|
||||
"domain": {
|
||||
"registrar": "注册商",
|
||||
"expiryDate": "到期日期",
|
||||
"expiryPrefix": "到期",
|
||||
"days": "天",
|
||||
"unknownRegistrar": "未知注册商"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"offlineServers": "離線伺服器",
|
||||
"totalBandwidth": "總帶寬",
|
||||
"speed": "速率",
|
||||
"network": "網路"
|
||||
"network": "網路",
|
||||
"totalDomains": "總域名數"
|
||||
},
|
||||
"map": {
|
||||
"Distributions": "伺服器分布在",
|
||||
@@ -94,7 +95,8 @@
|
||||
"theme": {
|
||||
"light": "亮色",
|
||||
"dark": "暗色",
|
||||
"system": "跟隨系統"
|
||||
"system": "跟隨系統",
|
||||
"scheduled": "定時切換"
|
||||
},
|
||||
"error": {
|
||||
"pageNotFound": "頁面不存在",
|
||||
@@ -136,6 +138,13 @@
|
||||
"ToggleDarkMode": "切換暗色模式",
|
||||
"ToggleSystemMode": "切換系統模式",
|
||||
"Home": "首頁",
|
||||
"domain": {
|
||||
"registrar": "註冊商",
|
||||
"expiryDate": "到期日期",
|
||||
"expiryPrefix": "到期",
|
||||
"days": "天",
|
||||
"unknownRegistrar": "未知註冊商"
|
||||
},
|
||||
"pwa": {
|
||||
"offlineReady": "可離線使用之應用程式",
|
||||
"newContent": "有新内容可用",
|
||||
|
||||
Reference in New Issue
Block a user