PageLayout.jsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 { ToastContainer } from 'react-toastify';
  21. import ErrorBoundary from '../common/ErrorBoundary';
  22. import React, { useContext, useEffect, useState } from 'react';
  23. import { useIsMobile } from '../../hooks/common/useIsMobile';
  24. import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
  25. import { useTranslation } from 'react-i18next';
  26. import {
  27. API,
  28. getLogo,
  29. getSystemName,
  30. showError,
  31. setStatusData,
  32. } from '../../helpers';
  33. import { UserContext } from '../../context/User';
  34. import { StatusContext } from '../../context/Status';
  35. import { useLocation } from 'react-router-dom';
  36. import { normalizeLanguage } from '../../i18n/language';
  37. const { Sider, Content, Header } = Layout;
  38. const PageLayout = () => {
  39. const [userState, userDispatch] = useContext(UserContext);
  40. const [, statusDispatch] = useContext(StatusContext);
  41. const isMobile = useIsMobile();
  42. const [collapsed, , setCollapsed] = useSidebarCollapsed();
  43. const [drawerOpen, setDrawerOpen] = useState(false);
  44. const { i18n } = useTranslation();
  45. const location = useLocation();
  46. const cardProPages = [
  47. '/console/channel',
  48. '/console/log',
  49. '/console/redemption',
  50. '/console/user',
  51. '/console/token',
  52. '/console/midjourney',
  53. '/console/task',
  54. '/console/models',
  55. '/pricing',
  56. ];
  57. const shouldHideFooter = cardProPages.includes(location.pathname);
  58. const shouldInnerPadding =
  59. location.pathname.includes('/console') &&
  60. !location.pathname.startsWith('/console/chat') &&
  61. location.pathname !== '/console/playground';
  62. const isConsoleRoute = location.pathname.startsWith('/console');
  63. const showSider = isConsoleRoute && (!isMobile || drawerOpen);
  64. useEffect(() => {
  65. if (isMobile && drawerOpen && collapsed) {
  66. setCollapsed(false);
  67. }
  68. }, [isMobile, drawerOpen, collapsed, setCollapsed]);
  69. const loadUser = () => {
  70. let user = localStorage.getItem('user');
  71. if (user) {
  72. let data = JSON.parse(user);
  73. userDispatch({ type: 'login', payload: data });
  74. }
  75. };
  76. const loadStatus = async () => {
  77. try {
  78. const res = await API.get('/api/status');
  79. const { success, data } = res.data;
  80. if (success) {
  81. statusDispatch({ type: 'set', payload: data });
  82. setStatusData(data);
  83. } else {
  84. showError('Unable to connect to server');
  85. }
  86. } catch (error) {
  87. showError('Failed to load status');
  88. }
  89. };
  90. useEffect(() => {
  91. loadUser();
  92. loadStatus().catch(console.error);
  93. let systemName = getSystemName();
  94. if (systemName) {
  95. document.title = systemName;
  96. }
  97. let logo = getLogo();
  98. if (logo) {
  99. let linkElement = document.querySelector("link[rel~='icon']");
  100. if (linkElement) {
  101. linkElement.href = logo;
  102. }
  103. }
  104. }, []);
  105. useEffect(() => {
  106. let preferredLang;
  107. if (userState?.user?.setting) {
  108. try {
  109. const settings = JSON.parse(userState.user.setting);
  110. preferredLang = normalizeLanguage(settings.language);
  111. } catch (e) {
  112. // Ignore parse errors
  113. }
  114. }
  115. if (!preferredLang) {
  116. const savedLang = localStorage.getItem('i18nextLng');
  117. if (savedLang) {
  118. preferredLang = normalizeLanguage(savedLang);
  119. }
  120. }
  121. if (preferredLang) {
  122. localStorage.setItem('i18nextLng', preferredLang);
  123. if (preferredLang !== i18n.language) {
  124. i18n.changeLanguage(preferredLang);
  125. }
  126. }
  127. }, [i18n, userState?.user?.setting]);
  128. return (
  129. <Layout
  130. className='app-layout'
  131. style={{
  132. display: 'flex',
  133. flexDirection: 'column',
  134. overflow: isMobile ? 'visible' : 'hidden',
  135. }}
  136. >
  137. <Header
  138. style={{
  139. padding: 0,
  140. height: 'auto',
  141. lineHeight: 'normal',
  142. position: 'fixed',
  143. width: '100%',
  144. top: 0,
  145. zIndex: 100,
  146. }}
  147. >
  148. <HeaderBar
  149. onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)}
  150. drawerOpen={drawerOpen}
  151. />
  152. </Header>
  153. <Layout
  154. style={{
  155. overflow: isMobile ? 'visible' : 'auto',
  156. display: 'flex',
  157. flexDirection: 'column',
  158. }}
  159. >
  160. {showSider && (
  161. <Sider
  162. className='app-sider'
  163. style={{
  164. position: 'fixed',
  165. left: 0,
  166. top: '64px',
  167. zIndex: 99,
  168. border: 'none',
  169. paddingRight: '0',
  170. width: 'var(--sidebar-current-width)',
  171. }}
  172. >
  173. <SiderBar
  174. onNavigate={() => {
  175. if (isMobile) setDrawerOpen(false);
  176. }}
  177. />
  178. </Sider>
  179. )}
  180. <Layout
  181. style={{
  182. marginLeft: isMobile
  183. ? '0'
  184. : showSider
  185. ? 'var(--sidebar-current-width)'
  186. : '0',
  187. flex: '1 1 auto',
  188. display: 'flex',
  189. flexDirection: 'column',
  190. }}
  191. >
  192. <Content
  193. style={{
  194. flex: '1 0 auto',
  195. overflowY: isMobile ? 'visible' : 'hidden',
  196. WebkitOverflowScrolling: 'touch',
  197. padding: shouldInnerPadding ? (isMobile ? '5px' : '24px') : '0',
  198. position: 'relative',
  199. }}
  200. >
  201. <ErrorBoundary>
  202. <App />
  203. </ErrorBoundary>
  204. </Content>
  205. {!shouldHideFooter && (
  206. <Layout.Footer
  207. style={{
  208. flex: '0 0 auto',
  209. width: '100%',
  210. }}
  211. >
  212. <FooterBar />
  213. </Layout.Footer>
  214. )}
  215. </Layout>
  216. </Layout>
  217. <ToastContainer />
  218. </Layout>
  219. );
  220. };
  221. export default PageLayout;