refactor(admin): remove window globals, move AdminLayout to layouts/

运行时配置不再通过 window.PikaRuntime/window.SystemConfig 全局变量传递:
- 删除 global.d.ts 和 main.tsx 的预加载逻辑,直接渲染
- 新增 api/runtime.ts:useRuntimeConfig hook 通过 React Query 从 /api/config 获取
- Header/Sider/Login 改用 useRuntimeConfig 替代 window.SystemConfig
- ColorModeContext 改用 useRuntimeConfig 替代 window.PikaRuntime
- SystemConfig 保存后 invalidate runtime-config 缓存

AdminLayout 从 pages/ 移到 layouts/(布局框架不是页面)。
删除 components/index.ts(多余的 barrel export),PageHeader 改为直接导入。
This commit is contained in:
dushixiang committed 2026-08-09 23:40:04 +08:00
1 parent cda2585f1a
commit 6ab1876899
21 files changed
+88 -82

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
import {useQuery} from '@tanstack/react-query';
export interface RuntimeConfig {
apiVersion: string;
system: {
nameZh: string;
nameEn: string;
logo: string;
icpCode: string;
version: string;
defaultView: string;
defaultColorMode: 'light' | 'dark' | 'system';
};
theme: {id: string; version: string};
features: Record<string, boolean>;
}
const fallback: RuntimeConfig = {
apiVersion: 'v1',
system: {
nameZh: '皮卡监控',
nameEn: 'Pika Monitor',
logo: '/api/logo',
icpCode: '',
version: '',
defaultView: 'grid',
defaultColorMode: 'system',
},
theme: {id: 'default', version: ''},
features: {},
};
/** 从 /api/config 获取运行时配置(系统名称、logo、版本等)。失败时返回 fallback。 */
export const useRuntimeConfig = () => {
return useQuery({
queryKey: ['runtime-config'],
queryFn: async (): Promise<RuntimeConfig> => {
const res = await fetch('/api/config');
if (!res.ok) throw new Error('无法加载运行时配置');
return res.json();
},
staleTime: 5 * 60 * 1000,
retry: 1,
});
};
export {fallback as defaultRuntimeConfig};
-2
View File
@@ -1,2 +0,0 @@
export { PageHeader } from './PageHeader';
export type { Action } from './PageHeader';
-30
View File
@@ -1,30 +0,0 @@
export {};
declare global {
interface AdminRuntimeConfig {
apiVersion: 'v1';
system: {
nameZh: string;
nameEn: string;
logo: string;
icpCode: string;
version: string;
defaultView: string;
defaultColorMode: 'light' | 'dark' | 'system';
};
theme: {id: string; version: string};
features: Record<string, boolean>;
legacySystemConfig: Window['SystemConfig'];
}
interface Window {
PikaRuntime: AdminRuntimeConfig;
SystemConfig: {
SystemNameZh: string;
SystemNameEn: string;
ICPCode: string;
DefaultView: string;
Version: string;
};
}
}
@@ -4,6 +4,7 @@ import type {MenuProps} from 'antd';
import {App as AntApp, Avatar, Button, Dropdown, Space} from 'antd';
import {BookOpen, Eye, LogOut, Moon, Sun, User as UserIcon} from 'lucide-react';
import {logout} from '@/api/auth';
import {useRuntimeConfig} from '@/api/runtime';
import type {User} from '@/types';
interface HeaderProps {
@@ -16,6 +17,7 @@ interface HeaderProps {
export const AdminHeader = ({userInfo, appliedTheme, themeButtonRef, onToggleTheme}: HeaderProps) => {
const navigate = useNavigate();
const {message: messageApi, modal} = AntApp.useApp();
const {data: runtime} = useRuntimeConfig();
const handleLogout = () => {
modal.confirm({
@@ -51,7 +53,7 @@ export const AdminHeader = ({userInfo, appliedTheme, themeButtonRef, onToggleThe
/>
</div>
<div>
<p className="text-xs uppercase tracking-[0.3em] text-white/60">{window.SystemConfig?.SystemNameZh}</p>
<p className="text-xs uppercase tracking-[0.3em] text-white/60">{runtime?.system.nameZh}</p>
<p className="text-sm font-semibold">控制台</p>
</div>
</div>
@@ -1,11 +1,13 @@
import {useLocation, useNavigate} from 'react-router-dom';
import {menuItems, SIDEBAR_WIDTH, HEADER_HEIGHT} from './menu';
import {cn} from '@/lib/utils';
import {useRuntimeConfig} from '@/api/runtime';
import type {VersionInfo} from '@/api/version';
export const AdminSider = ({version}: {version?: VersionInfo}) => {
const location = useLocation();
const navigate = useNavigate();
const {data: runtime} = useRuntimeConfig();
return (
<aside
@@ -52,7 +54,7 @@ export const AdminSider = ({version}: {version?: VersionInfo}) => {
<div className="mt-2">
<p className="text-sm font-semibold text-gray-900 dark:text-slate-100">Server: {version.version}</p>
<p className="text-sm font-semibold text-gray-900 dark:text-slate-100">Agent: {version.agentVersion}</p>
<p className="text-[11px] text-gray-500 dark:text-slate-400 uppercase tracking-[0.1em]">{window.SystemConfig?.SystemNameEn}</p>
<p className="text-[11px] text-gray-500 dark:text-slate-400 uppercase tracking-[0.1em]">{runtime?.system.nameEn}</p>
</div>
</div>
</div>
File renamed without changes.
File renamed without changes.
+11 -28
View File
@@ -8,31 +8,14 @@ import './index.css';
dayjs.locale('zh-cn');
// 异步加载运行时配置,不阻塞渲染
fetch('/api/config')
.then((res) => res.ok ? res.json() : Promise.reject())
.then((payload) => {
window.PikaRuntime = payload;
window.SystemConfig = payload.legacySystemConfig;
})
.catch(() => {
window.SystemConfig = {
SystemNameZh: '皮卡监控',
SystemNameEn: 'Pika Monitor',
ICPCode: '',
DefaultView: 'grid',
Version: '',
};
})
.finally(() => {
const queryClient = new QueryClient({
defaultOptions: {queries: {refetchOnWindowFocus: false, retry: 1}},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<AdminApp/>
</QueryClientProvider>
</StrictMode>,
);
});
const queryClient = new QueryClient({
defaultOptions: {queries: {refetchOnWindowFocus: false, retry: 1}},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<AdminApp/>
</QueryClientProvider>
</StrictMode>,
);
+1 -1
View File
@@ -9,7 +9,7 @@ import dayjs from 'dayjs';
import {deleteAgent, getTags, listAgentsByAdmin} from '@/api/agent.ts';
import type {Agent} from '@/types';
import {getErrorMessage} from '@/lib/utils';
import {PageHeader} from '@/components';
import {PageHeader} from '@/components/PageHeader';
import AgentEditModal from './AgentEditModal';
import BatchTagsModal from './BatchTagsModal';
import BatchTamperProtectionModal from './BatchTamperProtectionModal';
+1 -1
View File
@@ -7,7 +7,7 @@ import {clearAlertRecords, getAlertRecords} from '@/api/alert.ts';
import type {AlertRecord} from '@/types';
import dayjs from 'dayjs';
import {getErrorMessage} from '@/lib/utils';
import {PageHeader} from '@/components';
import {PageHeader} from '@/components/PageHeader';
import {useQuery, useQueryClient} from '@tanstack/react-query';
import {listAgentsByAdmin} from "@/api/agent.ts";
+1 -1
View File
@@ -7,7 +7,7 @@ import {deleteApiKey, disableApiKey, enableApiKey, getApiKeyRaw, listApiKeys} fr
import type {ApiKey} from '@/types';
import dayjs from 'dayjs';
import {getErrorMessage} from '@/lib/utils';
import {PageHeader} from '@/components';
import {PageHeader} from '@/components/PageHeader';
import copy from 'copy-to-clipboard';
import ApiKeyModal from './ApiKeyModal';
import ShowApiKeyModal from './ShowApiKeyModal';
+1 -1
View File
@@ -3,7 +3,7 @@ import {useSearchParams} from 'react-router-dom';
import {App, Button, Divider, Dropdown, Input, Space, Table, Tag, Tooltip} from 'antd';
import type {ColumnsType} from 'antd/es/table';
import type {MenuProps} from 'antd';
import {PageHeader} from '@/components';
import {PageHeader} from '@/components/PageHeader';
import {Globe, MoreVertical, Plus, Settings} from 'lucide-react';
import dayjs from 'dayjs';
import type {DDNSConfig} from '@/types';
+3 -1
View File
@@ -4,11 +4,13 @@ import { App, Button, ConfigProvider, Form, Input, theme } from 'antd';
import { GithubOutlined, GlobalOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
import { Moon, Sun } from 'lucide-react';
import { getAuthConfig, getGitHubAuthURL, getOIDCAuthURL, login } from '@/api/auth.ts';
import {useRuntimeConfig} from '@/api/runtime';
import type { LoginRequest } from '@/types';
import { useColorMode } from '@/shared/contexts/ColorModeContext';
import { flushSync } from 'react-dom';
const Login = () => {
const {data: runtime} = useRuntimeConfig();
const [loading, setLoading] = useState(false);
const [oidcEnabled, setOidcEnabled] = useState(false);
const [githubEnabled, setGithubEnabled] = useState(false);
@@ -147,7 +149,7 @@ const Login = () => {
{/* 头部:品牌名称 */}
<div className="mb-10 text-center">
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-zinc-100">
{window.SystemConfig.SystemNameZh}
{runtime?.system.nameZh || '皮卡监控'}
</h1>
</div>
@@ -7,7 +7,7 @@ import {deleteManageApiKey, disableManageApiKey, enableManageApiKey, listManageA
import type {ApiKey} from '@/types';
import dayjs from 'dayjs';
import {getErrorMessage} from '@/lib/utils';
import {PageHeader} from '@/components';
import {PageHeader} from '@/components/PageHeader';
import ManageApiKeyModal from './ManageApiKeyModal';
import ShowManageApiKeyModal from './ShowManageApiKeyModal';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
+1 -1
View File
@@ -8,7 +8,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {deleteMonitor, listMonitors} from '@/api/monitor.ts';
import type {MonitorTask} from '@/types';
import {getErrorMessage} from '@/lib/utils';
import {PageHeader} from '@/components';
import {PageHeader} from '@/components/PageHeader';
import MonitorModal from './MonitorModal';
const MonitorList = () => {
+1 -10
View File
@@ -25,16 +25,7 @@ const SystemConfigComponent = () => {
mutationFn: saveSystemConfig,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['systemConfig'] });
try {
const response = await fetch('/api/config');
if (response.ok) {
const runtime = await response.json();
window.PikaRuntime = runtime;
window.SystemConfig = runtime.legacySystemConfig;
}
} catch {
// 管理界面不依赖这次刷新成功;公开页面刷新后仍会读取最新配置。
}
await queryClient.invalidateQueries({ queryKey: ['runtime-config'] });
messageApi.success('保存成功,公开页面刷新后生效');
},
onError: (error: unknown) => {
+1 -1
View File
@@ -5,7 +5,7 @@ import NotificationChannels from './NotificationChannels';
import SystemConfig from './SystemConfig';
import PublicIPConfig from './PublicIPConfig';
import ThemeSettings from './ThemeSettings';
import {PageHeader} from "@/components";
import {PageHeader} from "@/components/PageHeader";
import {useSearchParams} from "react-router-dom";
// 默认 IPv4 API 列表
+1 -1
View File
@@ -5,7 +5,7 @@ import PrivateRoute from '@/components/PrivateRoute';
const LoginPage = lazy(() => import('@/pages/Login'));
const GitHubCallbackPage = lazy(() => import('@/pages/Login/GitHubCallback'));
const OIDCCallbackPage = lazy(() => import('@/pages/Login/OIDCCallback'));
const AdminLayout = lazy(() => import('@/pages/AdminLayout'));
const AdminLayout = lazy(() => import('@/layouts/AdminLayout'));
const AgentListPage = lazy(() => import('@/pages/Agents/AgentList'));
const AgentDetailPage = lazy(() => import('@/pages/Agents/AgentDetail'));
const AgentInstallOneClickPage = lazy(() => import('@/pages/Agents/AgentInstallOneClick'));
+12 -1
View File
@@ -1,4 +1,5 @@
import {createContext, useContext, useEffect, useState, type ReactNode} from 'react';
import {useRuntimeConfig} from '@/api/runtime';
export type ColorMode = 'light' | 'dark' | 'system';
export type ResolvedColorMode = 'light' | 'dark';
@@ -12,12 +13,22 @@ interface ColorModeContextValue {
const ColorModeContext = createContext<ColorModeContextValue | undefined>(undefined);
export const ColorModeProvider = ({children}: {children: ReactNode}) => {
const {data: runtime} = useRuntimeConfig();
const [colorMode, setColorModeState] = useState<ColorMode>(() => {
const saved = localStorage.getItem('colorMode') || localStorage.getItem('theme');
if (saved === 'auto') return 'system';
if (saved === 'light' || saved === 'dark' || saved === 'system') return saved;
return window.PikaRuntime?.system?.defaultColorMode || 'system';
return 'system';
});
// 运行时配置加载后,如果用户没有本地偏好,使用服务端默认值
useEffect(() => {
if (!runtime) return;
const saved = localStorage.getItem('colorMode') || localStorage.getItem('theme');
if (!saved) {
setColorModeState(runtime.system.defaultColorMode);
}
}, [runtime]);
const [resolvedColorMode, setResolvedColorMode] = useState<ResolvedColorMode>('dark');
const setColorMode = (mode: ColorMode) => {