import React, { useEffect, useMemo, useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { AppstoreOutlined, CloudOutlined, DashboardOutlined, DesktopOutlined, DownOutlined, ExperimentOutlined, FundProjectionScreenOutlined, LineChartOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MessageOutlined, MoonOutlined, RightOutlined, RobotOutlined, SettingOutlined, ShoppingOutlined, TableOutlined, TeamOutlined, UserOutlined, SunOutlined, } from '@ant-design/icons'; import { Dropdown, Switch } from 'antd'; import { verifyToken } from '../api/auth'; import { getSetupStatus, getSystemConfig } from '../api/admin'; import { getInstallStatus } from '../api/install'; import ErrorBoundary from '../components/ErrorBoundary'; import { AppShellSkeleton } from '../components/AppLoadingSkeleton'; import { DEFAULT_SIDEBAR_CONFIG, parseSidebarConfig, sortSidebarEntries } from '../config/sidebarNavigation'; import { canRestoreDesktopSessionImmediately, isDesktopEmbedSession, markDesktopEmbedSession, requestDesktopSessionRenewal, } from '../utils/desktopEmbedSession'; const AUTH_USER_STORAGE_KEY = 'auth_user'; const SIDEBAR_DARK_MODE_STORAGE_KEY = 'maixiang_sidebar_dark_mode'; const BRAND_HOME_URL = 'https://yyd.jiansuntuzhi.cn/analysis/summary'; const PLANNING_HOME_URL = '/planning/dev-style-preview'; const FALLBACK_SITE_NAME = '拼多多自动化运营平台'; const FALLBACK_LOGO = 'https://s3.jiansun.vip/gongxiangpan/logos/1775097467497-1d6a81b8a76a.png'; const readSidebarDarkModePreference = () => { try { const value = localStorage.getItem(SIDEBAR_DARK_MODE_STORAGE_KEY); if (value === 'dark') return true; if (value === 'light') return false; } catch { // Local persistence is optional in restricted browser contexts. } return null; }; const SIDEBAR_ICON_MAP = { appstore: AppstoreOutlined, cloud: CloudOutlined, dashboard: DashboardOutlined, desktop: DesktopOutlined, experiment: ExperimentOutlined, 'line-chart': LineChartOutlined, message: MessageOutlined, projection: FundProjectionScreenOutlined, robot: RobotOutlined, setting: SettingOutlined, shopping: ShoppingOutlined, table: TableOutlined, team: TeamOutlined, }; const SIDEBAR_ROUTE_MAP = { 'dashboard-preview': { path: '/planning/dev-style-preview' }, 'marketing-events': { path: '/planning/operation/marketing-events', activePath: '/planning/operation/marketing-events', matchMode: 'prefix', }, 'promotion-module': { path: '/planning/operation/promotion-module' }, 'product-monitoring': { path: '/planning/operation/product-monitoring' }, 'ops-b-incentive': { path: '/planning/operation/ops-b-incentive' }, 'product-analysis': { path: '/planning/operation/product-analysis' }, 'work-orders': { path: '/planning/operation/work-orders' }, 'modular-stock-monitor-accessory': { path: '/planning/operation/modular-stock-monitor', activePath: '/planning/operation/modular-stock-monitor', matchMode: 'prefix', }, 'mkh-bom-data': { path: '/planning/operation/mkh-bom-data', activePath: '/planning/operation/mkh-bom-data', matchMode: 'prefix', }, 'daily-inventory': { path: '/planning/operation/daily-inventory' }, settings: { path: '/settings' }, 'online-desktop-users': { path: '/settings/online-desktop-users' }, 'desktop-execution-tasks': { path: '/settings/desktop-execution-tasks' }, }; const buildSidebarSections = (config, canUseSettings) => sortSidebarEntries(config.entries).flatMap((entry) => { if (entry.requiresAdmin && !canUseSettings) return []; if (entry.type === 'section') return [{ ...entry }]; const route = SIDEBAR_ROUTE_MAP[entry.routeKey] || {}; const routeMatches = (itemRoute = {}, pathname = '') => { const excludedPaths = Array.isArray(itemRoute.excludePaths) ? itemRoute.excludePaths : []; if (excludedPaths.some((path) => pathname === path || pathname.startsWith(`${path}/`))) { return false; } const exactPaths = Array.isArray(itemRoute.matchPaths) ? itemRoute.matchPaths : []; if (exactPaths.some((path) => pathname === path || pathname.startsWith(`${path}/`))) { return true; } const matchPath = itemRoute.activePath || itemRoute.path; if (!matchPath) return false; return itemRoute.matchMode === 'prefix' ? pathname === matchPath || pathname.startsWith(`${matchPath}/`) : pathname === matchPath; }; const buildItem = (item) => { if (item.requiresAdmin && !canUseSettings) return null; const itemRoute = SIDEBAR_ROUTE_MAP[item.routeKey] || {}; return { ...item, icon: SIDEBAR_ICON_MAP[item.icon] || AppstoreOutlined, path: itemRoute.path, external: Boolean(itemRoute.externalUrl), onClick: itemRoute.externalUrl ? () => { window.location.href = itemRoute.externalUrl; } : undefined, match: (path) => routeMatches(itemRoute, path), }; }; const normalized = { ...entry, icon: SIDEBAR_ICON_MAP[entry.icon] || AppstoreOutlined, path: route.path, external: Boolean(route.externalUrl), onClick: route.externalUrl ? () => { window.location.href = route.externalUrl; } : undefined, match: (path) => routeMatches(route, path), }; if (entry.type === 'group') { normalized.children = sortSidebarEntries(entry.children).map(buildItem).filter(Boolean); } return [normalized]; }); function parseJwtPayload(token) { if (!token) { return null; } try { const [, payload] = token.split('.'); if (!payload) { return null; } const normalized = payload.replace(/-/g, '+').replace(/_/g, '/'); const padded = normalized.padEnd(normalized.length + ((4 - normalized.length % 4) % 4), '='); return JSON.parse(window.atob(padded)); } catch { return null; } } function isExpiredJwtPayload(payload) { const exp = Number(payload?.exp || 0); return exp > 0 && Math.floor(Date.now() / 1000) >= exp; } function readStoredAuthUser(tokenSub) { try { const raw = localStorage.getItem(AUTH_USER_STORAGE_KEY); if (!raw) { return null; } const storedUser = JSON.parse(raw); const storedSub = storedUser?.sub ?? storedUser?.id; return storedSub && String(storedSub) === String(tokenSub) ? storedUser : null; } catch { return null; } } function clearAuthSession() { localStorage.removeItem('token'); localStorage.removeItem(AUTH_USER_STORAGE_KEY); } function looksLikeMojibake(text) { return /[ÃÂ][\u0080-\u00ff]|[äåæçèé][\u0080-\u00ff]|[\u00c0-\u00ff][\u0080-\u00bf]/.test(text); } function repairMojibake(text) { if (!text || !looksLikeMojibake(text) || typeof TextDecoder === 'undefined') { return text; } try { const bytes = Uint8Array.from([...text].map((char) => char.charCodeAt(0) & 0xff)); const repaired = new TextDecoder('utf-8', { fatal: true }).decode(bytes).trim(); return repaired || text; } catch { return text; } } function normalizeDisplayName(...candidates) { const rawName = candidates.find((item) => typeof item === 'string' && item.trim()); return repairMojibake(rawName?.trim() || '个人中心'); } function buildFallbackUserFromToken(token) { const payload = parseJwtPayload(token); if (!payload?.sub || isExpiredJwtPayload(payload)) { return null; } const storedUser = readStoredAuthUser(payload.sub); return { sub: payload.sub, username: payload.username || '', name: payload.name || payload.username || '当前用户', email: payload.email || '', role: payload.role || storedUser?.role || 'user', picture: payload.picture || storedUser?.picture || '', authSource: payload.authSource || storedUser?.authSource || 'local', departmentId: payload.departmentId ?? storedUser?.departmentId ?? null, departmentName: payload.departmentName || storedUser?.departmentName || '', allowedPageKeys: Array.isArray(payload.allowedPageKeys) ? payload.allowedPageKeys : Array.isArray(storedUser?.allowedPageKeys) ? storedUser.allowedPageKeys : [], }; } function canFallbackToJwtPayload(error, fallbackUser) { if (!fallbackUser) { return false; } if (fallbackUser.authSource === 'local-preview') { return true; } const status = Number(error?.response?.status || 0); return !status || status >= 500; } const BasicLayout = () => { const location = useLocation(); const navigate = useNavigate(); const [user, setUser] = useState(null); const [authReady, setAuthReady] = useState(false); const [siteConfig, setSiteConfig] = useState({ siteName: FALLBACK_SITE_NAME, logo: FALLBACK_LOGO }); const [sidebarConfig, setSidebarConfig] = useState(DEFAULT_SIDEBAR_CONFIG); const [sidebarCollapsed, setSidebarCollapsed] = useState( () => DEFAULT_SIDEBAR_CONFIG.collapsedByDefault ); const [sidebarDarkMode, setSidebarDarkMode] = useState( () => readSidebarDarkModePreference() ?? DEFAULT_SIDEBAR_CONFIG.darkModeByDefault ); const [expandedSidebarKeys, setExpandedSidebarKeys] = useState( DEFAULT_SIDEBAR_CONFIG.entries.filter((entry) => entry.defaultExpanded).map((entry) => entry.key) ); const pathname = location.pathname; const isImmersivePlanningRoute = false; const canUseSettings = user?.role === 'admin'; const userDisplayName = normalizeDisplayName(user?.name, user?.username, user?.email); const userAvatar = user?.picture || siteConfig.logo || FALLBACK_LOGO; const sidebarSections = useMemo( () => buildSidebarSections(sidebarConfig, canUseSettings), [canUseSettings, sidebarConfig] ); useEffect(() => { const loadSiteConfig = async () => { try { const config = await getSystemConfig(); const nextSiteName = config?.site_name || FALLBACK_SITE_NAME; const nextLogo = config?.logo || FALLBACK_LOGO; const nextSidebarConfig = parseSidebarConfig(config?.sidebar_navigation); setSiteConfig({ siteName: nextSiteName, logo: nextLogo }); setSidebarConfig(nextSidebarConfig); setSidebarCollapsed(Boolean(nextSidebarConfig.collapsedByDefault)); setSidebarDarkMode(readSidebarDarkModePreference() ?? Boolean(nextSidebarConfig.darkModeByDefault)); setExpandedSidebarKeys( nextSidebarConfig.entries.filter((entry) => entry.defaultExpanded).map((entry) => entry.key) ); document.title = nextSiteName; let link = document.querySelector("link[rel*='icon']"); if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); } link.href = nextLogo; } catch { document.title = FALLBACK_SITE_NAME; } }; loadSiteConfig(); }, []); useEffect(() => { const handleSiteConfigUpdated = (event) => { const nextSiteName = event.detail?.siteName || FALLBACK_SITE_NAME; const nextLogo = event.detail?.logo; setSiteConfig((current) => ({ siteName: nextSiteName, logo: nextLogo || current.logo, })); document.title = nextSiteName; if (nextLogo) { let link = document.querySelector("link[rel*='icon']"); if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); } link.href = nextLogo; } }; window.addEventListener('site-config-updated', handleSiteConfigUpdated); return () => window.removeEventListener('site-config-updated', handleSiteConfigUpdated); }, []); useEffect(() => { const checkAuth = async () => { markDesktopEmbedSession(); const token = localStorage.getItem('token'); const fallbackUser = buildFallbackUserFromToken(token); if (canRestoreDesktopSessionImmediately(isDesktopEmbedSession(), fallbackUser)) { setUser(fallbackUser); setAuthReady(true); try { const verification = await verifyToken(); if (!verification?.valid || !verification.user) { clearAuthSession(); requestDesktopSessionRenewal('web-session-invalid'); return; } const resolvedUser = verification.user; localStorage.setItem(AUTH_USER_STORAGE_KEY, JSON.stringify(resolvedUser)); setUser(resolvedUser); if (resolvedUser.role === 'admin') { const setup = await getSetupStatus().catch((error) => { console.warn('Setup status unavailable, continue with current session:', error); return null; }); if (setup?.shouldShowWizard) { navigate('/setup', { replace: true }); } } } catch (error) { if (!canFallbackToJwtPayload(error, fallbackUser)) { clearAuthSession(); requestDesktopSessionRenewal('web-session-invalid'); } } return; } try { const installStatusPromise = getInstallStatus().catch(() => ({ required: false })); const verificationPromise = verifyToken() .then((res) => ({ res, error: null })) .catch((error) => ({ res: null, error })); const setupStatusPromise = fallbackUser?.role === 'admin' ? getSetupStatus().catch((error) => { console.warn('Setup status unavailable, continue with current session:', error); return null; }) : null; const [installStatus, verification] = await Promise.all([ installStatusPromise, verificationPromise, ]); if (installStatus?.required && !fallbackUser) { clearAuthSession(); navigate('/install', { replace: true }); return; } let resolvedUser = null; const verifyError = verification.error; if (verification.res?.valid) { resolvedUser = verification.res.user; } else if (verifyError) { console.warn('Token verification failed:', verifyError); } if (!resolvedUser && canFallbackToJwtPayload(verifyError, fallbackUser)) { resolvedUser = fallbackUser; } if (resolvedUser) { localStorage.setItem(AUTH_USER_STORAGE_KEY, JSON.stringify(resolvedUser)); if (resolvedUser.role === 'admin') { try { const setup = setupStatusPromise ? await setupStatusPromise : await getSetupStatus(); if (setup?.shouldShowWizard) { navigate('/setup', { replace: true }); return; } } catch (error) { console.warn('Setup status unavailable, continue with current session:', error); } } setUser(resolvedUser); } else if (installStatus?.required) { clearAuthSession(); navigate('/install', { replace: true }); } else { clearAuthSession(); if (!requestDesktopSessionRenewal('web-session-missing')) { navigate('/login'); } } } catch (error) { console.error(error); if (fallbackUser) { setUser(fallbackUser); } else { clearAuthSession(); if (!requestDesktopSessionRenewal('web-session-invalid')) { navigate('/login'); } } } finally { setAuthReady(true); } }; checkAuth(); }, [navigate]); useEffect(() => { if (!authReady || !user) { return; } if (pathname === '/' || pathname === '/dashboard') { navigate(PLANNING_HOME_URL, { replace: true }); return; } if (pathname.startsWith('/settings') && !canUseSettings) { navigate(PLANNING_HOME_URL, { replace: true }); return; } const isAllowedPath = ( pathname === '/planning' || pathname.startsWith('/planning/') || pathname === '/settings' || pathname.startsWith('/settings/') ); if (!isAllowedPath) { navigate(PLANNING_HOME_URL, { replace: true }); } }, [authReady, canUseSettings, navigate, pathname, user]); const handleLogout = () => { clearAuthSession(); navigate('/login'); }; const userMenu = [ canUseSettings ? { key: 'settings', icon: , label: '系统设置', onClick: () => navigate('/settings'), } : null, { key: 'logout', icon: , label: '退出登录', onClick: handleLogout, }, ].filter(Boolean); const isSidebarEntryActive = (entry) => { if (!entry) return false; if (entry.match?.(pathname)) return true; return Array.isArray(entry.children) && entry.children.some((child) => isSidebarEntryActive(child)); }; const toggleSidebarGroup = (key) => { setSidebarCollapsed(false); setExpandedSidebarKeys((keys) => ( keys.includes(key) ? keys.filter((item) => item !== key) : [...keys, key] )); }; const handleSidebarLeafClick = (item) => { if (item.external) { item.onClick?.(); return; } if (item.path) { navigate(item.path); } }; const handleSidebarDarkModeChange = (checked) => { setSidebarDarkMode(checked); try { localStorage.setItem(SIDEBAR_DARK_MODE_STORAGE_KEY, checked ? 'dark' : 'light'); } catch { // Keep the current-session preference when persistence is unavailable. } }; if (!authReady) { return ; } return (
); }; export default BasicLayout;