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:
1 parent
cda2585f1a
commit
6ab1876899
21 files changed
+88
-82
No files matched your search
@@ -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};
|
||||
@@ -1,2 +0,0 @@
|
||||
export { PageHeader } from './PageHeader';
|
||||
export type { Action } from './PageHeader';
|
||||
Vendored
-30
@@ -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>
|
||||
File renamed without changes.
@@ -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.
File renamed without changes.
+11
-28
@@ -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>,
|
||||
);
|
||||
@@ -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';
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 列表
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user