按 2026-09-25 业务口径,第一张子看板由「横林二仓客退数据监控」改为 「崔桥工厂仓客退数据监控」,取数仓位同步改为聚水潭「崔桥工厂客退临时上架仓」。 - 仓库配置:简尚-崔桥工厂仓 wms_co_id=15038426,客退位 customize_qty_1 - key 由 henglin2 改为 cuiqiao,同步改路由 / 侧边栏 / 路由映射 / 权限条目 - 旧路径 returns-inventory/henlin2* 保留为重定向,避免历史链接 404 - 核销看板(returnMonitorService)仍指向横林二仓,前端入口已下线,本次未动 副本取证:该仓客退位当前只有 2 编码 / 2 件(退货单有量:ERP 3115 条 + 奇门 1812 条),属「客退上架库存未同步进副本」,页面需靠导入功能补数据。
1389 lines
45 KiB
JavaScript
1389 lines
45 KiB
JavaScript
import React, { useEffect, useMemo, useState } from 'react';
|
||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||
import {
|
||
AppstoreOutlined,
|
||
CloudOutlined,
|
||
DashboardOutlined,
|
||
DesktopOutlined,
|
||
DownOutlined,
|
||
ExperimentOutlined,
|
||
FundProjectionScreenOutlined,
|
||
InboxOutlined,
|
||
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,
|
||
inbox: InboxOutlined,
|
||
'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' },
|
||
'returns-inventory-cuiqiao': {
|
||
path: '/planning/operation/returns-inventory/cuiqiao',
|
||
activePath: '/planning/operation/returns-inventory/cuiqiao',
|
||
matchMode: 'prefix',
|
||
},
|
||
'returns-inventory-subei': {
|
||
path: '/planning/operation/returns-inventory/subei',
|
||
activePath: '/planning/operation/returns-inventory/subei',
|
||
matchMode: 'prefix',
|
||
},
|
||
'returns-inventory-suining': {
|
||
path: '/planning/operation/returns-inventory/suining',
|
||
activePath: '/planning/operation/returns-inventory/suining',
|
||
matchMode: 'prefix',
|
||
},
|
||
'returns-inventory-shandong': {
|
||
path: '/planning/operation/returns-inventory/shandong',
|
||
activePath: '/planning/operation/returns-inventory/shandong',
|
||
matchMode: 'prefix',
|
||
},
|
||
'returns-inventory-henan': {
|
||
path: '/planning/operation/returns-inventory/henan',
|
||
activePath: '/planning/operation/returns-inventory/henan',
|
||
matchMode: 'prefix',
|
||
},
|
||
'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 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) return null;
|
||
if (item.requiresAdmin && !canUseSettings) return null;
|
||
const itemRoute = SIDEBAR_ROUTE_MAP[item.routeKey] || {};
|
||
const rawChildren = Array.isArray(item.children) ? item.children : null;
|
||
const children = rawChildren
|
||
? sortSidebarEntries(rawChildren).map((child) => buildItem(child)).filter(Boolean)
|
||
: undefined;
|
||
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),
|
||
...(children && children.length ? { children } : (rawChildren ? { children: [] } : {})),
|
||
};
|
||
};
|
||
|
||
const normalized = buildItem(entry);
|
||
if (!normalized) return [];
|
||
normalized.type = entry.type;
|
||
return [normalized];
|
||
});
|
||
|
||
// 递归收集默认展开的菜单 key(支持多级)
|
||
const collectDefaultExpandedKeys = (entries = []) => entries.flatMap((entry) => [
|
||
...(entry?.defaultExpanded && entry?.key ? [entry.key] : []),
|
||
...collectDefaultExpandedKeys(entry?.children || []),
|
||
]);
|
||
|
||
// 递归判断菜单项(含其子孙)是否命中当前路径
|
||
const isNavEntryActive = (entry, pathname) => {
|
||
if (!entry) return false;
|
||
if (entry.match?.(pathname)) return true;
|
||
return Array.isArray(entry.children) && entry.children.some((child) => isNavEntryActive(child, pathname));
|
||
};
|
||
|
||
// 侧边栏菜单节点(递归渲染,支持三级及以上)
|
||
const SidebarNavNode = ({
|
||
item, level, pathname, expandedKeys, onToggle, onLeaf, collapsed,
|
||
}) => {
|
||
const Icon = item.icon || AppstoreOutlined;
|
||
const expandable = Array.isArray(item.children) && item.children.length > 0;
|
||
const descendantActive = expandable && item.children.some((child) => isNavEntryActive(child, pathname));
|
||
const selfActive = Boolean(item.match?.(pathname));
|
||
const active = selfActive || (collapsed && descendantActive);
|
||
const expanded = expandable && expandedKeys.includes(item.key) && !collapsed;
|
||
const isTop = level <= 1;
|
||
const baseClass = isTop ? 'planning-subsystem-nav-item' : 'planning-subsystem-subnav-item';
|
||
const iconClass = isTop ? 'planning-subsystem-nav-icon' : 'planning-subsystem-subnav-icon';
|
||
|
||
return (
|
||
<div className="planning-subsystem-nav-group">
|
||
<button
|
||
type="button"
|
||
className={`${baseClass}${active ? ' is-active' : ''}${descendantActive ? ' is-descendant-active' : ''}${expandable ? ' is-expandable' : ''}`}
|
||
style={isTop ? undefined : { paddingLeft: 36 + (level - 2) * 20 }}
|
||
onClick={() => {
|
||
if (expandable) {
|
||
onToggle(item.key);
|
||
return;
|
||
}
|
||
onLeaf(item);
|
||
}}
|
||
title={item.label}
|
||
aria-expanded={expandable ? expanded : undefined}
|
||
>
|
||
<Icon className={iconClass} />
|
||
<span className={isTop ? 'planning-subsystem-nav-label' : 'planning-subsystem-subnav-label'}>{item.label}</span>
|
||
{expandable ? (
|
||
<span className="planning-subsystem-nav-chevron">
|
||
{expanded ? <DownOutlined /> : <RightOutlined />}
|
||
</span>
|
||
) : null}
|
||
</button>
|
||
|
||
{expandable && expanded ? (
|
||
<div className="planning-subsystem-subnav">
|
||
{item.children.map((child) => (
|
||
<SidebarNavNode
|
||
key={child.key}
|
||
item={child}
|
||
level={level + 1}
|
||
pathname={pathname}
|
||
expandedKeys={expandedKeys}
|
||
onToggle={onToggle}
|
||
onLeaf={onLeaf}
|
||
collapsed={collapsed}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
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(collectDefaultExpandedKeys(nextSidebarConfig.entries));
|
||
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: <SettingOutlined />,
|
||
label: '系统设置',
|
||
onClick: () => navigate('/settings'),
|
||
}
|
||
: null,
|
||
{
|
||
key: 'logout',
|
||
icon: <LogoutOutlined />,
|
||
label: '退出登录',
|
||
onClick: handleLogout,
|
||
},
|
||
].filter(Boolean);
|
||
|
||
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 <AppShellSkeleton />;
|
||
}
|
||
|
||
return (
|
||
<div className={`planning-subsystem-shell${isImmersivePlanningRoute ? ' is-immersive' : ''}${sidebarCollapsed ? ' is-sidebar-collapsed' : ''}${sidebarDarkMode ? ' is-sidebar-dark' : ''}`}>
|
||
<aside className="planning-subsystem-sidebar">
|
||
<button
|
||
type="button"
|
||
className="planning-subsystem-brand"
|
||
onClick={() => { window.location.href = BRAND_HOME_URL; }}
|
||
title="返回主系统"
|
||
>
|
||
<span className="planning-subsystem-brand-logo">
|
||
{siteConfig.logo ? <img src={siteConfig.logo} alt="" /> : <CloudOutlined />}
|
||
</span>
|
||
<span className="planning-subsystem-brand-title">{siteConfig.siteName || FALLBACK_SITE_NAME}</span>
|
||
</button>
|
||
|
||
<nav className="planning-subsystem-nav" aria-label="主导航">
|
||
{sidebarSections.map((item) => {
|
||
if (item.type === 'section') {
|
||
return (
|
||
<div key={item.key} className="planning-subsystem-nav-section">
|
||
{item.label}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<SidebarNavNode
|
||
key={item.key}
|
||
item={item}
|
||
level={1}
|
||
pathname={pathname}
|
||
expandedKeys={expandedSidebarKeys}
|
||
onToggle={toggleSidebarGroup}
|
||
onLeaf={handleSidebarLeafClick}
|
||
collapsed={sidebarCollapsed}
|
||
/>
|
||
);
|
||
})}
|
||
</nav>
|
||
|
||
<div className="planning-subsystem-sidebar-bottom">
|
||
<div className="planning-subsystem-sidebar-separator" />
|
||
<div className="planning-subsystem-theme-row" title={sidebarDarkMode ? '关闭侧栏暗黑模式' : '开启侧栏暗黑模式'}>
|
||
{sidebarDarkMode ? <MoonOutlined /> : <SunOutlined />}
|
||
<span className="planning-subsystem-theme-label">暗黑模式</span>
|
||
<Switch
|
||
size="small"
|
||
checked={sidebarDarkMode}
|
||
onChange={handleSidebarDarkModeChange}
|
||
aria-label="侧栏暗黑模式"
|
||
/>
|
||
</div>
|
||
<div className="planning-subsystem-account-row">
|
||
<button
|
||
type="button"
|
||
className="planning-subsystem-collapse"
|
||
onClick={() => setSidebarCollapsed((value) => !value)}
|
||
title={sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'}
|
||
>
|
||
{sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||
<span className="planning-subsystem-collapse-label">{sidebarCollapsed ? '展开' : '收起'}</span>
|
||
</button>
|
||
|
||
<Dropdown menu={{ items: userMenu }} trigger={['click']} placement="topRight">
|
||
<button type="button" className="planning-subsystem-user" title={userDisplayName}>
|
||
<img src={userAvatar} alt={userDisplayName} />
|
||
<span className="planning-subsystem-user-copy">
|
||
<strong>{userDisplayName}</strong>
|
||
</span>
|
||
<span className="planning-subsystem-user-more" aria-hidden="true">
|
||
<span />
|
||
<span />
|
||
<span />
|
||
</span>
|
||
</button>
|
||
</Dropdown>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main className={`planning-subsystem-content${isImmersivePlanningRoute ? ' is-immersive' : ''}`}>
|
||
<ErrorBoundary key={location.pathname}>
|
||
<Outlet context={{ sidebarCollapsed, setSidebarCollapsed }} />
|
||
</ErrorBoundary>
|
||
</main>
|
||
|
||
<style>{`
|
||
.planning-subsystem-shell {
|
||
--sidebar-width: var(--sycm-sidebar-width);
|
||
--sidebar-collapsed-width: var(--sycm-sidebar-collapsed-width);
|
||
--sidebar-bg: var(--sycm-sidebar-bg);
|
||
--sidebar-border: var(--sycm-sidebar-border);
|
||
--sidebar-active-bg: var(--sycm-sidebar-active-bg);
|
||
--sidebar-active-text: var(--sycm-sidebar-active-text);
|
||
--sidebar-hover-bg: var(--sycm-sidebar-hover-bg);
|
||
--sidebar-icon: var(--sycm-sidebar-icon-active);
|
||
--sidebar-icon-muted: var(--sycm-sidebar-icon);
|
||
--sidebar-text: var(--sycm-sidebar-text);
|
||
--sidebar-muted: var(--sycm-sidebar-muted);
|
||
--sidebar-section: var(--sycm-sidebar-tertiary);
|
||
--sidebar-accent: var(--sycm-sidebar-accent);
|
||
display: flex;
|
||
min-height: 100vh;
|
||
background: var(--sycm-bg-page-surface);
|
||
font-family: var(--sycm-font-family);
|
||
}
|
||
|
||
.planning-subsystem-shell.is-immersive {
|
||
background: var(--sycm-bg-page-surface);
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-dark {
|
||
--sidebar-bg: var(--sycm-sidebar-dark-bg);
|
||
--sidebar-border: var(--sycm-sidebar-dark-border);
|
||
--sidebar-active-bg: var(--sycm-sidebar-dark-active-bg);
|
||
--sidebar-active-text: var(--sycm-sidebar-dark-active-text);
|
||
--sidebar-hover-bg: var(--sycm-sidebar-dark-hover-bg);
|
||
--sidebar-icon: var(--sycm-sidebar-dark-icon-active);
|
||
--sidebar-icon-muted: var(--sycm-sidebar-dark-icon);
|
||
--sidebar-text: var(--sycm-sidebar-dark-text);
|
||
--sidebar-muted: var(--sycm-sidebar-dark-muted);
|
||
--sidebar-section: var(--sycm-sidebar-dark-tertiary);
|
||
--sidebar-accent: var(--sycm-sidebar-dark-accent);
|
||
}
|
||
|
||
.planning-subsystem-sidebar {
|
||
position: sticky;
|
||
top: 0;
|
||
display: flex;
|
||
width: var(--sidebar-width);
|
||
height: 100vh;
|
||
flex: 0 0 var(--sidebar-width);
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: var(--sycm-space-md) var(--sycm-space-xs) var(--sycm-space-sm);
|
||
border-right: 1px solid var(--sidebar-border);
|
||
background: var(--sidebar-bg);
|
||
box-shadow: var(--sycm-shadow-card);
|
||
transition:
|
||
width 0.25s ease,
|
||
flex-basis 0.25s ease,
|
||
padding 0.25s ease;
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-sidebar {
|
||
width: var(--sidebar-collapsed-width);
|
||
flex-basis: var(--sidebar-collapsed-width);
|
||
align-items: center;
|
||
padding: var(--sycm-space-md) var(--sycm-space-xs) var(--sycm-space-sm);
|
||
}
|
||
|
||
.planning-subsystem-brand {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
min-height: 52px;
|
||
justify-content: flex-start;
|
||
gap: 12px;
|
||
margin-bottom: var(--sycm-space-sm);
|
||
padding: 0 10px;
|
||
border: 0;
|
||
border-radius: var(--sycm-radius-sm);
|
||
background: transparent;
|
||
color: var(--sidebar-text);
|
||
font-family: inherit;
|
||
font-size: var(--sycm-font-size-title);
|
||
font-weight: var(--sycm-font-weight-semibold);
|
||
line-height: var(--sycm-line-height-tight);
|
||
letter-spacing: 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.planning-subsystem-brand-logo {
|
||
display: inline-flex;
|
||
width: 32px;
|
||
height: 32px;
|
||
flex: 0 0 32px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
border-radius: var(--sycm-radius-sm);
|
||
color: var(--sidebar-accent);
|
||
font-size: 24px;
|
||
}
|
||
|
||
.planning-subsystem-brand-logo img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
display: block;
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-dark .planning-subsystem-brand-logo {
|
||
padding: 4px;
|
||
background: rgba(255, 255, 255, 0.94);
|
||
}
|
||
|
||
.planning-subsystem-brand-title {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.planning-subsystem-nav {
|
||
display: flex;
|
||
min-height: 0;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
overflow: auto;
|
||
padding: 0;
|
||
}
|
||
|
||
.planning-subsystem-nav::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
.planning-subsystem-nav-group {
|
||
display: flex;
|
||
width: 100%;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.planning-subsystem-nav-section {
|
||
height: 18px;
|
||
margin: 10px 8px 2px;
|
||
color: var(--sidebar-section);
|
||
font-size: var(--sycm-font-size-caption);
|
||
font-weight: var(--sycm-font-weight-medium);
|
||
line-height: 18px;
|
||
letter-spacing: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.planning-subsystem-nav-item {
|
||
position: relative;
|
||
display: flex;
|
||
width: 100%;
|
||
height: 36px;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 10px;
|
||
padding: 0 10px;
|
||
border: 0;
|
||
border-radius: var(--sycm-radius-sm);
|
||
background: transparent;
|
||
color: var(--sidebar-text);
|
||
font-family: inherit;
|
||
font-size: var(--sycm-font-size-body);
|
||
font-weight: var(--sycm-font-weight-medium);
|
||
line-height: var(--sycm-line-height-tight);
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: color 0.18s ease;
|
||
}
|
||
|
||
.planning-subsystem-nav-item::before,
|
||
.planning-subsystem-subnav-item::before {
|
||
content: '';
|
||
position: absolute;
|
||
z-index: 0;
|
||
inset: 0 24px 0 -8px;
|
||
border-radius: 0 var(--sycm-radius-pill) var(--sycm-radius-pill) 0;
|
||
background: transparent;
|
||
pointer-events: none;
|
||
transition: background-color 0.18s ease;
|
||
}
|
||
|
||
.planning-subsystem-nav-item > *,
|
||
.planning-subsystem-subnav-item > * {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.planning-subsystem-nav-item:focus,
|
||
.planning-subsystem-subnav-item:focus {
|
||
outline: none;
|
||
}
|
||
|
||
.planning-subsystem-nav-item:focus-visible::before,
|
||
.planning-subsystem-subnav-item:focus-visible::before {
|
||
box-shadow: inset 0 0 0 var(--sycm-line-width-base) var(--sycm-color-primary-border);
|
||
}
|
||
|
||
.planning-subsystem-nav-item:hover {
|
||
color: var(--sidebar-text);
|
||
}
|
||
|
||
.planning-subsystem-nav-item:hover::before {
|
||
background: var(--sidebar-hover-bg);
|
||
}
|
||
|
||
.planning-subsystem-nav-item.is-active {
|
||
color: var(--sidebar-active-text);
|
||
font-weight: var(--sycm-font-weight-semibold);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.planning-subsystem-nav-item.is-active::before {
|
||
background: var(--sidebar-active-bg);
|
||
}
|
||
|
||
.planning-subsystem-nav-item.is-descendant-active:not(.is-active) {
|
||
color: var(--sidebar-active-text);
|
||
}
|
||
|
||
.planning-subsystem-nav-icon {
|
||
flex: 0 0 17px;
|
||
color: var(--sidebar-icon-muted);
|
||
font-size: 15px;
|
||
}
|
||
|
||
.planning-subsystem-nav-item.is-active .planning-subsystem-nav-icon,
|
||
.planning-subsystem-nav-item.is-descendant-active .planning-subsystem-nav-icon,
|
||
.planning-subsystem-nav-item:hover .planning-subsystem-nav-icon {
|
||
color: var(--sidebar-icon);
|
||
}
|
||
|
||
.planning-subsystem-nav-label {
|
||
min-width: 0;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.planning-subsystem-nav-chevron {
|
||
display: inline-flex;
|
||
width: 12px;
|
||
height: 12px;
|
||
flex: 0 0 12px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-left: auto;
|
||
color: var(--sidebar-section);
|
||
font-size: var(--sycm-font-size-micro);
|
||
line-height: 1;
|
||
}
|
||
|
||
.planning-subsystem-subnav {
|
||
display: flex;
|
||
width: 100%;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
margin: 0 0 4px;
|
||
}
|
||
|
||
.planning-subsystem-subnav-item {
|
||
position: relative;
|
||
display: flex;
|
||
width: 100%;
|
||
height: var(--sycm-control-height-sm);
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 9px;
|
||
padding: 0 8px 0 36px;
|
||
border: 0;
|
||
border-radius: var(--sycm-radius-sm);
|
||
background: transparent;
|
||
color: var(--sidebar-muted);
|
||
font-family: inherit;
|
||
font-size: var(--sycm-font-size-caption);
|
||
font-weight: var(--sycm-font-weight-medium);
|
||
line-height: 18px;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: color 0.18s ease;
|
||
}
|
||
|
||
.planning-subsystem-subnav-item:hover {
|
||
color: var(--sidebar-text);
|
||
}
|
||
|
||
.planning-subsystem-subnav-item:hover::before {
|
||
background: var(--sidebar-hover-bg);
|
||
}
|
||
|
||
.planning-subsystem-subnav-item.is-active {
|
||
color: var(--sidebar-active-text);
|
||
font-weight: var(--sycm-font-weight-semibold);
|
||
}
|
||
|
||
.planning-subsystem-subnav-item.is-active::before {
|
||
background: var(--sidebar-active-bg);
|
||
}
|
||
|
||
.planning-subsystem-subnav-item.is-descendant-active:not(.is-active) {
|
||
color: var(--sidebar-active-text);
|
||
}
|
||
|
||
.planning-subsystem-subnav-label {
|
||
min-width: 0;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.planning-subsystem-subnav-item.is-expandable .planning-subsystem-nav-chevron {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.planning-subsystem-subnav-icon {
|
||
flex: 0 0 14px;
|
||
color: var(--sidebar-icon-muted);
|
||
font-size: var(--sycm-font-size-caption);
|
||
}
|
||
|
||
.planning-subsystem-subnav-item:hover .planning-subsystem-subnav-icon,
|
||
.planning-subsystem-subnav-item.is-active .planning-subsystem-subnav-icon {
|
||
color: var(--sidebar-icon);
|
||
}
|
||
|
||
.planning-subsystem-nav-badge {
|
||
display: inline-flex;
|
||
min-width: 18px;
|
||
height: 18px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: var(--sycm-radius-pill);
|
||
padding: 0 5px;
|
||
background: var(--sycm-bg-subtle);
|
||
color: var(--sycm-text-secondary);
|
||
font-size: var(--sycm-font-size-caption);
|
||
font-weight: var(--sycm-font-weight-medium);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.planning-subsystem-sidebar-bottom {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.planning-subsystem-sidebar-separator {
|
||
height: 1px;
|
||
margin: 6px 0;
|
||
background: var(--sidebar-border);
|
||
}
|
||
|
||
.planning-subsystem-theme-row {
|
||
display: flex;
|
||
width: 100%;
|
||
height: var(--sycm-control-height);
|
||
align-items: center;
|
||
gap: var(--sycm-space-xs);
|
||
padding: 0 8px;
|
||
border-radius: var(--sycm-radius-sm);
|
||
color: var(--sidebar-muted);
|
||
font-size: var(--sycm-font-size-caption);
|
||
font-weight: var(--sycm-font-weight-medium);
|
||
}
|
||
|
||
.planning-subsystem-theme-row:hover {
|
||
background: var(--sidebar-hover-bg);
|
||
color: var(--sidebar-text);
|
||
}
|
||
|
||
.planning-subsystem-theme-row > .anticon {
|
||
flex: 0 0 16px;
|
||
color: var(--sidebar-icon-muted);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.planning-subsystem-theme-label {
|
||
min-width: 0;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.planning-subsystem-theme-row .ant-switch {
|
||
flex: 0 0 auto;
|
||
background: var(--sidebar-border);
|
||
}
|
||
|
||
.planning-subsystem-theme-row .ant-switch.ant-switch-checked {
|
||
background: var(--sidebar-accent);
|
||
}
|
||
|
||
.planning-subsystem-account-row {
|
||
display: grid;
|
||
width: 100%;
|
||
min-width: 0;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.planning-subsystem-collapse {
|
||
display: flex;
|
||
width: auto;
|
||
height: var(--sycm-control-height-sm);
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: var(--sycm-space-xs);
|
||
padding: 0 8px;
|
||
border: 0;
|
||
border-radius: var(--sycm-radius-sm);
|
||
background: transparent;
|
||
color: var(--sidebar-muted);
|
||
font-family: inherit;
|
||
font-size: var(--sycm-font-size-caption);
|
||
font-weight: var(--sycm-font-weight-medium);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.planning-subsystem-collapse:hover {
|
||
background: var(--sidebar-hover-bg);
|
||
color: var(--sidebar-text);
|
||
}
|
||
|
||
.planning-subsystem-collapse .anticon {
|
||
flex: 0 0 16px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.planning-subsystem-user {
|
||
display: flex;
|
||
width: 100%;
|
||
min-width: 0;
|
||
min-height: var(--sycm-control-height-sm);
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 0;
|
||
padding: 0 8px;
|
||
border: 0;
|
||
border-radius: var(--sycm-radius-sm);
|
||
background: transparent;
|
||
color: var(--sidebar-text);
|
||
font-family: inherit;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.planning-subsystem-user:hover {
|
||
background: var(--sidebar-hover-bg);
|
||
}
|
||
|
||
.planning-subsystem-user img {
|
||
width: 20px;
|
||
height: 20px;
|
||
flex: 0 0 20px;
|
||
border-radius: var(--sycm-radius-pill);
|
||
object-fit: cover;
|
||
background: var(--sycm-bg-subtle);
|
||
}
|
||
|
||
.planning-subsystem-user-copy {
|
||
display: block;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.planning-subsystem-user-copy strong {
|
||
overflow: hidden;
|
||
color: var(--sidebar-text);
|
||
font-size: var(--sycm-font-size-caption);
|
||
font-weight: var(--sycm-font-weight-medium);
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.planning-subsystem-user-more {
|
||
display: inline-flex;
|
||
width: 4px;
|
||
flex: 0 0 4px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
color: var(--sidebar-muted);
|
||
}
|
||
|
||
.planning-subsystem-user-more span {
|
||
width: 3px;
|
||
height: 3px;
|
||
border-radius: var(--sycm-radius-pill);
|
||
background: currentColor;
|
||
}
|
||
|
||
.planning-subsystem-content {
|
||
min-width: 0;
|
||
height: 100vh;
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 0;
|
||
background: var(--sycm-bg-page-surface);
|
||
}
|
||
|
||
.planning-subsystem-content.is-immersive {
|
||
height: 100vh;
|
||
padding: 0;
|
||
}
|
||
|
||
.planning-subsystem-content::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
.planning-subsystem-content::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.planning-subsystem-content::-webkit-scrollbar-thumb {
|
||
background: var(--sycm-border-strong);
|
||
border-radius: var(--sycm-radius-pill);
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-brand,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-nav-item,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-theme-row,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-collapse,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-user {
|
||
justify-content: center;
|
||
padding-right: 0;
|
||
padding-left: 0;
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-brand-title,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-nav-label,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-nav-badge,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-nav-chevron,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-nav-section,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-subnav,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-theme-label,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-collapse-label,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-user-copy,
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-user-more {
|
||
display: none;
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-nav-item {
|
||
width: 36px;
|
||
margin-left: 0;
|
||
border-radius: var(--sycm-radius-pill);
|
||
}
|
||
|
||
|
||
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-nav-item::before {
|
||
inset: 0;
|
||
border-radius: var(--sycm-radius-pill);
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-theme-row {
|
||
width: 36px;
|
||
min-width: 36px;
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-account-row {
|
||
display: flex;
|
||
width: 36px;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-theme-row > .anticon {
|
||
display: none;
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-collapse {
|
||
width: 36px;
|
||
min-width: 36px;
|
||
color: var(--sidebar-icon-muted);
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-collapse:hover {
|
||
color: var(--sidebar-icon);
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-collapse .anticon {
|
||
display: inline-flex;
|
||
flex: 0 0 16px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: currentColor;
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-sidebar-separator {
|
||
width: var(--sycm-control-height-sm);
|
||
}
|
||
|
||
.planning-subsystem-shell.is-sidebar-collapsed .planning-subsystem-user img {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.planning-subsystem-shell {
|
||
--sidebar-width: var(--sycm-sidebar-collapsed-width);
|
||
}
|
||
|
||
.planning-subsystem-sidebar {
|
||
width: var(--sidebar-width);
|
||
flex-basis: var(--sidebar-width);
|
||
align-items: center;
|
||
padding: var(--sycm-space-md) var(--sycm-space-xs) var(--sycm-space-sm);
|
||
}
|
||
|
||
.planning-subsystem-content {
|
||
height: 100vh;
|
||
}
|
||
|
||
.planning-subsystem-content.is-immersive {
|
||
height: 100vh;
|
||
}
|
||
|
||
.planning-subsystem-nav-item,
|
||
.planning-subsystem-nav-item:hover,
|
||
.planning-subsystem-nav-item.is-active {
|
||
width: 36px;
|
||
margin-left: 0;
|
||
padding-right: 0;
|
||
padding-left: 0;
|
||
border-radius: var(--sycm-radius-pill);
|
||
justify-content: center;
|
||
}
|
||
|
||
.planning-subsystem-nav-item::before {
|
||
inset: 0;
|
||
border-radius: var(--sycm-radius-pill);
|
||
}
|
||
|
||
|
||
|
||
|
||
.planning-subsystem-brand-title,
|
||
.planning-subsystem-nav-label,
|
||
.planning-subsystem-nav-badge,
|
||
.planning-subsystem-nav-chevron,
|
||
.planning-subsystem-nav-section,
|
||
.planning-subsystem-subnav,
|
||
.planning-subsystem-theme-label,
|
||
.planning-subsystem-collapse-label,
|
||
.planning-subsystem-user-copy,
|
||
.planning-subsystem-user-more {
|
||
display: none;
|
||
}
|
||
}
|
||
`}</style>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default BasicLayout;
|