/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import HeaderBar from './headerbar'; import { Layout, Button, Space } from '@douyinfe/semi-ui'; import { IconClose } from '@douyinfe/semi-icons'; import SiderBar from './SiderBar'; import App from '../../App'; import FooterBar from './Footer'; import OnboardingWizard from '../onboarding/OnboardingWizard'; import { ToastContainer } from 'react-toastify'; import React, { useContext, useEffect, useState } from 'react'; import { useIsMobile } from '../../hooks/common/useIsMobile'; import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed'; import { useTranslation } from 'react-i18next'; import { API, getLogo, showError, setStatusData } from '../../helpers'; import { UserContext } from '../../context/User'; import { StatusContext } from '../../context/Status'; import { useLocation } from 'react-router-dom'; import { normalizeLanguage, pickPrimaryNavigatorLanguage, isSupportedUiLanguage, I18N_ANON_LANG_INITIALIZED_KEY, I18N_BROWSER_LANG_MISMATCH_PROMPT_KEY, I18N_BROWSER_LANG_BANNER_PENDING_KEY, } from '../../i18n/language'; const { Sider, Content, Header } = Layout; const PageLayout = () => { const [userState, userDispatch] = useContext(UserContext); const [statusState, statusDispatch] = useContext(StatusContext); const isMobile = useIsMobile(); const [collapsed, , setCollapsed] = useSidebarCollapsed(); const [drawerOpen, setDrawerOpen] = useState(false); const [browserLangBanner, setBrowserLangBanner] = useState(null); const { i18n, t } = useTranslation(); const location = useLocation(); const cardProPages = [ '/', '/console/channel', '/console/log', '/console/redemption', '/console/user', '/console/token', '/console/midjourney', '/console/task', '/console/models', '/console/supplier/apply', '/console/distributor/apply', '/console/supplier/channel', '/console/supplier/models', '/console/supplier-application', '/console/suppliers', '/console/playground', '/pricing', '/console/model-heat' ]; const shouldHideFooter = cardProPages.includes(location.pathname); const pricingNaturalScroll = location.pathname === '/pricing'; const shouldInnerPadding = location.pathname.includes('/console') && !location.pathname.startsWith('/console/chat') && location.pathname !== '/console/playground'; const isConsoleRoute = location.pathname.startsWith('/console'); const showSider = isConsoleRoute && (!isMobile || drawerOpen); useEffect(() => { if (isMobile && drawerOpen && collapsed) { setCollapsed(false); } }, [isMobile, drawerOpen, collapsed, setCollapsed]); const loadUser = () => { let user = localStorage.getItem('user'); if (user) { let data = JSON.parse(user); userDispatch({ type: 'login', payload: data }); } }; /** 与 localStorage 中的 status 派生字段一致(须在 setStatusData 之后调用) */ const syncDocumentBrandingFromStorage = () => { // 仅用显式缓存:避免用 getSystemName() 的占位文案抢在 /api/status 之前改标题, // 也避免旧缓存里的默认名(如 TokenFactory)闪一下。 const systemName = localStorage.getItem('system_name'); if (systemName) { document.title = systemName; } const logo = getLogo(); if (logo) { const linkElement = document.querySelector("link[rel~='icon']"); if (linkElement) { linkElement.href = logo; } } }; const loadStatus = async () => { try { const res = await API.get('/api/status'); const { success, data } = res.data; if (success) { statusDispatch({ type: 'set', payload: data }); setStatusData(data); syncDocumentBrandingFromStorage(); } else { showError('Unable to connect to server'); syncDocumentBrandingFromStorage(); } } catch (error) { showError('Failed to load status'); syncDocumentBrandingFromStorage(); } }; useEffect(() => { loadUser(); loadStatus().catch(console.error); }, []); useEffect(() => { const status = statusState?.status; if (!status) return; let userLang; if (userState?.user?.setting) { try { const settings = JSON.parse(userState.user.setting); userLang = normalizeLanguage(settings.language); } catch (e) { /* ignore */ } } if (userLang && isSupportedUiLanguage(userLang)) { localStorage.setItem('i18nextLng', userLang); if (userLang !== normalizeLanguage(i18n.language)) { i18n.changeLanguage(userLang); } setBrowserLangBanner(null); return; } if (localStorage.getItem(I18N_ANON_LANG_INITIALIZED_KEY) === '1') { const saved = normalizeLanguage(localStorage.getItem('i18nextLng') || ''); if ( saved && isSupportedUiLanguage(saved) && saved !== normalizeLanguage(i18n.language) ) { i18n.changeLanguage(saved); } if (localStorage.getItem(I18N_BROWSER_LANG_MISMATCH_PROMPT_KEY) !== 'done') { try { const raw = sessionStorage.getItem(I18N_BROWSER_LANG_BANNER_PENDING_KEY); if (raw) { const parsed = JSON.parse(raw); if (parsed?.browserLang && parsed?.siteLang) { setBrowserLangBanner(parsed); } } } catch (e) { /* ignore */ } } return; } const siteDefaultRaw = normalizeLanguage( status.default_site_language || 'zh-CN', ); const siteDefault = isSupportedUiLanguage(siteDefaultRaw) ? siteDefaultRaw : 'zh-CN'; localStorage.setItem(I18N_ANON_LANG_INITIALIZED_KEY, '1'); localStorage.setItem('i18nextLng', siteDefault); if (normalizeLanguage(i18n.language) !== siteDefault) { i18n.changeLanguage(siteDefault); } if (localStorage.getItem(I18N_BROWSER_LANG_MISMATCH_PROMPT_KEY) === 'done') { return; } const browserLang = pickPrimaryNavigatorLanguage(); if (browserLang && browserLang !== siteDefault) { const payload = { browserLang, siteLang: siteDefault }; try { sessionStorage.setItem( I18N_BROWSER_LANG_BANNER_PENDING_KEY, JSON.stringify(payload), ); } catch (e) { /* ignore */ } setBrowserLangBanner(payload); } }, [statusState?.status, userState?.user?.setting, userState?.user?.id, i18n]); const resolveLangLabel = (code) => { try { const loc = normalizeLanguage(i18n.language) || 'zh-CN'; const dn = new Intl.DisplayNames([loc], { type: 'language' }); return dn.of(code) || code; } catch (e) { return code; } }; const dismissBrowserLangBanner = (mode) => { if (mode === 'browser' && browserLangBanner) { const { browserLang } = browserLangBanner; i18n.changeLanguage(browserLang); localStorage.setItem('i18nextLng', browserLang); } localStorage.setItem(I18N_BROWSER_LANG_MISMATCH_PROMPT_KEY, 'done'); try { sessionStorage.removeItem(I18N_BROWSER_LANG_BANNER_PENDING_KEY); } catch (e) { /* ignore */ } setBrowserLangBanner(null); }; return ( {browserLangBanner ? (
) : null} {location.pathname !== '/' && (
setDrawerOpen((prev) => !prev)} drawerOpen={drawerOpen} />
)} {showSider && ( { if (isMobile) setDrawerOpen(false); }} /> )} {!shouldHideFooter && ( )}
); }; export default PageLayout;