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: