diff --git a/.cursor/skills/dukang-v3/reference-req-index.md b/.cursor/skills/dukang-v3/reference-req-index.md index 6e9287c..0715e2c 100644 --- a/.cursor/skills/dukang-v3/reference-req-index.md +++ b/.cursor/skills/dukang-v3/reference-req-index.md @@ -32,6 +32,7 @@ | REQ-U-024 | 推广码 | 扫码归因;现场提货有码归码 | | REQ-U-025 | 分享 | 微信商品分享+渠道参数 | | REQ-U-026 | 门店规则 | 附件一完整展示 | +| REQ-U-027 | 门店套餐 **3.4.10** | 门店详情展示生效套餐;套餐异议申诉入口 | ## 门店端 REQ-S @@ -58,6 +59,7 @@ | REQ-S-018 | 权限 | §4.6 矩阵 | | REQ-S-019 | 多店选店 | 记住上次所选 | | REQ-S-020 | 待处理核销 | Wave 3 弱网单 | +| REQ-S-021 | 门店套餐 **3.4.10** | 套餐列表/编辑/提审 | ## 合伙人端 REQ-P @@ -89,6 +91,7 @@ | REQ-P-024 | 负责人复核 | 待复核→待总部审 | | REQ-P-025 | 合同同日 | 同日提交校验 | | REQ-P-026 | 我的仓库 | Wave 3 管仓只读 | +| REQ-P-027 | 门店套餐 **3.4.10** | 拓店套餐页;门店套餐列表/编辑/提审 | ## 总部端 REQ-H @@ -122,3 +125,4 @@ | REQ-H-022 | 签约主体 | 山西领势酒业 | | REQ-H-023 | 热力图 | 腾讯位置服务 | | REQ-H-024 | 结算异议 | 门店 3 日异议处理 | +| REQ-H-025 | 门店套餐 **3.4.10** | 门店详情套餐 Tab 直存;套餐变更审核通过/驳回 | diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 4f5adba..49051bd 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -5,6 +5,7 @@ import LoginPage from './pages/LoginPage'; import DashboardPage from './pages/DashboardPage'; import UsersPage from './pages/UsersPage'; import OrdersPage from './pages/OrdersPage'; +import StorePackageAuditsPage from './pages/StorePackageAuditsPage'; import StoresPage from './pages/StoresPage'; import StoreRatingsPage from './pages/StoreRatingsPage'; import StoreAccountsPage from './pages/StoreAccountsPage'; @@ -79,6 +80,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/admin-web/src/components/AdminStorePackagesSection.tsx b/apps/admin-web/src/components/AdminStorePackagesSection.tsx new file mode 100644 index 0000000..9a38790 --- /dev/null +++ b/apps/admin-web/src/components/AdminStorePackagesSection.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from 'react'; +import { Button, Space, Typography, message } from 'antd'; +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 [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 }))); + } + + 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 ( +
+ + 总部直存立即生效,无需审核。最多 {STORE_PACKAGE_MAX_COUNT} 条。 + + {items.map((item, index) => ( +
+ + 套餐 {index + 1} + {items.length > 1 ? ( + + ) : null} + + + updateAt(index, { name: e.target.value })} + style={{ width: '100%', padding: 8 }} + /> + updateAt(index, { price: e.target.value })} + style={{ width: '100%', padding: 8 }} + /> +