Files
ewoooc/MOMO Pro/app/shell.jsx

403 lines
17 KiB
JavaScript
Raw Permalink 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.
// EwoooC - 後台外殼Sidebar + TopbarNothing × Claude 風格
// 對應截圖的「商品看板 / 活動看板 / 分析報表 / 廠商缺貨 / AI 助手 / 雲端匯入 / 系統管理」
const NAV_GROUPS = [
{
title: '監控',
items: [
{ id: 'dashboard', label: '商品看板', icon: 'dashboard', code: '01' },
{ id: 'campaigns', label: '活動看板', icon: 'marketing', code: '02' },
{
id: 'analytics', label: '分析報表', icon: 'analytics', code: '03',
children: [
{ id: 'analytics-sales', label: '業績分析', code: '03·1' },
{ id: 'analytics-daily', label: '當日業績', code: '03·2' },
{ id: 'analytics-growth', label: '成長分析', code: '03·3' },
{ id: 'analytics-monthly', label: '月份總表', code: '03·4' },
],
},
],
},
{
title: '營運',
items: [
{ id: 'outofstock',label: '廠商缺貨', icon: 'inventory', code: '04', badge: 48 },
{ id: 'ai', label: 'AI 助手', icon: 'sparkle', code: '05' },
{ id: 'cloud', label: '雲端匯入', icon: 'download', code: '06' },
],
},
{
title: '系統',
items: [
{ id: 'settings', label: '系統管理', icon: 'settings', code: '07' },
],
},
];
// ===== Sidebar =====
const Sidebar = ({ active, onNavigate, collapsed, sidebarTheme }) => {
const isDark = sidebarTheme === 'dark';
const width = collapsed ? 72 : 240;
// 子選單展開狀態:父 id 命中active 為自己或子項之一)就展開
const parentOf = React.useMemo(() => {
const map = {};
NAV_GROUPS.forEach(g => g.items.forEach(it => {
if (it.children) it.children.forEach(c => { map[c.id] = it.id; });
}));
return map;
}, []);
const [openMap, setOpenMap] = React.useState({});
const isOpen = (id) => openMap[id] ?? (active === id || parentOf[active] === id);
// 淺色側邊欄改用米色 paper不純白active 用焦糖橘
const bg = isDark ? '#1f1a14' : 'var(--momo-bg-paper)';
const textMuted = isDark ? 'rgba(255,247,240,0.55)' : 'var(--momo-text-secondary)';
const text = isDark ? '#faf7f0' : 'var(--momo-text-primary)';
const itemHoverBg = isDark ? 'rgba(255,247,240,0.06)' : 'rgba(201,100,66,0.08)';
const itemActiveBg = isDark ? 'rgba(201,100,66,0.18)' : 'var(--momo-accent)';
const itemActiveText = isDark ? '#faf7f0' : '#faf7f0';
const itemActiveBorder = 'var(--momo-accent)';
const groupTitle = isDark ? 'rgba(255,247,240,0.4)' : 'var(--momo-text-tertiary)';
const borderC = isDark ? 'rgba(255,247,240,0.08)' : 'var(--momo-border-light)';
return (
<aside style={{
width, flexShrink: 0,
background: bg,
borderRight: `1px solid ${borderC}`,
display: 'flex', flexDirection: 'column',
transition: 'width var(--momo-duration-normal) var(--momo-ease-in-out)',
overflow: 'hidden',
position: 'relative',
zIndex: 2,
}}>
{/* Logo */}
<div style={{
height: 64, flexShrink: 0,
display: 'flex', alignItems: 'center', gap: 10,
padding: collapsed ? '0' : '0 20px',
justifyContent: collapsed ? 'center' : 'flex-start',
borderBottom: `1px solid ${borderC}`,
}}>
<div style={{
width: 32, height: 32,
borderRadius: 2,
background: isDark ? '#faf7f0' : 'var(--momo-ink)',
color: isDark ? 'var(--momo-ink)' : '#faf7f0',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
gap: 1.5, padding: 5,
flexShrink: 0,
}}>
{[1,1,1,1,0,1,1,1,1].map((on, i) => (
<span key={i} style={{ background: on ? 'currentColor' : 'transparent', borderRadius: '50%' }} />
))}
</div>
{!collapsed && (
<div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1.05 }}>
<span className="momo-display" style={{
fontSize: 18, fontWeight: 700, color: text, letterSpacing: '-0.02em',
}}>EwoooC</span>
<span className="momo-label" style={{ color: textMuted }}>價格監控 v2.4</span>
</div>
)}
</div>
{/* Nav */}
<nav className="momo-scroll" style={{ flex: 1, overflowY: 'auto', padding: '12px 8px' }}>
{NAV_GROUPS.map((group, gi) => (
<div key={gi} style={{ marginBottom: 4 }}>
{!collapsed && (
<div className="momo-label" style={{
padding: '14px 12px 8px',
color: groupTitle,
display: 'flex', alignItems: 'center', gap: 8,
}}>
<span style={{ flex: 1 }}>{group.title}</span>
<span style={{ height: 1, flex: 2, background: borderC }} />
</div>
)}
{collapsed && gi > 0 && <div style={{ height: 1, background: borderC, margin: '8px 12px' }} />}
{group.items.map(item => {
const hasChildren = !!item.children;
const childActive = hasChildren && item.children.some(c => c.id === active);
const isActive = active === item.id || (hasChildren && childActive && collapsed);
const expanded = !collapsed && hasChildren && (isOpen(item.id) || childActive);
const onClick = () => {
if (hasChildren && !collapsed) {
// 父層點擊 = 切換展開;若尚未指向任何子項,順手導向第一個子項
setOpenMap(m => ({ ...m, [item.id]: !isOpen(item.id) }));
if (!childActive) onNavigate(item.children[0].id);
} else {
onNavigate(item.id);
}
};
return (
<React.Fragment key={item.id}>
<button onClick={onClick}
title={collapsed ? item.label : ''}
style={{
width: '100%',
display: 'flex', alignItems: 'center',
gap: 12,
padding: collapsed ? '10px' : '9px 12px',
justifyContent: collapsed ? 'center' : 'flex-start',
borderRadius: 4,
background: isActive ? itemActiveBg : 'transparent',
color: isActive ? itemActiveText : (isDark ? textMuted : text),
fontSize: 'var(--momo-font-size-sm)',
fontWeight: isActive ? 'var(--momo-font-weight-semibold)' : 'var(--momo-font-weight-medium)',
transition: 'var(--momo-transition-base)',
marginBottom: 2,
position: 'relative',
border: isActive && isDark ? `1px solid ${itemActiveBorder}` : '1px solid transparent',
}}
onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = itemHoverBg; }}
onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }}
>
<Icon name={item.icon} size={16} />
{!collapsed && (
<>
<span style={{ flex: 1, textAlign: 'left' }}>{item.label}</span>
{hasChildren && (
<Icon name="chevronDown" size={12} style={{
opacity: 0.55,
transform: expanded ? 'rotate(0deg)' : 'rotate(-90deg)',
transition: 'transform 0.15s ease',
}} />
)}
{!hasChildren && item.code && (
<span className="momo-mono" style={{
fontSize: 10, opacity: isActive ? 0.8 : 0.45, fontWeight: 600,
}}>{item.code}</span>
)}
{item.badge && (
<span className="momo-mono" style={{
background: 'var(--momo-accent)',
color: '#fff',
fontSize: 10, fontWeight: 700,
padding: '1px 6px',
borderRadius: 2,
minWidth: 20, textAlign: 'center',
}}>{item.badge}</span>
)}
</>
)}
{collapsed && item.badge && (
<span style={{
position: 'absolute', top: 6, right: 8,
width: 8, height: 8, borderRadius: '50%',
background: 'var(--momo-accent)',
border: `2px solid ${isDark ? '#1f1a14' : 'var(--momo-bg-paper)'}`,
}} />
)}
</button>
{/* 子選單 */}
{expanded && item.children.map(child => {
const cActive = active === child.id;
return (
<button key={child.id} onClick={() => onNavigate(child.id)}
style={{
width: '100%',
display: 'flex', alignItems: 'center', gap: 10,
padding: '7px 12px 7px 36px',
borderRadius: 4,
background: cActive ? itemHoverBg : 'transparent',
color: cActive ? (isDark ? '#faf7f0' : 'var(--momo-accent)') : (isDark ? textMuted : 'var(--momo-text-secondary)'),
fontSize: 12,
fontWeight: cActive ? 600 : 500,
transition: 'var(--momo-transition-base)',
marginBottom: 1,
position: 'relative',
border: '1px solid transparent',
}}
onMouseEnter={e => { if (!cActive) e.currentTarget.style.background = itemHoverBg; }}
onMouseLeave={e => { if (!cActive) e.currentTarget.style.background = 'transparent'; }}
>
{/* 縮排線 */}
<span style={{
position: 'absolute', left: 22, top: 6, bottom: 6, width: 2,
background: cActive ? 'var(--momo-accent)' : borderC, borderRadius: 1,
}} />
<span style={{ flex: 1, textAlign: 'left' }}>{child.label}</span>
{child.code && (
<span className="momo-mono" style={{
fontSize: 9, opacity: cActive ? 0.8 : 0.45, fontWeight: 600,
}}>{child.code}</span>
)}
</button>
);
})}
</React.Fragment>
);
})}
</div>
))}
</nav>
{/* Bottom: 爬蟲狀態面板(暖墨底)*/}
{!collapsed && (
<div style={{ padding: 12 }}>
<div style={{
background: '#1f1a14',
color: '#faf7f0',
border: `1px solid rgba(201,100,66,0.35)`,
borderRadius: 4,
padding: 14,
position: 'relative',
overflow: 'hidden',
}}>
<div style={{ position: 'absolute', inset: 0,
backgroundImage: 'radial-gradient(circle, rgba(201,100,66,0.12) 1px, transparent 1px)',
backgroundSize: '6px 6px', pointerEvents: 'none' }} />
<div style={{ position: 'relative' }}>
<div className="momo-label" style={{ color: 'rgba(255,247,240,0.55)', marginBottom: 8 }}>
爬蟲狀態
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--momo-accent)', boxShadow: '0 0 8px var(--momo-accent)', animation: 'momo-pulse-dot 2s infinite' }} />
<span className="momo-mono" style={{ fontSize: 11, fontWeight: 600 }}>執行中</span>
</div>
<div className="momo-mono" style={{ fontSize: 10, color: 'rgba(255,247,240,0.55)', lineHeight: 1.7 }}>
上次執行 12:54:23<br/>
掃描筆數 1,569<br/>
新增筆數 +0
</div>
</div>
</div>
</div>
)}
</aside>
);
};
// ===== Topbar =====
const Topbar = ({ onToggleSidebar, onOpenCmd }) => (
<header className="momo-topbar" style={{
height: 64, flexShrink: 0,
background: 'var(--momo-bg-surface)',
borderBottom: '1px solid var(--momo-border-light)',
display: 'flex', alignItems: 'center',
padding: '0 24px',
gap: 16,
zIndex: 1,
containerType: 'inline-size',
}}>
<button onClick={onToggleSidebar}
style={{
width: 36, height: 36,
borderRadius: 4,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--momo-text-secondary)',
transition: 'var(--momo-transition-base)',
}}
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-subtle)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<Icon name="menu" size={18} />
</button>
<button onClick={onOpenCmd}
className="momo-search"
style={{
flex: 1, maxWidth: 480, minWidth: 0,
height: 38,
background: 'var(--momo-bg-paper)',
border: '1px solid var(--momo-border)',
borderRadius: 4,
padding: '0 12px',
display: 'flex', alignItems: 'center', gap: 10,
color: 'var(--momo-text-secondary)',
fontSize: 'var(--momo-font-size-sm)',
transition: 'var(--momo-transition-base)',
overflow: 'hidden',
}}
onMouseEnter={e => { e.currentTarget.style.background = '#fff'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'var(--momo-bg-paper)'; }}
>
<Icon name="search" size={15} />
<span style={{ flex: 1, minWidth: 0, textAlign: 'left', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} className="momo-mono momo-search-text">搜尋商品名稱編號品牌</span>
<kbd className="momo-mono" style={{
fontSize: 10, fontWeight: 600,
padding: '2px 6px',
background: 'var(--momo-accent)',
color: '#faf7f0',
borderRadius: 2,
}}>K</kbd>
</button>
<div style={{ flex: 1 }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{/* 排程徽章 */}
<div className="momo-schedule-pill" style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '6px 12px',
background: '#1f1a14',
color: '#faf7f0',
borderRadius: 4,
fontSize: 11,
border: '1px solid rgba(201,100,66,0.35)',
}}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--momo-accent)', boxShadow: '0 0 8px var(--momo-accent)', animation: 'momo-pulse-dot 2s infinite' }} />
<span className="momo-mono" style={{ color: 'rgba(255,247,240,0.55)' }}>下次排程</span>
<span className="momo-mono" style={{ fontWeight: 600 }}>13:00</span>
</div>
<button title="發送通知"
style={{
width: 36, height: 36, borderRadius: 4,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--momo-text-secondary)',
transition: 'var(--momo-transition-base)',
}}
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-subtle)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<Icon name="helpCircle" size={18} />
</button>
<button title="通知"
style={{
width: 36, height: 36, borderRadius: 4,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--momo-text-secondary)',
position: 'relative',
transition: 'var(--momo-transition-base)',
}}
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-subtle)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<Icon name="bell" size={18} />
<span style={{
position: 'absolute', top: 8, right: 9,
width: 8, height: 8, borderRadius: '50%',
background: 'var(--momo-accent)',
border: '2px solid var(--momo-bg-surface)',
}} />
</button>
<div style={{ width: 1, height: 24, background: 'var(--momo-border-light)', margin: '0 4px' }} />
<button style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '4px 10px 4px 4px',
height: 40,
borderRadius: 'var(--momo-radius-pill)',
transition: 'var(--momo-transition-base)',
}}
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-subtle)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<Avatar name="蜡蜡佯" size={32} gradient />
<div className="momo-user-meta" style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', lineHeight: 1.2 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--momo-text-primary)' }}>蜡蜡佯</span>
<span style={{ fontSize: 11, color: 'var(--momo-text-tertiary)' }}>管理員</span>
</div>
<Icon name="chevronDown" size={14} color="var(--momo-text-tertiary)" />
</button>
</div>
</header>
);
Object.assign(window, { Sidebar, Topbar, NAV_GROUPS });