cangku/frontend/src/layouts/BasicLayout.jsx
lunanting b0f56b27f5 客退看板首仓改为崔桥工厂仓(数据源:崔桥工厂客退临时上架仓)
按 2026-09-25 业务口径,第一张子看板由「横林二仓客退数据监控」改为
「崔桥工厂仓客退数据监控」,取数仓位同步改为聚水潭「崔桥工厂客退临时上架仓」。

- 仓库配置:简尚-崔桥工厂仓 wms_co_id=15038426,客退位 customize_qty_1
- key 由 henglin2 改为 cuiqiao,同步改路由 / 侧边栏 / 路由映射 / 权限条目
- 旧路径 returns-inventory/henlin2* 保留为重定向,避免历史链接 404
- 核销看板(returnMonitorService)仍指向横林二仓,前端入口已下线,本次未动

副本取证:该仓客退位当前只有 2 编码 / 2 件(退货单有量:ERP 3115 条 +
奇门 1812 条),属「客退上架库存未同步进副本」,页面需靠导入功能补数据。
2026-09-25 14:16:11 +08:00

1389 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;