feat(store): dual business hours, avg price, and status lock
CI / verify (pull_request) Has been cancelled

Support 1-2 hour segments and optional avgPrice; show bank settlement on HQ store detail; enforce pickup min 2 bottles; Chinese order statuses; block shop reopen after permanent close.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-26 10:16:22 +08:00
parent 94d2a88581
commit 92cf51ba3d
25 changed files with 560 additions and 146 deletions
@@ -35,7 +35,7 @@ type OrderPreview = {
export default function OrderConfirmPickupPage() {
const router = useRouter();
const productId = router.params.productId ?? '';
const [quantity, setQuantity] = useState(Math.max(1, Number(router.params.qty || 1)));
const [quantity, setQuantity] = useState(Math.max(2, Number(router.params.qty || 2)));
const [preview, setPreview] = useState<OrderPreview | null>(null);
const [previewLoading, setPreviewLoading] = useState(false);
const [loading, setLoading] = useState(false);
@@ -70,12 +70,12 @@ export default function OrderConfirmPickupPage() {
};
}, [productId, quantity]);
const minQty = preview?.minQty ?? 1;
const minQty = preview?.minQty ?? 2;
const quantityOk = preview ? preview.quantityOk !== false && quantity >= minQty : false;
const canSubmit = !!preview && quantityOk && !loading && !previewLoading;
function updateQuantity(next: number) {
if (next < 1) return;
if (next < minQty) return;
setQuantity(next);
}
@@ -95,7 +95,7 @@ export default function OrderConfirmPickupPage() {
async function submit() {
if (!canSubmit) {
if (!quantityOk) setMsg(`现场货至少购买 ${minQty}`);
if (!quantityOk) setMsg(`现场货至少购买 ${minQty}`);
return;
}
@@ -185,8 +185,8 @@ export default function OrderConfirmPickupPage() {
<Text></Text>
<View className="order-qty-controls">
<View
className={`order-qty-btn${quantity <= 1 ? ' order-qty-btn--disabled' : ''}`}
onClick={() => updateQuantity(quantity - 1)}
className={`order-qty-btn${quantity <= minQty ? ' order-qty-btn--disabled' : ''}`}
onClick={() => updateQuantity(Math.max(minQty, quantity - 1))}
>
<Text></Text>
</View>
+3 -11
View File
@@ -6,6 +6,8 @@ import SubPageHeader from '../../components/SubPageHeader';
import { request, toast } from '../../lib/api';
import { buildPayUrl } from '../../lib/checkout-nav';
import { ORDER_STATUS_LABELS } from '@dukang/shared-types';
const TABS = [
{ key: 'all', label: '全部订单' },
{ key: 'pending_pay', label: '待付款' },
@@ -13,22 +15,12 @@ const TABS = [
{ key: 'completed', label: '已完成' },
] as const;
const STATUS_LABELS: Record<string, string> = {
PENDING_PAY: '待付款',
PENDING_SHIP: '已付款',
OUT_WAREHOUSE: '已付款',
SHIPPED: '已付款',
DELIVERED: '已付款',
COMPLETED: '已完成',
CANCELLED: '已取消',
};
function orderStatusLabel(tab: string, status?: string): string {
if (tab !== 'all') {
return TABS.find((t) => t.key === tab)?.label || status || '';
}
if (!status) return '';
return STATUS_LABELS[status] || status;
return ORDER_STATUS_LABELS[status] || status;
}
type OrderItem = {
@@ -23,6 +23,9 @@ type Store = {
carouselUrls?: string[] | null;
openTime?: string | null;
closeTime?: string | null;
openTime2?: string | null;
closeTime2?: string | null;
avgPrice?: number | null;
category?: { name: string } | null;
};
@@ -112,8 +115,17 @@ export default function StoreDetailPage() {
{store.address || '地址待完善'}
</Text>
<Text className="store-detail-meta">
: {store.openTime && store.closeTime ? `${store.openTime}-${store.closeTime}` : '10:00-22:00'}
:{' '}
{(() => {
const parts: string[] = [];
if (store.openTime && store.closeTime) parts.push(`${store.openTime}-${store.closeTime}`);
if (store.openTime2 && store.closeTime2) parts.push(`${store.openTime2}-${store.closeTime2}`);
return parts.length ? parts.join('') : '10:00-22:00';
})()}
</Text>
{store.avgPrice != null && Number(store.avgPrice) > 0 ? (
<Text className="store-detail-meta"> ¥{Number(store.avgPrice).toFixed(0)}</Text>
) : null}
{store.phone ? <Text className="store-detail-meta">: {store.phone}</Text> : null}
<View className="store-detail-tags">
{store.category?.name ? (
+10 -4
View File
@@ -31,6 +31,9 @@ type Store = {
coverUrl?: string | null;
openTime?: string | null;
closeTime?: string | null;
openTime2?: string | null;
closeTime2?: string | null;
avgPrice?: number | null;
status?: string;
categoryId?: string | null;
category?: { id?: string; name?: string; parentId?: string | null } | null;
@@ -142,10 +145,10 @@ export default function StoresPage() {
}
function formatHours(store: Store) {
if (store.openTime && store.closeTime) {
return `营业时间: ${store.openTime}-${store.closeTime}`;
}
return '营业时间: 10:00-22:00';
const parts: string[] = [];
if (store.openTime && store.closeTime) parts.push(`${store.openTime}-${store.closeTime}`);
if (store.openTime2 && store.closeTime2) parts.push(`${store.openTime2}-${store.closeTime2}`);
return parts.length ? `营业时间: ${parts.join('')}` : '营业时间: 10:00-22:00';
}
return (
@@ -204,6 +207,9 @@ export default function StoresPage() {
{s.address || '地址待完善'}
</Text>
<Text className="store-card-meta">{formatHours(s)}</Text>
{s.avgPrice != null && Number(s.avgPrice) > 0 ? (
<Text className="store-card-meta">¥{Number(s.avgPrice).toFixed(0)}</Text>
) : null}
<View className="store-card-footer">
<Text className="store-card-distance">{MOCK_DISTANCES[index % MOCK_DISTANCES.length]}</Text>
<Text