326 lines
14 KiB
JavaScript
326 lines
14 KiB
JavaScript
// EwoooC - 後台外殼(Sidebar + Topbar)Nothing × 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' },
|
||
],
|
||
},
|
||
{
|
||
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;
|
||
|
||
// 淺色側邊欄改用米色 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 isActive = active === item.id;
|
||
return (
|
||
<button key={item.id} onClick={() => onNavigate(item.id)}
|
||
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>
|
||
{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>
|
||
);
|
||
})}
|
||
</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 });
|