import { useEffect, useState } from 'react'; import { Alert, Button, Form, Input, InputNumber, Space, Typography, message } from 'antd'; import { DownOutlined, UpOutlined } from '@ant-design/icons'; import type { StorePackageItemDto } from '@dukang/shared-types'; import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types'; import { request } from '../lib/api'; type PackageRow = StorePackageItemDto; function emptyRow(index = 0): PackageRow { return { name: '', price: '0', dishes: '', usableTime: '', otherNotes: '', sortOrder: index }; } export default function AdminStorePackagesSection({ storeId }: { storeId: string }) { const [items, setItems] = useState([emptyRow()]); const [collapsed, setCollapsed] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); useEffect(() => { setLoading(true); request<{ live: PackageRow[] }>(`/admin/stores/${storeId}/packages`) .then((data) => { setItems( data.live?.length ? data.live.map((p, i) => ({ ...p, price: String(p.price), sortOrder: i })) : [emptyRow()], ); }) .catch((e) => message.error(e instanceof Error ? e.message : '加载套餐失败')) .finally(() => setLoading(false)); }, [storeId]); function updateAt(index: number, patch: Partial) { setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item))); } function addRow() { if (items.length >= STORE_PACKAGE_MAX_COUNT) return; setItems((prev) => [...prev, emptyRow(prev.length)]); } function removeAt(index: number) { setItems((prev) => prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, sortOrder: i }))); setCollapsed((prev) => { const next: Record = {}; Object.entries(prev).forEach(([k, v]) => { const i = Number(k); if (i < index) next[i] = v; else if (i > index) next[i - 1] = v; }); return next; }); } function toggleCollapse(index: number) { setCollapsed((prev) => ({ ...prev, [index]: !prev[index] })); } async function save() { const filled = items .map((item, index) => ({ name: item.name.trim(), price: item.price.trim(), dishes: item.dishes.trim(), usableTime: item.usableTime?.trim() || null, otherNotes: item.otherNotes?.trim() || null, sortOrder: index, })) .filter((item) => item.name || item.dishes || item.price); for (let i = 0; i < filled.length; i++) { const item = filled[i]; if (!item.name) { message.warning(`第 ${i + 1} 条套餐名称不能为空`); return; } if (!item.dishes) { message.warning(`第 ${i + 1} 条套餐菜品不能为空`); return; } const price = Number(item.price); if (!Number.isFinite(price) || price < 0) { message.warning(`第 ${i + 1} 条套餐价格须为非负数字`); return; } } setSaving(true); try { await request(`/admin/stores/${storeId}/packages`, { method: 'PUT', body: JSON.stringify({ packages: filled.map((p) => ({ ...p, price: Number(p.price).toFixed(2) })), }), }); message.success('套餐已保存并生效'); } catch (e) { message.error(e instanceof Error ? e.message : '保存失败'); } finally { setSaving(false); } } if (loading) { return 加载套餐中…; } return (
{items.map((item, index) => { const isCollapsed = !!collapsed[index]; const displayName = item.name.trim() || `套餐 ${index + 1}`; return (
{items.length > 1 ? ( ) : null} {!isCollapsed ? ( <> updateAt(index, { name: e.target.value })} /> updateAt(index, { price: v != null ? String(v) : '' })} /> updateAt(index, { dishes: e.target.value })} /> updateAt(index, { usableTime: e.target.value })} /> updateAt(index, { otherNotes: e.target.value })} /> ) : null}
); })} {items.length < STORE_PACKAGE_MAX_COUNT ? ( ) : null} 修改后点击下方按钮保存,C 端将立即展示生效套餐。 ); }