291 lines
14 KiB
JavaScript
291 lines
14 KiB
JavaScript
// MOMO Pro - 訂單管理頁
|
||
|
||
const OrdersPage = ({ density = 'comfortable', cardStyle = 'shadow', buttonStyle = 'gradient' }) => {
|
||
const [selected, setSelected] = React.useState(new Set());
|
||
const [statusFilter, setStatusFilter] = React.useState('all');
|
||
const [search, setSearch] = React.useState('');
|
||
|
||
const filtered = MOMO_DATA.orders.filter(o => {
|
||
if (statusFilter !== 'all' && o.status !== statusFilter) return false;
|
||
if (search && !o.id.toLowerCase().includes(search.toLowerCase()) && !o.customer.includes(search)) return false;
|
||
return true;
|
||
});
|
||
|
||
const toggleAll = () => {
|
||
if (selected.size === filtered.length) setSelected(new Set());
|
||
else setSelected(new Set(filtered.map(o => o.id)));
|
||
};
|
||
const toggleOne = (id) => {
|
||
const s = new Set(selected);
|
||
if (s.has(id)) s.delete(id); else s.add(id);
|
||
setSelected(s);
|
||
};
|
||
|
||
const tabs = [
|
||
{ id: 'all', label: '全部', count: MOMO_DATA.orders.length },
|
||
{ id: 'pending', label: '待處理', count: MOMO_DATA.orders.filter(o => o.status === 'pending').length },
|
||
{ id: 'processing', label: '處理中', count: MOMO_DATA.orders.filter(o => o.status === 'processing').length },
|
||
{ id: 'shipped', label: '已出貨', count: MOMO_DATA.orders.filter(o => o.status === 'shipped').length },
|
||
{ id: 'completed', label: '已完成', count: MOMO_DATA.orders.filter(o => o.status === 'completed').length },
|
||
{ id: 'cancelled', label: '已取消', count: MOMO_DATA.orders.filter(o => o.status === 'cancelled').length },
|
||
];
|
||
|
||
const rowPad = density === 'compact' ? '8px 16px' : '14px 16px';
|
||
const headPad = density === 'compact' ? '8px 16px' : '12px 16px';
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
title="訂單管理"
|
||
subtitle={`共 ${MOMO_DATA.orders.length} 筆訂單 · 24 筆待處理`}
|
||
breadcrumbs={['首頁', '訂單', '訂單列表']}
|
||
actions={
|
||
<>
|
||
<Button variant="secondary" size="md" icon="download">匯出 CSV</Button>
|
||
<Button variant={buttonStyle} size="md" icon="plus">建立訂單</Button>
|
||
</>
|
||
}
|
||
/>
|
||
|
||
{/* Tabs */}
|
||
<div style={{
|
||
display: 'flex', alignItems: 'center', gap: 4,
|
||
borderBottom: '1px solid var(--momo-border-light)',
|
||
marginBottom: 'var(--momo-space-4)',
|
||
overflowX: 'auto',
|
||
}} className="momo-scroll">
|
||
{tabs.map(t => (
|
||
<button key={t.id} onClick={() => { setStatusFilter(t.id); setSelected(new Set()); }}
|
||
style={{
|
||
padding: '10px 14px',
|
||
fontSize: 'var(--momo-font-size-sm)',
|
||
fontWeight: 'var(--momo-font-weight-medium)',
|
||
color: statusFilter === t.id ? 'var(--momo-primary-700)' : 'var(--momo-text-secondary)',
|
||
borderBottom: `2px solid ${statusFilter === t.id ? 'var(--momo-primary)' : 'transparent'}`,
|
||
marginBottom: -1,
|
||
transition: 'var(--momo-transition-base)',
|
||
display: 'flex', alignItems: 'center', gap: 6,
|
||
whiteSpace: 'nowrap',
|
||
}}>
|
||
{t.label}
|
||
<span style={{
|
||
padding: '1px 7px',
|
||
fontSize: 10, fontWeight: 700,
|
||
borderRadius: 'var(--momo-radius-pill)',
|
||
background: statusFilter === t.id ? 'var(--momo-primary-100)' : 'var(--momo-bg-muted)',
|
||
color: statusFilter === t.id ? 'var(--momo-primary-700)' : 'var(--momo-text-tertiary)',
|
||
}}>{t.count}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<Card cardStyle={cardStyle} padding={false}>
|
||
{/* Toolbar */}
|
||
<div style={{
|
||
padding: '14px 20px',
|
||
display: 'flex', alignItems: 'center', gap: 10,
|
||
borderBottom: '1px solid var(--momo-border-light)',
|
||
}}>
|
||
<div style={{ flex: 1, maxWidth: 320 }}>
|
||
<Input icon="search" placeholder="搜尋訂單編號、會員姓名…" value={search} onChange={e => setSearch(e.target.value)} size="sm" />
|
||
</div>
|
||
<Button variant="secondary" size="sm" icon="filter">進階篩選</Button>
|
||
<Button variant="secondary" size="sm" icon="calendar">日期區間</Button>
|
||
<div style={{ flex: 1 }} />
|
||
<Button variant="ghost" size="sm" icon="refresh" />
|
||
<Button variant="ghost" size="sm" icon="settings" />
|
||
</div>
|
||
|
||
{/* Bulk action bar */}
|
||
{selected.size > 0 && (
|
||
<div style={{
|
||
padding: '10px 20px',
|
||
background: 'var(--momo-primary-50)',
|
||
borderBottom: '1px solid var(--momo-primary-100)',
|
||
display: 'flex', alignItems: 'center', gap: 12,
|
||
fontSize: 'var(--momo-font-size-sm)',
|
||
animation: 'momo-fade-in 0.2s var(--momo-ease-out)',
|
||
}}>
|
||
<span style={{ color: 'var(--momo-primary-700)', fontWeight: 600 }}>
|
||
已選取 {selected.size} 筆訂單
|
||
</span>
|
||
<div style={{ width: 1, height: 16, background: 'var(--momo-primary-200)' }} />
|
||
<button style={{ fontSize: 13, color: 'var(--momo-primary-700)', fontWeight: 500 }}>標記為已處理</button>
|
||
<button style={{ fontSize: 13, color: 'var(--momo-primary-700)', fontWeight: 500 }}>列印出貨單</button>
|
||
<button style={{ fontSize: 13, color: 'var(--momo-primary-700)', fontWeight: 500 }}>批次匯出</button>
|
||
<div style={{ flex: 1 }} />
|
||
<button onClick={() => setSelected(new Set())}
|
||
style={{ fontSize: 13, color: 'var(--momo-text-secondary)' }}>取消選取</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Table */}
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 'var(--momo-font-size-sm)' }}>
|
||
<thead>
|
||
<tr style={{ background: 'var(--momo-bg-subtle)' }}>
|
||
<th style={{ padding: headPad, width: 40 }}>
|
||
<Checkbox
|
||
checked={selected.size === filtered.length && filtered.length > 0}
|
||
indeterminate={selected.size > 0 && selected.size < filtered.length}
|
||
onChange={toggleAll}
|
||
/>
|
||
</th>
|
||
{[
|
||
{ label: '訂單編號', sort: true },
|
||
{ label: '會員', sort: false },
|
||
{ label: '金額', sort: true, align: 'right' },
|
||
{ label: '商品', sort: false, align: 'right' },
|
||
{ label: '狀態', sort: false },
|
||
{ label: '付款', sort: false },
|
||
{ label: '通路', sort: false },
|
||
{ label: '建立時間', sort: true },
|
||
{ label: '', sort: false, align: 'right' },
|
||
].map((h, i) => (
|
||
<th key={i} style={{
|
||
padding: headPad,
|
||
textAlign: h.align || 'left',
|
||
fontSize: 11, fontWeight: 600,
|
||
color: 'var(--momo-text-secondary)',
|
||
letterSpacing: '0.04em',
|
||
textTransform: 'uppercase',
|
||
whiteSpace: 'nowrap',
|
||
}}>
|
||
{h.sort ? (
|
||
<button style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color: 'inherit', fontSize: 'inherit', fontWeight: 'inherit', textTransform: 'inherit', letterSpacing: 'inherit' }}>
|
||
{h.label}
|
||
<Icon name="chevronDown" size={11} />
|
||
</button>
|
||
) : h.label}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filtered.map(order => {
|
||
const isSelected = selected.has(order.id);
|
||
return (
|
||
<tr key={order.id} style={{
|
||
borderTop: '1px solid var(--momo-border-light)',
|
||
background: isSelected ? 'var(--momo-primary-50)' : 'transparent',
|
||
transition: 'background var(--momo-duration-fast)',
|
||
}}
|
||
onMouseEnter={e => !isSelected && (e.currentTarget.style.background = 'var(--momo-bg-subtle)')}
|
||
onMouseLeave={e => !isSelected && (e.currentTarget.style.background = 'transparent')}
|
||
>
|
||
<td style={{ padding: rowPad }}>
|
||
<Checkbox checked={isSelected} onChange={() => toggleOne(order.id)} />
|
||
</td>
|
||
<td style={{ padding: rowPad }}>
|
||
<a href="#" style={{
|
||
color: 'var(--momo-text-link)',
|
||
fontFamily: 'var(--momo-font-family-mono)',
|
||
fontSize: 12, fontWeight: 600,
|
||
textDecoration: 'none',
|
||
}}>{order.id}</a>
|
||
</td>
|
||
<td style={{ padding: rowPad }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Avatar name={order.customer} size={28} />
|
||
<div style={{ lineHeight: 1.3 }}>
|
||
<div style={{ fontWeight: 500, color: 'var(--momo-text-primary)' }}>{order.customer}</div>
|
||
<div style={{ fontSize: 11, color: 'var(--momo-text-tertiary)' }}>{order.email}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td style={{ padding: rowPad, textAlign: 'right', fontFamily: 'var(--momo-font-family-mono)', fontWeight: 600, color: 'var(--momo-text-primary)' }}>
|
||
NT$ {order.total.toLocaleString()}
|
||
</td>
|
||
<td style={{ padding: rowPad, textAlign: 'right', color: 'var(--momo-text-secondary)' }}>
|
||
{order.items} 項
|
||
</td>
|
||
<td style={{ padding: rowPad }}>
|
||
<Badge tone={STATUS_MAP[order.status].tone} dot>{STATUS_MAP[order.status].label}</Badge>
|
||
</td>
|
||
<td style={{ padding: rowPad }}>
|
||
<Badge tone={STATUS_MAP[order.payment].tone}>{STATUS_MAP[order.payment].label}</Badge>
|
||
</td>
|
||
<td style={{ padding: rowPad }}>
|
||
<span style={{
|
||
fontSize: 11, color: 'var(--momo-text-secondary)',
|
||
background: 'var(--momo-bg-subtle)',
|
||
padding: '2px 8px',
|
||
borderRadius: 'var(--momo-radius-sm)',
|
||
}}>{order.channel}</span>
|
||
</td>
|
||
<td style={{ padding: rowPad, color: 'var(--momo-text-secondary)', fontSize: 12, fontFamily: 'var(--momo-font-family-mono)', whiteSpace: 'nowrap' }}>
|
||
{order.date}
|
||
</td>
|
||
<td style={{ padding: rowPad, textAlign: 'right' }}>
|
||
<div style={{ display: 'inline-flex', gap: 2 }}>
|
||
<button title="檢視" style={{
|
||
width: 28, height: 28, borderRadius: 6,
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
color: 'var(--momo-text-secondary)',
|
||
}}
|
||
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-muted)'}
|
||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||
<Icon name="eye" size={15} />
|
||
</button>
|
||
<button title="編輯" style={{
|
||
width: 28, height: 28, borderRadius: 6,
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
color: 'var(--momo-text-secondary)',
|
||
}}
|
||
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-muted)'}
|
||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||
<Icon name="edit" size={15} />
|
||
</button>
|
||
<button title="更多" style={{
|
||
width: 28, height: 28, borderRadius: 6,
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
color: 'var(--momo-text-secondary)',
|
||
}}
|
||
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-muted)'}
|
||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||
<Icon name="moreHorizontal" size={15} />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
<div style={{
|
||
padding: '14px 20px',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||
borderTop: '1px solid var(--momo-border-light)',
|
||
fontSize: 'var(--momo-font-size-sm)',
|
||
color: 'var(--momo-text-secondary)',
|
||
}}>
|
||
<div>顯示 1-{filtered.length} 筆,共 {filtered.length} 筆結果</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||
<Button variant="ghost" size="sm" icon="chevronLeft" disabled />
|
||
{[1, 2, 3, 4, 5].map(n => (
|
||
<button key={n} style={{
|
||
width: 30, height: 30,
|
||
borderRadius: 'var(--momo-radius-md)',
|
||
fontSize: 13, fontWeight: 500,
|
||
background: n === 1 ? 'var(--momo-primary-100)' : 'transparent',
|
||
color: n === 1 ? 'var(--momo-primary-700)' : 'var(--momo-text-secondary)',
|
||
}}>{n}</button>
|
||
))}
|
||
<span style={{ padding: '0 4px', color: 'var(--momo-text-tertiary)' }}>…</span>
|
||
<button style={{
|
||
width: 30, height: 30, borderRadius: 'var(--momo-radius-md)',
|
||
fontSize: 13, color: 'var(--momo-text-secondary)',
|
||
}}>32</button>
|
||
<Button variant="ghost" size="sm" icon="chevronRight" />
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
window.OrdersPage = OrdersPage;
|