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

328 lines
15 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 - 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 });