From cd936f5e92d466862c8ca1cb43cc415f8cc68766 Mon Sep 17 00:00:00 2001
From: youruo <15018737611@163.com>
Date: Wed, 23 Sep 2026 17:41:00 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=AF=8F=E6=97=A5?=
=?UTF-8?q?=E8=BF=9B=E9=94=80=E5=AD=98=EF=BC=88=E5=A4=A7=E8=B4=A7=E5=BA=93?=
=?UTF-8?q?=E5=AD=98=E7=9B=91=E6=8E=A7=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/src/layouts/BasicLayout.jsx | 2651 +++++++++++++-------------
1 file changed, 1326 insertions(+), 1325 deletions(-)
diff --git a/frontend/src/layouts/BasicLayout.jsx b/frontend/src/layouts/BasicLayout.jsx
index 6cb7ddc..eabe42e 100644
--- a/frontend/src/layouts/BasicLayout.jsx
+++ b/frontend/src/layouts/BasicLayout.jsx
@@ -1,1325 +1,1326 @@
-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',
- },
- 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: