1、后台系统设置settings页面增加一个开启关闭网站的按钮

2、如果网站关闭,前台页面所有请求暂时跳转单独的页如果网站关闭,前台页面所有请求暂时跳转单独的页面,页面内容 系统正在升级相关信息
3、要提供一个开发人员可以查看真实网站内容的入口
This commit is contained in:
2026-07-11 10:58:48 +08:00
parent 111c74c7c6
commit d2a30c58ca
11 changed files with 461 additions and 5 deletions
+39 -1
View File
@@ -1,10 +1,11 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { ConfigProvider, App as AntApp, Spin } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
import MaintenancePage from './pages/MaintenancePage';
import AppLayout from './components/Layout/AppLayout';
import LoginPage from './pages/LoginPage';
import ProjectsPage from './pages/ProjectsPage';
@@ -61,11 +62,47 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const App = () => {
const { checkAuth } = useAuthStore();
const [siteReady, setSiteReady] = useState(false);
useEffect(() => {
// 处理 URL 中的开发者访问令牌
const params = new URLSearchParams(window.location.search);
const devToken = params.get('dev_access');
if (devToken) {
localStorage.setItem('dev_access_token', devToken);
// 清除 URL 中的令牌参数
params.delete('dev_access');
const newSearch = params.toString();
window.history.replaceState({}, '', window.location.pathname + (newSearch ? `?${newSearch}` : ''));
}
// 检查网站状态(仅当用户没有有效 dev token 时)
checkAuth();
if (!localStorage.getItem('dev_access_token')) {
import('./api').then(({ getSiteInfo }) => {
getSiteInfo()
.then((info) => {
if (!info.siteEnabled && !window.location.pathname.startsWith('/maintenance')) {
window.location.href = '/maintenance';
}
})
.catch(() => { /* 请求失败时默认放行 */ })
.finally(() => setSiteReady(true));
});
} else {
setSiteReady(true);
}
}, []);
// 网站状态检查中显示 loading
if (!siteReady && !localStorage.getItem('dev_access_token')) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<Spin size="large" />
</div>
);
}
return (
<ConfigProvider
locale={zhCN}
@@ -93,6 +130,7 @@ const App = () => {
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/maintenance" element={<MaintenancePage />} />
<Route path="/join-team" element={<JoinTeamPage />} />
<Route path="/private-portrait-authorized" element={<PrivatePortraitAuthorizeResult />} />
<Route
+13
View File
@@ -66,6 +66,12 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
if (token) headers['Authorization'] = `Bearer ${token}`;
}
// 开发者访问令牌(网站关闭时用于绕过限制)
const devAccessToken = localStorage.getItem('dev_access_token');
if (devAccessToken) {
headers['X-Dev-Access'] = devAccessToken;
}
let bodyStr: string | undefined;
if (body !== undefined) {
const json = JSON.stringify(body);
@@ -110,6 +116,13 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
// Handle error responses
if (!res.ok) {
// 网站关闭 → 跳转到维护页面
if (res.status === 503 && parsed?.detail?.code === 'SITE_CLOSED') {
if (!window.location.pathname.startsWith('/maintenance')) {
window.location.href = '/maintenance';
}
throw new Error(parsed?.detail?.message || '系统正在升级维护');
}
let msg = parsed?.detail?.message || parsed?.message || `请求失败 (${res.status})`;
if (typeof parsed?.detail === 'string') {
msg = parsed.detail;
+1 -1
View File
@@ -290,7 +290,7 @@ export async function verifyCaptcha(captchaId: string, x: number): Promise<strin
return res.token;
}
// ── Site Info ─────────────────────────────────────────────
export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementPrivacyUrl: string; siteCopyright: string; operationManual: string }> {
export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementPrivacyUrl: string; siteCopyright: string; operationManual: string; siteEnabled: boolean }> {
if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementPrivacyUrl: '', siteCopyright: '© 2024 民众智创 版权所有', operationManual: '' };
return api.get('/auth/site-info', false);
}
+103
View File
@@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react';
import { Button, Input, Space, Typography, message } from 'antd';
import { ToolOutlined, SafetyOutlined } from '@ant-design/icons';
import { getSiteInfo } from '../api';
const MaintenancePage: React.FC = () => {
const [devToken, setDevToken] = useState('');
const [showDevInput, setShowDevInput] = useState(false);
const [siteName, setSiteName] = useState('');
useEffect(() => {
// site-info 在网站关闭时仍可访问(白名单)
getSiteInfo().then((info) => {
if (info.siteName) setSiteName(info.siteName);
}).catch(() => {});
}, []);
const handleDevAccess = () => {
const token = devToken.trim();
if (!token) {
message.warning('请输入开发者访问令牌');
return;
}
localStorage.setItem('dev_access_token', token);
message.success('开发者访问已启用,正在刷新...');
window.location.href = '/';
};
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%)',
padding: 24,
textAlign: 'center',
position: 'relative',
}}
>
{/* 开发者入口 — 右下角 */}
<div style={{ position: 'absolute', right: 20, bottom: 20 }}>
{!showDevInput ? (
<Typography.Link
onClick={() => setShowDevInput(true)}
style={{ fontSize: 12, color: '#94a3b8' }}
>
<SafetyOutlined />
</Typography.Link>
) : (
<Space.Compact>
<Input.Password
placeholder="输入开发者访问令牌"
value={devToken}
onChange={(e) => setDevToken(e.target.value)}
onPressEnter={handleDevAccess}
style={{ width: 200 }}
/>
<Button type="primary" onClick={handleDevAccess}>
</Button>
</Space.Compact>
)}
</div>
{/* 主内容 */}
<div
style={{
width: 80,
height: 80,
borderRadius: 20,
background: 'rgba(99, 102, 241, 0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 32,
}}
>
<ToolOutlined style={{ fontSize: 40, color: '#6366f1' }} />
</div>
{siteName && (
<Typography.Text type="secondary" style={{ fontSize: 13, marginBottom: 8, letterSpacing: 1 }}>
{siteName}
</Typography.Text>
)}
<Typography.Title level={2} style={{ marginBottom: 8, color: '#1e293b' }}>
</Typography.Title>
<Typography.Paragraph type="secondary" style={{ fontSize: 15, maxWidth: 400 }}>
<br />
</Typography.Paragraph>
</div>
);
};
export default MaintenancePage;