328 lines
15 KiB
JavaScript
328 lines
15 KiB
JavaScript
// MOMO Pro - Modal & 命令面板
|
||
|
||
// ===== 通用 Modal 容器 =====
|
||
const Modal = ({ open, onClose, title, children, footer, size = 'md' }) => {
|
||
if (!open) return null;
|
||
const widths = { sm: 480, md: 640, lg: 880 };
|
||
return (
|
||
<div style={{
|
||
position: 'absolute', inset: 0,
|
||
background: 'var(--momo-bg-overlay)',
|
||
zIndex: 'var(--momo-z-modal-backdrop)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
padding: 24,
|
||
animation: 'momo-fade-in 0.15s ease-out',
|
||
}} onClick={onClose}>
|
||
<div onClick={e => e.stopPropagation()} style={{
|
||
background: 'var(--momo-bg-surface)',
|
||
borderRadius: 'var(--momo-radius-lg)',
|
||
boxShadow: 'var(--momo-shadow-lg)',
|
||
width: '100%', maxWidth: widths[size],
|
||
maxHeight: '90%',
|
||
display: 'flex', flexDirection: 'column',
|
||
overflow: 'hidden',
|
||
animation: 'momo-slide-up 0.2s ease-out',
|
||
}}>
|
||
<div style={{
|
||
padding: '18px 24px',
|
||
borderBottom: '1px solid var(--momo-border-light)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||
}}>
|
||
<h3 style={{ margin: 0, fontSize: 16, fontWeight: 700, color: 'var(--momo-text-primary)' }}>{title}</h3>
|
||
<button onClick={onClose} style={{
|
||
width: 32, height: 32, borderRadius: 'var(--momo-radius-md)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
color: 'var(--momo-text-secondary)',
|
||
}}
|
||
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-bg-subtle)'}
|
||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||
<Icon name="x" size={18} />
|
||
</button>
|
||
</div>
|
||
<div className="momo-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 24px' }}>
|
||
{children}
|
||
</div>
|
||
{footer && (
|
||
<div style={{
|
||
padding: '14px 24px',
|
||
borderTop: '1px solid var(--momo-border-light)',
|
||
display: 'flex', justifyContent: 'flex-end', gap: 8,
|
||
background: 'var(--momo-bg-subtle)',
|
||
}}>{footer}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ===== 編輯商品 Modal =====
|
||
const ProductEditModal = ({ open, product, onClose, buttonStyle = 'gradient' }) => {
|
||
if (!product) return null;
|
||
const Field = ({ label, children, hint }) => (
|
||
<div style={{ marginBottom: 16 }}>
|
||
<label style={{
|
||
display: 'block', fontSize: 12, fontWeight: 600,
|
||
color: 'var(--momo-text-primary)', marginBottom: 6,
|
||
}}>{label}</label>
|
||
{children}
|
||
{hint && <div style={{ fontSize: 11, color: 'var(--momo-text-tertiary)', marginTop: 4 }}>{hint}</div>}
|
||
</div>
|
||
);
|
||
const fieldStyle = {
|
||
width: '100%', padding: '8px 12px',
|
||
border: '1px solid var(--momo-border)',
|
||
borderRadius: 'var(--momo-radius-md)',
|
||
fontSize: 13, outline: 'none',
|
||
background: 'var(--momo-bg-surface)',
|
||
transition: 'var(--momo-transition-base)',
|
||
};
|
||
|
||
return (
|
||
<Modal open={open} onClose={onClose} size="lg" title={`編輯商品 · ${product.id}`}
|
||
footer={
|
||
<>
|
||
<Button variant="secondary" onClick={onClose}>取消</Button>
|
||
<Button variant="ghost-hover">儲存草稿</Button>
|
||
<Button variant={buttonStyle} icon="check" onClick={onClose}>儲存並上架</Button>
|
||
</>
|
||
}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 220px', gap: 24 }}>
|
||
<div>
|
||
<Field label="商品名稱">
|
||
<input defaultValue={product.name} style={fieldStyle} />
|
||
</Field>
|
||
<Field label="商品描述" hint="支援 Markdown 格式">
|
||
<textarea rows="4" defaultValue="精選材質,舒適耐用,享受高品質體驗。30 天滿意保證,免費退換貨。" style={{ ...fieldStyle, resize: 'vertical', lineHeight: 1.6 }} />
|
||
</Field>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||
<Field label="售價(NT$)">
|
||
<input type="number" defaultValue={product.price} style={fieldStyle} />
|
||
</Field>
|
||
<Field label="原價(NT$)">
|
||
<input type="number" defaultValue={Math.round(product.price * 1.3)} style={fieldStyle} />
|
||
</Field>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||
<Field label="庫存量">
|
||
<input type="number" defaultValue={product.stock} style={fieldStyle} />
|
||
</Field>
|
||
<Field label="SKU">
|
||
<input defaultValue={product.sku} style={{ ...fieldStyle, fontFamily: 'var(--momo-font-family-mono)' }} />
|
||
</Field>
|
||
</div>
|
||
<Field label="商品分類">
|
||
<select defaultValue={product.category} style={fieldStyle}>
|
||
{[...new Set(MOMO_DATA.products.map(p => p.category))].map(c => (
|
||
<option key={c}>{c}</option>
|
||
))}
|
||
</select>
|
||
</Field>
|
||
<Field label="商品標籤">
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, padding: 8,
|
||
border: '1px solid var(--momo-border)', borderRadius: 'var(--momo-radius-md)',
|
||
background: 'var(--momo-bg-surface)' }}>
|
||
{['熱銷', '新品上市', '限時優惠'].map(t => (
|
||
<span key={t} style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||
padding: '3px 8px', fontSize: 12,
|
||
background: 'var(--momo-primary-100)', color: 'var(--momo-primary-700)',
|
||
borderRadius: 'var(--momo-radius-sm)', fontWeight: 500,
|
||
}}>
|
||
<Icon name="tag" size={11} />
|
||
{t}
|
||
<Icon name="x" size={11} />
|
||
</span>
|
||
))}
|
||
<input placeholder="新增標籤…" style={{ flex: 1, minWidth: 100, border: 'none', outline: 'none', fontSize: 13 }} />
|
||
</div>
|
||
</Field>
|
||
</div>
|
||
|
||
<div>
|
||
<Field label="商品圖片">
|
||
<div style={{
|
||
aspectRatio: '1', borderRadius: 'var(--momo-radius-md)',
|
||
background: 'var(--momo-gradient-subtle)',
|
||
border: '1px solid var(--momo-border-light)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: 80, marginBottom: 8,
|
||
}}>{product.emoji}</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 6, marginBottom: 8 }}>
|
||
{[1,2,3].map(i => (
|
||
<div key={i} style={{
|
||
aspectRatio: '1', borderRadius: 'var(--momo-radius-sm)',
|
||
background: 'var(--momo-bg-subtle)',
|
||
border: '1px solid var(--momo-border-light)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
fontSize: 24, opacity: 0.4,
|
||
}}>{product.emoji}</div>
|
||
))}
|
||
<button style={{
|
||
aspectRatio: '1', borderRadius: 'var(--momo-radius-sm)',
|
||
background: 'var(--momo-bg-surface)',
|
||
border: '1.5px dashed var(--momo-border-dark)',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
color: 'var(--momo-text-tertiary)',
|
||
}}><Icon name="plus" size={16} /></button>
|
||
</div>
|
||
</Field>
|
||
<Field label="商品狀態">
|
||
<select defaultValue={product.status} style={fieldStyle}>
|
||
<option value="active">上架中</option>
|
||
<option value="draft">草稿</option>
|
||
<option value="soldout">下架</option>
|
||
</select>
|
||
</Field>
|
||
<div style={{
|
||
padding: 12,
|
||
background: 'var(--momo-info-bg)',
|
||
border: '1px solid var(--momo-info-border)',
|
||
borderRadius: 'var(--momo-radius-md)',
|
||
fontSize: 11, color: 'var(--momo-info-text)',
|
||
lineHeight: 1.5,
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontWeight: 600, marginBottom: 4 }}>
|
||
<Icon name="helpCircle" size={12} />
|
||
SEO 提示
|
||
</div>
|
||
標題建議 30 字內,描述含 3-5 個關鍵字,可提升搜尋曝光。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
// ===== 命令面板 =====
|
||
const CommandPalette = ({ open, onClose, onNavigate }) => {
|
||
const [query, setQuery] = React.useState('');
|
||
React.useEffect(() => { if (open) setQuery(''); }, [open]);
|
||
if (!open) return null;
|
||
|
||
const cmds = [
|
||
{ id: 'go-dashboard', label: '前往儀表板', kind: '導覽', icon: 'dashboard', action: () => { onNavigate('dashboard'); onClose(); } },
|
||
{ id: 'go-orders', label: '前往訂單管理', kind: '導覽', icon: 'orders', action: () => { onNavigate('orders'); onClose(); } },
|
||
{ id: 'go-products', label: '前往商品管理', kind: '導覽', icon: 'products', action: () => { onNavigate('products'); onClose(); } },
|
||
{ id: 'create-order', label: '建立新訂單', kind: '快速操作', icon: 'plus', shortcut: 'O' },
|
||
{ id: 'create-product', label: '新增商品', kind: '快速操作', icon: 'plus', shortcut: 'P' },
|
||
{ id: 'export-report', label: '匯出本月報表', kind: '快速操作', icon: 'download' },
|
||
{ id: 'recent-1', label: 'MM-202604-08742 · 陳俊宏', kind: '最近訂單', icon: 'orders' },
|
||
{ id: 'recent-2', label: 'P-2451 · 無線藍牙降噪耳機 Pro', kind: '最近商品', icon: 'package' },
|
||
];
|
||
const filtered = query
|
||
? cmds.filter(c => c.label.toLowerCase().includes(query.toLowerCase()))
|
||
: cmds;
|
||
|
||
// 依分類分群
|
||
const groups = {};
|
||
filtered.forEach(c => { (groups[c.kind] = groups[c.kind] || []).push(c); });
|
||
|
||
return (
|
||
<div style={{
|
||
position: 'absolute', inset: 0,
|
||
background: 'rgba(20,25,40,0.45)',
|
||
backdropFilter: 'blur(4px)',
|
||
zIndex: 'var(--momo-z-modal)',
|
||
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
||
paddingTop: 100,
|
||
}} onClick={onClose}>
|
||
<div onClick={e => e.stopPropagation()} style={{
|
||
width: 560, maxWidth: '90%',
|
||
background: 'var(--momo-bg-surface)',
|
||
borderRadius: 'var(--momo-radius-lg)',
|
||
boxShadow: 'var(--momo-shadow-lg)',
|
||
overflow: 'hidden',
|
||
animation: 'momo-slide-up 0.18s ease-out',
|
||
}}>
|
||
<div style={{
|
||
display: 'flex', alignItems: 'center', gap: 12,
|
||
padding: '14px 18px',
|
||
borderBottom: '1px solid var(--momo-border-light)',
|
||
}}>
|
||
<Icon name="search" size={18} color="var(--momo-text-secondary)" />
|
||
<input autoFocus value={query} onChange={e => setQuery(e.target.value)}
|
||
placeholder="搜尋指令、訂單、商品、會員…"
|
||
style={{
|
||
flex: 1, border: 'none', outline: 'none',
|
||
fontSize: 15, color: 'var(--momo-text-primary)',
|
||
background: 'transparent',
|
||
}} />
|
||
<kbd style={{
|
||
fontSize: 10, fontWeight: 600,
|
||
padding: '3px 7px',
|
||
background: 'var(--momo-bg-subtle)',
|
||
border: '1px solid var(--momo-border)',
|
||
borderRadius: 4,
|
||
color: 'var(--momo-text-secondary)',
|
||
fontFamily: 'var(--momo-font-family-mono)',
|
||
}}>ESC</kbd>
|
||
</div>
|
||
<div className="momo-scroll" style={{ maxHeight: 400, overflowY: 'auto', padding: 8 }}>
|
||
{Object.keys(groups).length === 0 && (
|
||
<div style={{ padding: 32, textAlign: 'center', color: 'var(--momo-text-tertiary)', fontSize: 13 }}>
|
||
找不到相符的結果
|
||
</div>
|
||
)}
|
||
{Object.entries(groups).map(([kind, items]) => (
|
||
<div key={kind} style={{ marginBottom: 4 }}>
|
||
<div style={{
|
||
padding: '6px 10px',
|
||
fontSize: 10, fontWeight: 700,
|
||
color: 'var(--momo-text-tertiary)',
|
||
letterSpacing: '0.06em', textTransform: 'uppercase',
|
||
}}>{kind}</div>
|
||
{items.map((c, i) => (
|
||
<button key={c.id} onClick={c.action}
|
||
style={{
|
||
width: '100%',
|
||
display: 'flex', alignItems: 'center', gap: 12,
|
||
padding: '8px 10px',
|
||
borderRadius: 'var(--momo-radius-md)',
|
||
fontSize: 13,
|
||
color: 'var(--momo-text-primary)',
|
||
transition: 'background var(--momo-duration-fast)',
|
||
background: i === 0 && kind === Object.keys(groups)[0] ? 'var(--momo-bg-subtle)' : 'transparent',
|
||
}}
|
||
onMouseEnter={e => e.currentTarget.style.background = 'var(--momo-primary-50)'}
|
||
onMouseLeave={e => e.currentTarget.style.background = i === 0 && kind === Object.keys(groups)[0] ? 'var(--momo-bg-subtle)' : 'transparent'}>
|
||
<Icon name={c.icon} size={16} color="var(--momo-text-secondary)" />
|
||
<span style={{ flex: 1, textAlign: 'left' }}>{c.label}</span>
|
||
{c.shortcut && (
|
||
<kbd style={{
|
||
fontSize: 10, fontWeight: 600,
|
||
padding: '2px 6px',
|
||
background: 'var(--momo-bg-subtle)',
|
||
border: '1px solid var(--momo-border-light)',
|
||
borderRadius: 4,
|
||
color: 'var(--momo-text-secondary)',
|
||
fontFamily: 'var(--momo-font-family-mono)',
|
||
}}>⌘{c.shortcut}</kbd>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div style={{
|
||
padding: '8px 14px',
|
||
borderTop: '1px solid var(--momo-border-light)',
|
||
background: 'var(--momo-bg-subtle)',
|
||
display: 'flex', alignItems: 'center', gap: 16,
|
||
fontSize: 11, color: 'var(--momo-text-tertiary)',
|
||
}}>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||
<kbd style={{ fontSize: 10, padding: '1px 5px', background: '#fff', border: '1px solid var(--momo-border)', borderRadius: 3, fontFamily: 'var(--momo-font-family-mono)' }}>↑↓</kbd>
|
||
選擇
|
||
</span>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||
<kbd style={{ fontSize: 10, padding: '1px 5px', background: '#fff', border: '1px solid var(--momo-border)', borderRadius: 3, fontFamily: 'var(--momo-font-family-mono)' }}>↵</kbd>
|
||
執行
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
Object.assign(window, { Modal, ProductEditModal, CommandPalette });
|