Files
ewoooc/MOMO Pro/app/page-campaigns.jsx

684 lines
29 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.
// 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;