684 lines
29 KiB
JavaScript
684 lines
29 KiB
JavaScript
// EwoooC - 活動看板 v2(對齊 Nothing × Claude 規範)
|
||
// 三個 Hero 版本:simple / dotted / inverse — 由 prop heroVariant 控制
|
||
|
||
// ===== 共用 - 編號標籤(呼應 dashboard) =====
|
||
const CampSectionLabel = ({ num, children, sub }) => (
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 12 }}>
|
||
<span className="momo-mono" style={{
|
||
fontSize: 11, fontWeight: 700, color: 'var(--momo-text-tertiary)',
|
||
letterSpacing: '0.08em',
|
||
}}>{num}</span>
|
||
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--momo-text-primary)', letterSpacing: '0.02em' }}>
|
||
{children}
|
||
</span>
|
||
{sub && (
|
||
<span className="momo-mono" style={{ fontSize: 10, color: 'var(--momo-text-tertiary)', marginLeft: 'auto' }}>
|
||
{sub}
|
||
</span>
|
||
)}
|
||
</div>
|
||
);
|
||
|
||
// 每個活動配不同暖色調 accent(全部留在暖色域,不用冷色)
|
||
// 對應 design-tokens.css 的 --momo-warm-* 家族
|
||
const CAMPAIGN_ACCENTS = {
|
||
flash: 'var(--momo-warm-caramel)', // 焦糖橘 #c96442
|
||
festival: 'var(--momo-warm-honey)', // 蜂蜜金 #b88416
|
||
mothers: 'var(--momo-warm-rust)', // 暖紅 #b5342f
|
||
valentine: 'var(--momo-warm-mahogany)', // 深焦糖 #8f4530
|
||
laborday: 'var(--momo-warm-earth)', // 焦土 #8a5a2b
|
||
};
|
||
|
||
// ===== 活動切換 — segmented tabs(橫向 scroll,避免溢出) =====
|
||
const CampaignSwitcher = ({ active, setActive, campaigns }) => {
|
||
const ids = Object.keys(campaigns);
|
||
return (
|
||
<div className="momo-scroll" style={{
|
||
overflowX: 'auto',
|
||
paddingBottom: 4, // for scrollbar
|
||
}}>
|
||
<div style={{
|
||
display: 'inline-flex',
|
||
background: 'var(--momo-bg-surface)',
|
||
border: '1px solid var(--momo-border-light)',
|
||
borderRadius: 4, padding: 3,
|
||
gap: 2,
|
||
}}>
|
||
{ids.map(id => {
|
||
const c = campaigns[id];
|
||
const isActive = id === active;
|
||
return (
|
||
<button key={id} onClick={() => setActive(id)} style={{
|
||
padding: '8px 14px',
|
||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||
fontSize: 13, fontWeight: 600,
|
||
color: isActive ? '#faf7f0' : 'var(--momo-text-secondary)',
|
||
background: isActive ? 'var(--momo-ink)' : 'transparent',
|
||
border: 'none', borderRadius: 2,
|
||
transition: 'var(--momo-transition-base)',
|
||
whiteSpace: 'nowrap',
|
||
}}>
|
||
<span>{c.name}</span>
|
||
<span className="momo-mono" style={{
|
||
fontSize: 10, fontWeight: 700,
|
||
padding: '1px 6px', borderRadius: 2,
|
||
background: isActive ? 'rgba(255,255,255,0.15)' : 'var(--momo-bg-subtle)',
|
||
color: isActive ? 'rgba(255,255,255,0.9)' : 'var(--momo-text-tertiary)',
|
||
letterSpacing: '0.04em',
|
||
}}>{c.total}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ===== Hero V1:簡約(米白卡 + 左 4px 直條) =====
|
||
const HeroSimple = ({ c, activeId }) => {
|
||
const accent = CAMPAIGN_ACCENTS[activeId] || 'var(--momo-accent)';
|
||
return (
|
||
<div style={{
|
||
position: 'relative',
|
||
background: 'var(--momo-bg-surface)',
|
||
border: '1px solid var(--momo-border-light)',
|
||
borderRadius: 8, padding: '28px 32px',
|
||
paddingLeft: 36,
|
||
overflow: 'hidden',
|
||
}}>
|
||
{/* 左側 4px 直條 */}
|
||
<div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 4, background: accent }} />
|
||
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 14 }}>
|
||
<span className="momo-mono" style={{
|
||
fontSize: 10, fontWeight: 700, letterSpacing: '0.12em',
|
||
color: accent, textTransform: 'uppercase',
|
||
}}>CAMPAIGN</span>
|
||
<span className="momo-mono" style={{ fontSize: 11, color: 'var(--momo-text-tertiary)' }}>
|
||
ID · {activeId.toUpperCase()}
|
||
</span>
|
||
</div>
|
||
|
||
<h1 style={{
|
||
margin: 0, fontSize: 36, fontWeight: 800,
|
||
lineHeight: 1.1, letterSpacing: '-0.02em',
|
||
color: 'var(--momo-text-primary)', marginBottom: 18,
|
||
}}>{c.name}</h1>
|
||
|
||
<div style={{ display: 'flex', gap: 28, flexWrap: 'wrap', fontFamily: 'var(--momo-font-family-mono)' }}>
|
||
<div>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--momo-text-tertiary)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
活動時段
|
||
</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--momo-text-primary)' }}>{c.time}</div>
|
||
</div>
|
||
<div>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--momo-text-tertiary)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
最後更新
|
||
</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--momo-text-primary)' }}>{c.lastUpdate}</div>
|
||
</div>
|
||
<div>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--momo-text-tertiary)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
商品總數
|
||
</div>
|
||
<div className="momo-mono" style={{ fontSize: 24, fontWeight: 800, color: 'var(--momo-text-primary)', letterSpacing: '-0.02em', lineHeight: 1 }}>
|
||
{c.total.toLocaleString()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 8, marginTop: 20 }}>
|
||
<button style={{
|
||
padding: '8px 14px', fontSize: 12, fontWeight: 600,
|
||
background: 'var(--momo-bg-paper)', color: 'var(--momo-text-primary)',
|
||
border: '1px solid var(--momo-border-light)', borderRadius: 4,
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
}}>
|
||
<Icon name="refresh" size={12} /> 手動更新
|
||
</button>
|
||
{activeId === 'flash' && (
|
||
<button style={{
|
||
padding: '8px 14px', fontSize: 12, fontWeight: 600,
|
||
background: 'var(--momo-ink)', color: '#faf7f0',
|
||
border: 'none', borderRadius: 4,
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
}}>
|
||
<Icon name="bell" size={12} /> 發送通知
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ===== Hero V2:點陣(Nothing 招牌) =====
|
||
const HeroDotted = ({ c, activeId }) => {
|
||
const accent = CAMPAIGN_ACCENTS[activeId] || 'var(--momo-accent)';
|
||
return (
|
||
<div className="camp-hero" style={{
|
||
position: 'relative',
|
||
background: 'var(--momo-bg-paper)',
|
||
border: '1px solid var(--momo-border-light)',
|
||
borderRadius: 8, padding: '28px 32px',
|
||
overflow: 'hidden',
|
||
}}>
|
||
{/* 點陣背景 */}
|
||
<div className="momo-dot-bg-dark" style={{
|
||
position: 'absolute', inset: 0, opacity: 0.6, pointerEvents: 'none',
|
||
}} />
|
||
{/* accent 角飾線 */}
|
||
<div style={{
|
||
position: 'absolute', top: 0, left: 0, width: 64, height: 4, background: accent,
|
||
}} />
|
||
<div style={{
|
||
position: 'absolute', top: 0, left: 0, width: 4, height: 64, background: accent,
|
||
}} />
|
||
|
||
<div style={{ position: 'relative' }}>
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 14 }}>
|
||
<span className="momo-mono" style={{
|
||
fontSize: 10, fontWeight: 700, letterSpacing: '0.12em',
|
||
color: accent, textTransform: 'uppercase',
|
||
padding: '3px 8px',
|
||
background: 'var(--momo-bg-surface)',
|
||
border: `1px solid ${accent}`,
|
||
borderRadius: 2,
|
||
}}>CAMPAIGN / {activeId.toUpperCase()}</span>
|
||
</div>
|
||
|
||
<h1 className="camp-hero-title" style={{
|
||
margin: 0, fontSize: 40, fontWeight: 800,
|
||
lineHeight: 1.05, letterSpacing: '-0.025em',
|
||
color: 'var(--momo-text-primary)', marginBottom: 20,
|
||
fontFamily: 'var(--momo-font-display)',
|
||
}}>{c.name}</h1>
|
||
|
||
<div className="camp-hero-meta-row" style={{ display: 'flex', gap: 32, flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||
<div>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--momo-text-tertiary)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
活動時段
|
||
</div>
|
||
<div className="momo-mono" style={{ fontSize: 13, fontWeight: 600, color: 'var(--momo-text-primary)' }}>{c.time}</div>
|
||
</div>
|
||
<div>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--momo-text-tertiary)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
最後更新
|
||
</div>
|
||
<div className="momo-mono" style={{ fontSize: 13, fontWeight: 600, color: 'var(--momo-text-primary)' }}>{c.lastUpdate}</div>
|
||
</div>
|
||
<div className="camp-hero-total-wrap" style={{ marginLeft: 'auto', textAlign: 'right' }}>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--momo-text-tertiary)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
商品總數
|
||
</div>
|
||
<div className="momo-mono camp-hero-total" style={{ fontSize: 36, fontWeight: 800, color: accent, letterSpacing: '-0.03em', lineHeight: 1 }}>
|
||
{c.total.toLocaleString()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 8, marginTop: 20 }}>
|
||
<button style={{
|
||
padding: '8px 14px', fontSize: 12, fontWeight: 600,
|
||
background: 'var(--momo-bg-surface)', color: 'var(--momo-text-primary)',
|
||
border: '1px solid var(--momo-border-light)', borderRadius: 4,
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
}}>
|
||
<Icon name="refresh" size={12} /> 手動更新
|
||
</button>
|
||
{activeId === 'flash' && (
|
||
<button style={{
|
||
padding: '8px 14px', fontSize: 12, fontWeight: 600,
|
||
background: 'var(--momo-ink)', color: '#faf7f0',
|
||
border: 'none', borderRadius: 4,
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
}}>
|
||
<Icon name="bell" size={12} /> 發送通知
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ===== Hero V3:反差(暖墨底 + accent 點綴,呼應 dashboard 反白 KPI) =====
|
||
const HeroInverse = ({ c, activeId }) => {
|
||
const accent = CAMPAIGN_ACCENTS[activeId] || 'var(--momo-accent)';
|
||
return (
|
||
<div style={{
|
||
position: 'relative',
|
||
background: 'var(--momo-ink)',
|
||
borderRadius: 8, padding: '28px 32px',
|
||
overflow: 'hidden',
|
||
color: '#faf7f0',
|
||
}}>
|
||
{/* 點陣裝飾 */}
|
||
<div style={{
|
||
position: 'absolute', inset: 0,
|
||
backgroundImage: 'radial-gradient(circle, rgba(250,247,240,0.06) 1px, transparent 1px)',
|
||
backgroundSize: '12px 12px', pointerEvents: 'none',
|
||
}} />
|
||
|
||
<div style={{ position: 'relative' }}>
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 14 }}>
|
||
<span className="momo-mono" style={{
|
||
fontSize: 10, fontWeight: 700, letterSpacing: '0.12em',
|
||
color: accent, textTransform: 'uppercase',
|
||
}}>● CAMPAIGN</span>
|
||
<span className="momo-mono" style={{ fontSize: 11, color: 'rgba(250,247,240,0.5)' }}>
|
||
ID · {activeId.toUpperCase()}
|
||
</span>
|
||
</div>
|
||
|
||
<h1 style={{
|
||
margin: 0, fontSize: 40, fontWeight: 800,
|
||
lineHeight: 1.05, letterSpacing: '-0.025em',
|
||
color: '#faf7f0', marginBottom: 20,
|
||
}}>{c.name}</h1>
|
||
|
||
<div style={{ display: 'flex', gap: 32, flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||
<div>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'rgba(250,247,240,0.5)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
活動時段
|
||
</div>
|
||
<div className="momo-mono" style={{ fontSize: 13, fontWeight: 600, color: '#faf7f0' }}>{c.time}</div>
|
||
</div>
|
||
<div>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'rgba(250,247,240,0.5)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
最後更新
|
||
</div>
|
||
<div className="momo-mono" style={{ fontSize: 13, fontWeight: 600, color: '#faf7f0' }}>{c.lastUpdate}</div>
|
||
</div>
|
||
<div style={{ marginLeft: 'auto', textAlign: 'right' }}>
|
||
<div className="momo-mono" style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', color: 'rgba(250,247,240,0.5)', textTransform: 'uppercase', marginBottom: 4 }}>
|
||
商品總數
|
||
</div>
|
||
<div className="momo-mono" style={{ fontSize: 36, fontWeight: 800, color: '#faf7f0', letterSpacing: '-0.03em', lineHeight: 1 }}>
|
||
{c.total.toLocaleString()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 8, marginTop: 20 }}>
|
||
<button style={{
|
||
padding: '8px 14px', fontSize: 12, fontWeight: 600,
|
||
background: 'rgba(250,247,240,0.1)', color: '#faf7f0',
|
||
border: '1px solid rgba(250,247,240,0.2)', borderRadius: 4,
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
}}>
|
||
<Icon name="refresh" size={12} /> 手動更新
|
||
</button>
|
||
{activeId === 'flash' && (
|
||
<button style={{
|
||
padding: '8px 14px', fontSize: 12, fontWeight: 600,
|
||
background: accent, color: '#faf7f0',
|
||
border: 'none', borderRadius: 4,
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
}}>
|
||
<Icon name="bell" size={12} /> 發送通知
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ===== KPI 4 顆(無框、扁平、跟 dashboard 監控總覽同款) =====
|
||
const CampaignKPIs = ({ stats, schedule }) => {
|
||
const items = [
|
||
{ label: '上架商品', value: stats.listed, sub: '本期活動' },
|
||
{ label: '新品', value: stats.new, sub: `+${stats.new} 件` },
|
||
{ label: '漲價', value: stats.up, sub: stats.up > 0 ? '注意異動' : '—', tone: 'danger' },
|
||
{ label: '降價', value: stats.down, sub: stats.down > 0 ? '優惠加深' : '—', tone: 'success' },
|
||
];
|
||
const toneColor = (t) => t === 'danger' ? 'var(--momo-danger)' : t === 'success' ? 'var(--momo-success)' : 'var(--momo-text-primary)';
|
||
|
||
return (
|
||
<div className="camp-kpis" style={{
|
||
display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)',
|
||
background: 'var(--momo-bg-surface)',
|
||
border: '1px solid var(--momo-border-light)',
|
||
borderRadius: 8, overflow: 'hidden',
|
||
}}>
|
||
{items.map((it, i) => (
|
||
<div key={i} style={{
|
||
padding: '20px 24px',
|
||
borderRight: i < items.length - 1 ? '1px solid var(--momo-border-light)' : 'none',
|
||
}}>
|
||
<div className="momo-mono" style={{
|
||
fontSize: 10, fontWeight: 700, letterSpacing: '0.1em',
|
||
color: 'var(--momo-text-tertiary)',
|
||
textTransform: 'uppercase', marginBottom: 10,
|
||
}}>{it.label}</div>
|
||
<div className="momo-mono camp-kpi-num" style={{
|
||
fontSize: 36, fontWeight: 700,
|
||
color: toneColor(it.tone),
|
||
letterSpacing: '-0.03em', lineHeight: 1, marginBottom: 8,
|
||
}}>{it.value.toLocaleString()}</div>
|
||
<div className="momo-mono" style={{ fontSize: 11, color: 'var(--momo-text-secondary)' }}>
|
||
{it.sub}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ===== 時段時間軸(限時搶購用) — 去花俏化 =====
|
||
const TimeSlotTimeline = ({ slots }) => {
|
||
const max = Math.max(...slots.map(s => s.count), 1);
|
||
return (
|
||
<div style={{
|
||
padding: '20px 24px', background: 'var(--momo-bg-surface)',
|
||
border: '1px solid var(--momo-border-light)', borderRadius: 8,
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 18 }}>
|
||
<div className="momo-mono" style={{
|
||
fontSize: 10, fontWeight: 700, letterSpacing: '0.1em',
|
||
color: 'var(--momo-text-tertiary)', textTransform: 'uppercase',
|
||
}}>時段排程</div>
|
||
<span className="momo-mono" style={{ fontSize: 11, color: 'var(--momo-text-tertiary)' }}>
|
||
24H · {slots.reduce((a, b) => a + b.count, 0)} 件
|
||
</span>
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${slots.length}, 1fr)`, gap: 8, alignItems: 'end' }}>
|
||
{slots.map((s, i) => {
|
||
const heightPct = max > 0 ? (s.count / max) * 100 : 0;
|
||
return (
|
||
<div key={i} style={{
|
||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
|
||
cursor: 'pointer',
|
||
}}>
|
||
<div style={{ height: 80, width: '100%', display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
|
||
<div style={{
|
||
width: '100%', maxWidth: 36,
|
||
height: `${Math.max(heightPct, 4)}%`,
|
||
background: s.active ? 'var(--momo-accent)' : 'var(--momo-text-primary)',
|
||
opacity: s.active ? 1 : (s.count === 0 ? 0.12 : 0.6),
|
||
borderRadius: '2px 2px 0 0',
|
||
transition: 'all 0.2s',
|
||
position: 'relative',
|
||
}}>
|
||
{s.active && (
|
||
<div className="momo-mono" style={{
|
||
position: 'absolute', top: -22, left: '50%', transform: 'translateX(-50%)',
|
||
padding: '2px 6px', borderRadius: 2,
|
||
background: 'var(--momo-accent)', color: '#faf7f0',
|
||
fontSize: 9, fontWeight: 700,
|
||
letterSpacing: '0.08em',
|
||
whiteSpace: 'nowrap',
|
||
}}>NOW</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="momo-mono" style={{
|
||
fontSize: 11, fontWeight: 700, color: 'var(--momo-text-primary)',
|
||
}}>{s.time}</div>
|
||
<div className="momo-mono" style={{
|
||
fontSize: 10, color: s.active ? 'var(--momo-accent)' : 'var(--momo-text-tertiary)',
|
||
fontWeight: s.active ? 700 : 500,
|
||
}}>{s.count}</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ===== 分類 chip 列 — 對齊 dashboard tab 風格 =====
|
||
const CategoryChips = ({ cats, activeIdx, setActive }) => (
|
||
<div style={{
|
||
padding: '14px 20px', background: 'var(--momo-bg-surface)',
|
||
border: '1px solid var(--momo-border-light)', borderRadius: 8,
|
||
}}>
|
||
<div className="momo-mono" style={{
|
||
fontSize: 10, fontWeight: 700, letterSpacing: '0.1em',
|
||
color: 'var(--momo-text-tertiary)', textTransform: 'uppercase', marginBottom: 10,
|
||
}}>分類 / {cats.length}</div>
|
||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||
{cats.map((cat, i) => {
|
||
const isActive = activeIdx == null ? cat.active : i === activeIdx;
|
||
return (
|
||
<button key={i} onClick={() => setActive && setActive(i)} style={{
|
||
padding: '6px 12px',
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
fontSize: 12, fontWeight: 500,
|
||
border: `1px solid ${isActive ? 'var(--momo-ink)' : 'var(--momo-border-light)'}`,
|
||
background: isActive ? 'var(--momo-ink)' : 'var(--momo-bg-paper)',
|
||
color: isActive ? '#faf7f0' : 'var(--momo-text-secondary)',
|
||
borderRadius: 2,
|
||
transition: 'var(--momo-transition-base)',
|
||
whiteSpace: 'nowrap',
|
||
}}>
|
||
<span>{cat.name}</span>
|
||
<span className="momo-mono" style={{
|
||
fontSize: 10, fontWeight: 700,
|
||
padding: '1px 5px', borderRadius: 2,
|
||
background: isActive ? 'rgba(250,247,240,0.18)' : 'var(--momo-bg-subtle)',
|
||
color: isActive ? 'rgba(250,247,240,0.9)' : 'var(--momo-text-tertiary)',
|
||
}}>{cat.count}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// ===== 商品列表 row — 去掉花俏火焰膠囊,改 mono 風格 =====
|
||
const Tag = ({ children, tone }) => {
|
||
const tones = {
|
||
info: { bg: 'var(--momo-info-bg)', fg: 'var(--momo-info-text)' },
|
||
danger: { bg: 'var(--momo-danger-bg)', fg: 'var(--momo-danger-text)' },
|
||
success: { bg: 'var(--momo-success-bg)', fg: 'var(--momo-success-text)' },
|
||
warning: { bg: 'var(--momo-warning-bg)', fg: 'var(--momo-warning-text)' },
|
||
};
|
||
const t = tones[tone] || tones.info;
|
||
return (
|
||
<span className="momo-mono" style={{
|
||
display: 'inline-block', padding: '1px 6px',
|
||
fontSize: 10, fontWeight: 700,
|
||
background: t.bg, color: t.fg, borderRadius: 2,
|
||
letterSpacing: '0.04em',
|
||
}}>{children}</span>
|
||
);
|
||
};
|
||
|
||
const CampaignProductRow = ({ p, isFlash, idx }) => (
|
||
<tr style={{
|
||
borderTop: idx === 0 ? 'none' : '1px solid var(--momo-border-light)',
|
||
cursor: 'pointer', transition: 'var(--momo-transition-base)',
|
||
}}
|
||
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-paper)'}
|
||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||
<td className="camp-td-cat" style={{ padding: '14px 16px' }}>
|
||
<span className="momo-mono" style={{
|
||
display: 'inline-block', padding: '2px 8px', fontSize: 11,
|
||
background: 'var(--momo-bg-subtle)', color: 'var(--momo-text-secondary)',
|
||
borderRadius: 2, whiteSpace: 'nowrap',
|
||
}}>{p.cat}</span>
|
||
</td>
|
||
<td style={{ padding: '14px 16px' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||
<div style={{
|
||
width: 40, height: 40, borderRadius: 4,
|
||
background: 'var(--momo-bg-subtle)', border: '1px solid var(--momo-border-light)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: 20, flexShrink: 0,
|
||
}}>{p.emoji}</div>
|
||
<div style={{ minWidth: 0, flex: 1 }}>
|
||
<div style={{
|
||
fontSize: 13, fontWeight: 500, color: 'var(--momo-text-primary)',
|
||
lineHeight: 1.4, marginBottom: 3,
|
||
display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
||
}}>{p.name}</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'var(--momo-text-tertiary)' }}>
|
||
<span className="momo-mono">#{p.id}</span>
|
||
{p.new && <Tag tone="info">NEW</Tag>}
|
||
{p.delisted && <Tag tone="warning">下架</Tag>}
|
||
{p.up && <Tag tone="danger">漲價</Tag>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td style={{ padding: '14px 16px', textAlign: 'right' }}>
|
||
{p.up && p.change ? (
|
||
<div>
|
||
<div className="momo-mono" style={{ fontSize: 11, color: 'var(--momo-danger)', fontWeight: 700, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 4 }}>
|
||
<span style={{ fontSize: 9 }}>▲</span> +${p.change} ({p.changePct}%)
|
||
</div>
|
||
<div className="momo-mono" style={{ marginTop: 2, display: 'flex', justifyContent: 'flex-end', alignItems: 'baseline', gap: 6 }}>
|
||
<span style={{ fontSize: 11, color: 'var(--momo-text-tertiary)', textDecoration: 'line-through' }}>
|
||
${p.oldPrice.toLocaleString()}
|
||
</span>
|
||
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--momo-text-primary)' }}>
|
||
${p.price.toLocaleString()}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<span className="momo-mono" style={{ fontSize: 14, fontWeight: 700, color: 'var(--momo-text-primary)' }}>
|
||
${p.price.toLocaleString()}
|
||
</span>
|
||
)}
|
||
</td>
|
||
<td style={{ padding: '14px 16px', textAlign: 'right' }}>
|
||
{isFlash && p.limit ? (
|
||
<span className="momo-mono" style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
padding: '4px 10px',
|
||
background: 'var(--momo-accent-soft)',
|
||
color: 'var(--momo-accent-700)',
|
||
border: '1px solid var(--momo-accent-200)',
|
||
borderRadius: 2, fontSize: 11, fontWeight: 700,
|
||
letterSpacing: '0.04em',
|
||
}}>
|
||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--momo-accent)', animation: 'momo-pulse-dot 1.4s ease-in-out infinite' }} />
|
||
{p.limit} 組
|
||
</span>
|
||
) : p.off ? (
|
||
<Tag tone="danger">{p.off}</Tag>
|
||
) : (
|
||
<span className="momo-mono" style={{
|
||
display: 'inline-flex', alignItems: 'center',
|
||
padding: '3px 8px',
|
||
background: 'var(--momo-bg-subtle)', color: 'var(--momo-text-secondary)',
|
||
borderRadius: 2, fontSize: 11, fontWeight: 500,
|
||
}}>{p.status || '活動中'}</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
|
||
// ===== 商品列表(黑色表頭 mono — 對齊 dashboard) =====
|
||
const CampaignProductTable = ({ products, total, isFlash }) => (
|
||
<div style={{
|
||
background: 'var(--momo-bg-surface)',
|
||
border: '1px solid var(--momo-border-light)', borderRadius: 8, overflow: 'hidden',
|
||
}}>
|
||
<div style={{
|
||
padding: '14px 20px', borderBottom: '1px solid var(--momo-border-light)',
|
||
display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap',
|
||
background: 'var(--momo-bg-paper)',
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
||
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--momo-text-primary)' }}>商品列表</span>
|
||
<span className="momo-mono" style={{ fontSize: 11, color: 'var(--momo-text-tertiary)' }}>
|
||
{total.toLocaleString()} 筆
|
||
</span>
|
||
</div>
|
||
<div style={{ flex: 1 }} />
|
||
<Button variant="secondary" size="sm" icon="download">匯出</Button>
|
||
</div>
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table className="camp-table" style={{ width: '100%', borderCollapse: 'collapse', fontSize: 'var(--momo-font-size-sm)' }}>
|
||
<thead>
|
||
<tr style={{ background: 'var(--momo-ink)', color: '#faf7f0' }}>
|
||
{[
|
||
{ label: '分類', sub: 'CATEGORY', w: 140 },
|
||
{ label: '商品資訊', sub: 'PRODUCT' },
|
||
{ label: '價格', sub: 'PRICE', w: 160, align: 'right' },
|
||
{ label: isFlash ? '倒數組數' : '狀態', sub: isFlash ? 'LIMIT' : 'STATUS', w: 130, align: 'right' },
|
||
].map((h, i) => (
|
||
<th key={i} className={i === 0 ? 'camp-th-cat' : ''} style={{
|
||
padding: '12px 16px', textAlign: h.align || 'left', width: h.w,
|
||
fontSize: 11, fontWeight: 700, whiteSpace: 'nowrap',
|
||
fontFamily: 'var(--momo-font-family-mono)',
|
||
letterSpacing: '0.08em',
|
||
}}>
|
||
<div style={{ fontSize: 9, opacity: 0.5, marginBottom: 1 }}>{h.sub}</div>
|
||
<div style={{ fontFamily: 'var(--momo-font-family-base)', letterSpacing: 0, fontWeight: 700, fontSize: 12 }}>{h.label}</div>
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{products.map((p, i) => <CampaignProductRow key={p.id} p={p} isFlash={isFlash} idx={i} />)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// ===== Page =====
|
||
const CampaignsPage = ({ density = 'comfortable', heroVariant = 'simple' }) => {
|
||
const D = EWOOOC_DATA.campaigns;
|
||
const [active, setActive] = React.useState('flash');
|
||
const [catIdx, setCatIdx] = React.useState(null);
|
||
const c = D[active];
|
||
|
||
React.useEffect(() => { setCatIdx(null); }, [active]);
|
||
|
||
const HeroComp = heroVariant === 'dotted' ? HeroDotted
|
||
: heroVariant === 'inverse' ? HeroInverse
|
||
: HeroSimple;
|
||
|
||
return (
|
||
<div className="camp-page" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
<CampaignSwitcher active={active} setActive={setActive} campaigns={D} />
|
||
|
||
{/* 01 - Hero */}
|
||
<div>
|
||
<CampSectionLabel num="01" sub="活動主資訊">活動概覽</CampSectionLabel>
|
||
<HeroComp c={c} activeId={active} />
|
||
</div>
|
||
|
||
{/* 02 - KPIs */}
|
||
<div>
|
||
<CampSectionLabel num="02" sub={`排程 · ${c.schedule.lastRun}`}>活動數據</CampSectionLabel>
|
||
<CampaignKPIs stats={c.stats} schedule={c.schedule} />
|
||
</div>
|
||
|
||
{/* 03 - 時段排程(限時搶購) */}
|
||
{c.timeSlots && (
|
||
<div>
|
||
<CampSectionLabel num="03" sub={`24H · ${c.timeSlots.reduce((a,b)=>a+b.count,0)} 件`}>時段排程</CampSectionLabel>
|
||
<TimeSlotTimeline slots={c.timeSlots} />
|
||
</div>
|
||
)}
|
||
|
||
{/* 04 - 分類 */}
|
||
{c.categories && (
|
||
<div>
|
||
<CampSectionLabel num={c.timeSlots ? '04' : '03'} sub={`${c.categories.length} 個分類`}>分類篩選</CampSectionLabel>
|
||
<CategoryChips cats={c.categories} activeIdx={catIdx} setActive={setCatIdx} />
|
||
</div>
|
||
)}
|
||
|
||
{/* 05 - 商品列表 */}
|
||
<div>
|
||
<CampSectionLabel
|
||
num={c.timeSlots && c.categories ? '05' : c.categories ? '04' : '03'}
|
||
sub={`${c.products.length} / ${c.total.toLocaleString()} 筆`}
|
||
>商品列表</CampSectionLabel>
|
||
<CampaignProductTable products={c.products} total={c.total} isFlash={active === 'flash'} />
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
window.CampaignsPage = CampaignsPage;
|