PageLayout.jsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import HeaderBar from './headerbar';
  16. import { Layout } from '@douyinfe/semi-ui';
  17. import SiderBar from './SiderBar';
  18. import App from '../../App';
  19. import FooterBar from './Footer';
  20. import ClassicFrontendDeprecationBanner from './ClassicFrontendDeprecationBanner';
  21. import { ToastContainer } from 'react-toastify';
  22. import ErrorBoundary from '../common/ErrorBoundary';
  23. import React, { useContext, useEffect, useState } from 'react';
  24. import { useIsMobile } from '../../hooks/common/useIsMobile';
  25. import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
  26. import { useTranslation } from 'react-i18next';
  27. import {
  28. API,
  29. getLogo,
  30. getSystemName,
  31. showError,
  32. setStatusData,
  33. } from '../../helpers';
  34. import { UserContext } from '../../context/User';
  35. import { StatusContext } from '../../context/Status';
  36. import { useLocation } from 'react-router-dom';
  37. import { normalizeLanguage } from '../../i18n/language';
  38. const { Sider, Content, Header } = Layout;
  39. const PageLayout = () => {
  40. const [userState, userDispatch] = useContext(UserContext);
  41. const [, statusDispatch] = useContext(StatusContext);
  42. const isMobile = useIsMobile();
  43. const [collapsed, , setCollapsed] = useSidebarCollapsed();
  44. const [drawerOpen, setDrawerOpen] = useState(false);
  45. const { i18n } = useTranslation();
  46. const location = useLocation();
  47. const cardProPages = [
  48. '/console/channel',
  49. '/console/log',
  50. '/console/redemption',
  51. '/console/user',
  52. '/console/token',
  53. '/console/midjourney',
  54. '/console/task',
  55. '/console/models',
  56. '/pricing',
  57. ];
  58. const shouldHideFooter = cardProPages.includes(location.pathname);
  59. const shouldInnerPadding =
  60. location.pathname.includes('/console') &&
  61. !location.pathname.startsWith('/console/chat') &&
  62. location.pathname !== '/console/playground';
  63. const isConsoleRoute = location.pathname.startsWith('/console');
  64. const showSider = isConsoleRoute && (!isMobile || drawerOpen);
  65. const isFixedLayout = isConsoleRoute || location.pathname === '/pricing';
  66. useEffect(() => {
  67. if (isMobile && drawerOpen && collapsed) {
  68. setCollapsed(false);
  69. }
  70. }, [isMobile, drawerOpen, collapsed, setCollapsed]);
  71. const loadUser = () => {
  72. let user = localStorage.getItem('user');
  73. if (user) {
  74. let data = JSON.parse(user);
  75. userDispatch({ type: 'login', payload: data });
  76. }
  77. };
  78. const loadStatus = async () => {
  79. try {
  80. const res = await API.get('/api/status');
  81. const { success, data } = res.data;
  82. if (success) {
  83. statusDispatch({ type: 'set', payload: data });
  84. setStatusData(data);
  85. } else {
  86. showError('Unable to connect to server');
  87. }
  88. } catch (error) {
  89. showError('Failed to load status');
  90. }
  91. };
  92. useEffect(() => {
  93. loadUser();
  94. loadStatus().catch(console.error);
  95. let systemName = getSystemName();
  96. if (systemName) {
  97. document.title = systemName;
  98. }
  99. let logo = getLogo();
  100. if (logo) {
  101. let linkElement = document.querySelector("link[rel~='icon']");
  102. if (linkElement) {
  103. linkElement.href = logo;
  104. }
  105. }
  106. }, []);
  107. useEffect(() => {
  108. let preferredLang;
  109. if (userState?.user?.setting) {
  110. try {
  111. const settings = JSON.parse(userState.user.setting);
  112. preferredLang = normalizeLanguage(settings.language);
  113. } catch (e) {
  114. // Ignore parse errors
  115. }
  116. }
  117. if (!preferredLang) {
  118. const savedLang = localStorage.getItem('i18nextLng');
  119. if (savedLang) {
  120. preferredLang = normalizeLanguage(savedLang);
  121. }
  122. }
  123. if (preferredLang) {
  124. localStorage.setItem('i18nextLng', preferredLang);
  125. if (preferredLang !== i18n.language) {
  126. i18n.changeLanguage(preferredLang);
  127. }
  128. }
  129. }, [i18n, userState?.user?.setting]);
  130. return (
  131. <Layout
  132. className={`app-layout${isFixedLayout ? ' app-layout-fixed' : ''}`}
  133. style={{
  134. display: 'flex',
  135. flexDirection: 'column',
  136. overflow: isFixedLayout && !isMobile ? 'hidden' : 'visible',
  137. }}
  138. >
  139. <Header
  140. style={{
  141. padding: 0,
  142. height: 'auto',
  143. lineHeight: 'normal',
  144. position: 'fixed',
  145. width: '100%',
  146. top: 0,
  147. zIndex: 100,
  148. }}
  149. >
  150. <HeaderBar
  151. onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)}
  152. drawerOpen={drawerOpen}
  153. />
  154. </Header>
  155. <Layout
  156. style={{
  157. overflow: isFixedLayout && !isMobile ? 'auto' : 'visible',
  158. display: 'flex',
  159. flexDirection: 'column',
  160. flex: '1 1 auto',
  161. }}
  162. >
  163. {showSider && (
  164. <Sider
  165. className='app-sider'
  166. style={{
  167. position: 'fixed',
  168. left: 0,
  169. top: '64px',
  170. zIndex: 99,
  171. border: 'none',
  172. paddingRight: '0',
  173. width: 'var(--sidebar-current-width)',
  174. }}
  175. >
  176. <SiderBar
  177. onNavigate={() => {
  178. if (isMobile) setDrawerOpen(false);
  179. }}
  180. />
  181. </Sider>
  182. )}
  183. <Layout
  184. style={{
  185. marginLeft: isMobile
  186. ? '0'
  187. : showSider
  188. ? 'var(--sidebar-current-width)'
  189. : '0',
  190. flex: '1 1 auto',
  191. display: 'flex',
  192. flexDirection: 'column',
  193. minHeight: 0,
  194. }}
  195. >
  196. <ClassicFrontendDeprecationBanner />
  197. <Content
  198. className={isFixedLayout ? undefined : 'public-page-content'}
  199. style={{
  200. flex: isFixedLayout ? '1 0 auto' : '1 1 auto',
  201. overflowY: isFixedLayout && !isMobile ? 'hidden' : 'visible',
  202. WebkitOverflowScrolling: 'touch',
  203. padding: shouldInnerPadding ? (isMobile ? '5px' : '24px') : '0',
  204. position: 'relative',
  205. minHeight: 0,
  206. }}
  207. >
  208. <ErrorBoundary>
  209. <App />
  210. </ErrorBoundary>
  211. </Content>
  212. {!shouldHideFooter && (
  213. <Layout.Footer
  214. style={{
  215. flex: '0 0 auto',
  216. width: '100%',
  217. }}
  218. >
  219. <FooterBar />
  220. </Layout.Footer>
  221. )}
  222. </Layout>
  223. </Layout>
  224. <ToastContainer />
  225. </Layout>
  226. );
  227. };
  228. export default PageLayout;