Files
ewoooc/MOMO Pro/app/page-orders.jsx
2026-04-30 23:37:52 +08:00

291 lines
14 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.
// 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;