diff --git a/apps/h5-user/src/lib/navigation.ts b/apps/h5-user/src/lib/navigation.ts index af7f27f..d98af2c 100644 --- a/apps/h5-user/src/lib/navigation.ts +++ b/apps/h5-user/src/lib/navigation.ts @@ -53,6 +53,10 @@ export function buildProductDetailUrl(productId?: string | null) { return productId ? `/product/${productId}` : '/'; } +export function buildOrderAddressSelectUrl(orderId: string) { + return `/addresses?orderId=${orderId}&select=1`; +} + export function hasCheckoutContext(ctx: CheckoutContext) { return Boolean(ctx.productId || ctx.select === true || ctx.select === '1'); } diff --git a/apps/h5-user/src/pages/AddressListPage.tsx b/apps/h5-user/src/pages/AddressListPage.tsx index 2104e9a..2fb390d 100644 --- a/apps/h5-user/src/pages/AddressListPage.tsx +++ b/apps/h5-user/src/pages/AddressListPage.tsx @@ -24,10 +24,12 @@ function formatAddress(a: Address) { } export default function AddressListPage() { - const [list, setList] = useState([]); + const [pendingAddress, setPendingAddress] = useState
(null); + const [savingOrderAddress, setSavingOrderAddress] = useState(false); const [params] = useSearchParams(); const navigate = useNavigate(); const selectMode = params.get('select') === '1'; + const orderId = params.get('orderId') || ''; const productId = params.get('productId') || ''; const qty = params.get('qty') || ''; const cross = params.get('cross') === '1'; @@ -41,16 +43,44 @@ export default function AddressListPage() { loadList(); }, [loadList]); - function selectAddress(id: string) { + function selectAddress(addr: Address) { if (!selectMode) return; + if (orderId) { + setPendingAddress(addr); + return; + } const qs = new URLSearchParams(); if (productId) qs.set('productId', productId); if (qty) qs.set('qty', qty); if (cross) qs.set('cross', '1'); - qs.set('addressId', id); + qs.set('addressId', addr.id); navigate(`/order/confirm?${qs.toString()}`); } + async function confirmOrderAddress() { + if (!orderId || !pendingAddress) return; + setSavingOrderAddress(true); + try { + await request('USER_H5', `/trade/orders/${orderId}/address`, { + method: 'PUT', + body: JSON.stringify({ + receiverName: pendingAddress.receiverName, + receiverPhone: pendingAddress.phone, + receiverProvince: pendingAddress.province, + receiverCity: pendingAddress.city, + receiverDistrict: pendingAddress.district, + receiverAddress: `${pendingAddress.province}${pendingAddress.city}${pendingAddress.district}${pendingAddress.detail}`, + }), + }); + navigate(`/orders/${orderId}`); + } catch (e) { + window.alert(e instanceof Error ? e.message : '修改地址失败'); + } finally { + setSavingOrderAddress(false); + setPendingAddress(null); + } + } + async function setDefault(addr: Address, e: React.MouseEvent) { e.stopPropagation(); if (addr.isDefault === 1) return; @@ -84,6 +114,10 @@ export default function AddressListPage() { } function goBack() { + if (orderId) { + navigate(`/orders/${orderId}`); + return; + } if (hasCheckoutContext(checkoutCtx)) { navigate(buildOrderConfirmUrl(checkoutCtx)); return; @@ -93,7 +127,7 @@ export default function AddressListPage() { return (
- +
{list.length === 0 && ( @@ -108,11 +142,11 @@ export default function AddressListPage() {
selectAddress(String(a.id))} + onClick={() => selectAddress(a)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - selectAddress(String(a.id)); + selectAddress(a); } }} role={selectMode ? 'button' : undefined} @@ -187,6 +221,38 @@ export default function AddressListPage() { 新增收货地址 + + {pendingAddress && ( +
setPendingAddress(null)}> +
e.stopPropagation()}> +
+ location_on +

修改收货地址

+
+
+

确认将订单收货地址修改为:

+

+ {pendingAddress.receiverName} {maskPhone(pendingAddress.phone)} +
+ {formatAddress(pendingAddress)} +

+
+
+ + +
+
+
+ )}
); } diff --git a/apps/h5-user/src/pages/OrderDetailPage.tsx b/apps/h5-user/src/pages/OrderDetailPage.tsx index c2f3928..6ebc7ce 100644 --- a/apps/h5-user/src/pages/OrderDetailPage.tsx +++ b/apps/h5-user/src/pages/OrderDetailPage.tsx @@ -1,8 +1,8 @@ import { useEffect, useMemo, useState } from 'react'; import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import SubPageHeader from '../components/SubPageHeader'; import AppImage from '@dukang/shared-ui/AppImage'; import { request } from '../lib/api'; +import { buildOrderAddressSelectUrl } from '../lib/navigation'; import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images'; type OrderItem = { @@ -13,10 +13,12 @@ type OrderItem = { quantity: number; }; -type StatusLog = { - toStatus: string; - createdAt: string; - remark?: string; +type OrderDelivery = { + provider?: string; +}; + +type OrderPayment = { + status?: string; }; type Order = { @@ -28,34 +30,48 @@ type Order = { remark?: string | null; receiverName: string; receiverPhone: string; + receiverProvince?: string; + receiverCity?: string; + receiverDistrict?: string; receiverAddress: string; + productAmount?: number; + freightAmount?: number; payAmount: number; benefitAmount: number; createdAt: string; items?: OrderItem[]; - statusLogs?: StatusLog[]; + delivery?: OrderDelivery | null; + payment?: OrderPayment | null; }; -const RESHIP_TIMELINE = [ - { key: 'placed', title: '补发单已下达', desc: '' }, - { key: 'pickup', title: '包裹揽收中', desc: '包裹正由物流网点揽收处理' }, - { key: 'transit', title: '运输中', desc: '暂无物流信息' }, +const PROGRESS_STEPS = [ + { label: '下单成功' }, + { label: '出库中' }, + { label: '配送中' }, + { label: '待签收' }, + { label: '完成' }, ] as const; const STATUS_BANNER: Record = { PENDING_PAY: { title: '待付款', subtitle: '请尽快完成支付' }, - PENDING_SHIP: { title: '待发货', subtitle: '商家正在备货' }, - OUT_WAREHOUSE: { title: '出库中', subtitle: '商品正在出库' }, - SHIPPING: { title: '配送中', subtitle: '包裹正在配送途中' }, - PENDING_RECEIVE: { title: '待收货', subtitle: '请注意查收' }, - COMPLETED: { title: '已完成', subtitle: '感谢您的购买' }, + PENDING_SHIP: { title: '待发货', subtitle: '商家正在备货,请耐心等待' }, + OUT_WAREHOUSE: { title: '出库中', subtitle: '商品正在出库打包' }, + SHIPPING: { title: '配送中', subtitle: '您的佳酿正在赶往您的餐桌,请保持电话畅通' }, + PENDING_RECEIVE: { title: '待签收', subtitle: '包裹已送达,请确认签收' }, + COMPLETED: { title: '已完成', subtitle: '感谢您的购买,期待再次光临' }, RESHIP: { title: '补发中', subtitle: '包裹正在揽收,请耐心等待' }, }; +const EDITABLE_STATUSES = new Set(['PENDING_PAY', 'PENDING_SHIP', 'OUT_WAREHOUSE']); + function maskPhone(phone: string) { return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); } +function formatMoney(amount: number) { + return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + function formatDateTime(value: string) { const d = new Date(value); if (Number.isNaN(d.getTime())) return value; @@ -63,13 +79,38 @@ function formatDateTime(value: string) { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } -function reshipTimelineStep(status: string, index: number): 'done' | 'active' | 'pending' { - if (status === 'COMPLETED') return 'done'; - let activeIndex = 1; - if (['SHIPPING', 'PENDING_RECEIVE'].includes(status)) activeIndex = 2; - if (index < activeIndex) return 'done'; - if (index === activeIndex) return 'active'; - return 'pending'; +function progressActiveIndex(status: string) { + switch (status) { + case 'PENDING_PAY': + return 0; + case 'PENDING_SHIP': + return 1; + case 'OUT_WAREHOUSE': + return 1; + case 'SHIPPING': + return 2; + case 'PENDING_RECEIVE': + return 3; + case 'COMPLETED': + return 4; + default: + return 0; + } +} + +function deliveryProviderLabel(provider?: string) { + if (!provider || provider === 'MOCK' || provider === 'XIAOFEIXIA') return '小飞侠配送'; + return provider; +} + +function fullReceiverAddress(order: Order) { + const region = [order.receiverProvince, order.receiverCity, order.receiverDistrict] + .filter(Boolean) + .join(''); + if (region && order.receiverAddress.startsWith(region)) { + return order.receiverAddress; + } + return `${region}${order.receiverAddress}`; } export default function OrderDetailPage() { @@ -78,168 +119,278 @@ export default function OrderDetailPage() { const navigate = useNavigate(); const [order, setOrder] = useState(null); const [showCs, setShowCs] = useState(false); - - useEffect(() => { - if (id) request('USER_H5', `/trade/orders/${id}`).then(setOrder); - }, [id]); + const [copyHint, setCopyHint] = useState(''); + const [confirming, setConfirming] = useState(false); const isReship = order?.orderType === 'RESHIPMENT' || params.get('type') === 'reship'; + async function loadOrder() { + if (!id) return; + const data = await request('USER_H5', `/trade/orders/${id}`); + setOrder(data); + } + + useEffect(() => { + loadOrder().catch(() => {}); + }, [id]); + const banner = useMemo(() => { if (!order) return { title: '', subtitle: '' }; if (isReship) return STATUS_BANNER.RESHIP; return STATUS_BANNER[order.status] ?? { title: order.status, subtitle: '' }; }, [order, isReship]); + const activeProgress = order ? progressActiveIndex(order.status) : 0; const item = order?.items?.[0]; const productImage = item?.productImage || STITCH_ORDER_PRODUCT_IMAGE; - const placedAt = order?.statusLogs?.find((l) => l.toStatus === 'PENDING_SHIP')?.createdAt - ?? order?.createdAt - ?? ''; + const canEditAddress = order ? EDITABLE_STATUSES.has(order.status) : false; + const canConfirmReceive = order?.status === 'PENDING_RECEIVE' && !isReship; + const productTotal = Number(order?.productAmount ?? order?.payAmount ?? 0); + const freightTotal = Number(order?.freightAmount ?? 0); + + async function copyOrderNo() { + if (!order) return; + try { + await navigator.clipboard.writeText(order.orderNo); + setCopyHint('已复制'); + window.setTimeout(() => setCopyHint(''), 2000); + } catch { + setCopyHint('复制失败'); + } + } + + async function confirmReceive() { + if (!id || !canConfirmReceive) return; + setConfirming(true); + try { + const updated = await request('USER_H5', `/trade/orders/${id}/confirm-receive`, { + method: 'POST', + }); + setOrder(updated); + } catch (e) { + window.alert(e instanceof Error ? e.message : '确认收货失败'); + } finally { + setConfirming(false); + } + } if (!order) return
加载中...
; return ( -
- navigate(-1)} /> +
+
+ +

杜康好客

+
+ + +
+
-
-
-
-

{banner.title}

+
+
+
+ local_shipping +
+
+
+ local_shipping +

{banner.title}

+

{banner.subtitle}

- local_shipping
-
- {isReship && ( -
-

物流动态

-
- {RESHIP_TIMELINE.map((step, index) => { - const state = reshipTimelineStep(order.status, index); - return ( -
-
- {state === 'done' && ( - check - )} - {state === 'active' && } - {state === 'pending' && } -
-
-

{step.title}

- {index === 0 && placedAt && ( -

{formatDateTime(placedAt)}

- )} - {step.desc && state !== 'pending' && ( -

{step.desc}

- )} - {step.desc && state === 'pending' && ( -

{step.desc}

- )} -
+ {!isReship && ( +
+
+ {PROGRESS_STEPS.map((step, index) => { + const done = index < activeProgress; + const active = index === activeProgress; + const pending = index > activeProgress; + return ( +
+ {index < PROGRESS_STEPS.length - 1 && ( + + )} +
+ {done && check} + {active && } + {pending && }
- ); - })} -
-
- )} - - {isReship && ( -
-
- 补发原因 - {order.remark || '商品破损'} -
-
- 关联原订单 - {order.originOrderId ? ( - - 查看原订单 - - ) : ( - DK20231005002 - )} -
-
- )} - - {item && ( -
-
-
- -
-
-
-

{item.productName}

-

{item.productSpec}

+ {step.label}
-
- - ¥{isReship ? '0.00' : Number(item.unitPrice).toFixed(2)} - - x{item.quantity} -
-
-
- {Number(order.benefitAmount) > 0 && ( -
-
- - ¥{order.benefitAmount}好客权益 - - 随单赠送 -
- info -
- )} -
- )} - -
-
- location_on -

收货信息

-
-
-
- {order.receiverName} - {maskPhone(order.receiverPhone)} -
-

{order.receiverAddress}

+ ); + })}
+ )} - {!isReship && ( -
-
- 订单号 - {order.orderNo} + {isReship && ( +
+
+ 补发原因 + {order.remark || '商品破损'} +
+
+ 关联原订单 + {order.originOrderId ? ( + + 查看原订单 + + ) : ( + + )} +
+
+ )} + + {item && ( +
+
+
+
-
+
+
+

{item.productName}

+

规格:{item.productSpec}

+
+
+ + ¥ + {formatMoney(isReship ? 0 : Number(item.unitPrice)).replace('.00', '')} + + x{item.quantity} +
+
+
+ + {Number(order.benefitAmount) > 0 && !isReship && ( +
+
+
+ confirmation_number +
+
+
+ ¥{Math.round(Number(order.benefitAmount))} + 好客权益 +
+

全城100+餐厅通用

+
+
+ +
+ )} +
+ )} + +
+
+

+ location_on + 收货信息 +

+ {canEditAddress && id && ( + + )} +
+
+
+ 收货人 + + {order.receiverName} {maskPhone(order.receiverPhone)} + +
+
+ 收货地址 + {fullReceiverAddress(order)} +
+
+ 配送方式 + {deliveryProviderLabel(order.delivery?.provider)} +
+
+
+ + {!isReship && ( +
+

+ info + 订单信息 +

+
+
+ 订单编号 + + {order.orderNo} + + +
+
+ 创建时间 + {formatDateTime(order.createdAt)} +
+
+ 支付方式 + {order.payment?.status === 'SUCCESS' ? '微信支付' : '—'} +
+
+
+
+ 商品总额 + ¥{formatMoney(productTotal)} +
+
+ 运费 + ¥{formatMoney(freightTotal)} +
+
实付金额 - ¥{order.payAmount} + ¥{formatMoney(Number(order.payAmount))}
-
- )} -
+
+
+ )}
- {showCs && ( diff --git a/apps/h5-user/src/styles.css b/apps/h5-user/src/styles.css index e193536..59c8958 100644 --- a/apps/h5-user/src/styles.css +++ b/apps/h5-user/src/styles.css @@ -3678,6 +3678,530 @@ font-size: 20px; } +/* ── 订单详情 Stitch 版(user/40 支持修改地址) ── */ +.order-detail-page--stitch { + padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 88px); +} + +.order-detail-topbar { + position: sticky; + top: 0; + z-index: 50; + height: 64px; + padding: 0 var(--space-page); + display: flex; + align-items: center; + justify-content: space-between; + background: var(--color-surface); + box-shadow: 0 4px 20px rgba(166, 29, 36, 0.05); +} + +.order-detail-topbar-title { + flex: 1; + margin-left: 8px; + font-family: var(--font-headline); + font-size: 20px; + font-weight: 700; + color: var(--color-heritage-red); +} + +.order-detail-topbar-actions { + display: flex; + align-items: center; + gap: 16px; +} + +.order-detail-topbar-btn { + border: none; + background: none; + padding: 0; + color: var(--color-on-surface-variant); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.order-detail-topbar-btn .material-symbols-outlined { + font-size: 22px; +} + +.order-detail-main--stitch { + padding: var(--space-md) var(--space-page) 24px; + display: flex; + flex-direction: column; + gap: var(--space-md); + max-width: 480px; + margin: 0 auto; +} + +.order-detail-status-card { + position: relative; + overflow: hidden; + border-radius: var(--radius-lg); + background: var(--color-heritage-red); + padding: 24px; + color: var(--color-on-primary); + box-shadow: 0 8px 24px rgba(166, 29, 36, 0.15); +} + +.order-detail-status-deco { + position: absolute; + right: -40px; + top: -40px; + opacity: 0.1; + pointer-events: none; +} + +.order-detail-status-deco .material-symbols-outlined { + font-size: 120px; + font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; +} + +.order-detail-status-body { + position: relative; + z-index: 1; +} + +.order-detail-status-head { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.order-detail-status-head h2 { + font-family: var(--font-headline); + font-size: 20px; + font-weight: 700; + line-height: 28px; +} + +.order-detail-status-body p { + font-size: 14px; + line-height: 20px; + opacity: 0.9; +} + +.order-detail-progress-card { + padding: 16px 8px 12px !important; +} + +.order-detail-progress { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.order-detail-progress-step { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + min-width: 0; +} + +.order-detail-progress-line { + position: absolute; + top: 14px; + left: 50%; + width: 100%; + height: 2px; + background: var(--color-surface-container-highest); + z-index: 0; +} + +.order-detail-progress-line.done { + background: var(--color-heritage-red); +} + +.order-detail-progress-dot { + position: relative; + z-index: 1; + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-surface-container-highest); +} + +.order-detail-progress-step.done .order-detail-progress-dot { + background: var(--color-heritage-red); + color: var(--color-on-primary); +} + +.order-detail-progress-step.done .order-detail-progress-dot .material-symbols-outlined { + font-size: 16px; + font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 24; +} + +.order-detail-progress-step.active .order-detail-progress-dot { + background: var(--color-heritage-red); + box-shadow: 0 0 0 4px var(--color-primary-fixed, #ffdad7); +} + +.order-detail-progress-pulse { + width: 8px; + height: 8px; + border-radius: 50%; + background: #fff; + animation: order-step-pulse 1.5s ease-in-out infinite; +} + +.order-detail-progress-idle { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-subtle-gray); +} + +.order-detail-progress-label { + margin-top: 8px; + font-size: 12px; + line-height: 16px; + color: var(--color-subtle-gray); + text-align: center; + white-space: nowrap; +} + +.order-detail-progress-step.done .order-detail-progress-label, +.order-detail-progress-step.active .order-detail-progress-label { + color: var(--color-on-surface); +} + +.order-detail-progress-step.active .order-detail-progress-label { + color: var(--color-heritage-red); + font-weight: 600; +} + +.order-detail-card-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.order-detail-card-head h3, +.order-detail-card-title { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--font-headline); + font-size: 18px; + font-weight: 600; + color: var(--color-on-surface); + margin: 0 0 16px; +} + +.order-detail-card-head h3 { + margin-bottom: 0; +} + +.order-detail-card-head .material-symbols-outlined, +.order-detail-card-title .material-symbols-outlined { + font-size: 20px; + color: var(--color-on-surface-variant); +} + +.order-detail-edit-btn { + border: none; + background: none; + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--color-heritage-red); + font-size: 12px; + font-weight: 500; + cursor: pointer; +} + +.order-detail-edit-btn .material-symbols-outlined { + font-size: 16px; +} + +.order-detail-kv-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.order-detail-kv-list--bordered { + padding-bottom: 16px; + border-bottom: 1px solid rgba(227, 226, 224, 0.6); +} + +.order-detail-kv { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + font-size: 14px; + line-height: 20px; +} + +.order-detail-kv > span:first-child { + color: var(--color-on-surface-variant); + flex-shrink: 0; +} + +.order-detail-kv > span:last-child, +.order-detail-kv > a { + font-weight: 500; + color: var(--color-on-surface); + text-align: right; +} + +.order-detail-kv--address > span:last-child { + max-width: 68%; +} + +.order-detail-kv--muted { + font-size: 12px; +} + +.order-detail-kv--muted > span { + color: var(--color-on-surface-variant) !important; + font-weight: 400 !important; +} + +.order-detail-kv--total { + padding-top: 8px; + align-items: baseline; +} + +.order-detail-kv--total > span:first-child { + font-weight: 700; + color: var(--color-on-surface); +} + +.order-detail-kv-inline { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.order-detail-copy-btn { + border: 1px solid rgba(166, 29, 36, 0.3); + background: none; + color: var(--color-heritage-red); + font-size: 10px; + line-height: 1; + padding: 2px 4px; + border-radius: 2px; + cursor: pointer; +} + +.order-detail-summary { + padding-top: 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.order-detail-pay-total { + font-family: var(--font-headline); + font-size: 20px; + font-weight: 700; + color: var(--color-heritage-red) !important; +} + +.order-detail-benefit-panel { + margin-top: 16px; + padding: 16px; + border-radius: var(--radius-lg); + background: rgba(255, 191, 0, 0.1); + border: 1px solid rgba(255, 191, 0, 0.2); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.order-detail-benefit-panel-left { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.order-detail-benefit-icon { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--color-aged-amber, #ffbf00); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.order-detail-benefit-amount { + display: flex; + align-items: baseline; + gap: 4px; + font-family: var(--font-headline); + font-weight: 600; + color: var(--color-on-secondary-fixed-variant, #574500); +} + +.order-detail-benefit-amount span:first-child { + font-size: 18px; +} + +.order-detail-benefit-amount span:last-child { + font-size: 12px; +} + +.order-detail-benefit-sub { + margin-top: 2px; + font-size: 10px; + color: var(--color-on-secondary-fixed-variant, #574500); + opacity: 0.7; +} + +.order-detail-benefit-use { + border: none; + background: var(--color-aged-amber, #ffbf00); + color: var(--color-on-secondary-fixed-variant, #574500); + font-size: 12px; + font-weight: 700; + padding: 8px 16px; + border-radius: var(--radius-full); + cursor: pointer; + flex-shrink: 0; +} + +.order-detail-link { + color: var(--color-heritage-red); + text-decoration: none; + font-weight: 500; +} + +.order-detail-actionbar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 50; + display: flex; + gap: 16px; + padding: 16px var(--space-page) calc(env(safe-area-inset-bottom, 0px) + 16px); + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(10px); + box-shadow: 0 -4px 20px rgba(166, 29, 36, 0.05); +} + +.order-detail-action-outline, +.order-detail-action-primary { + height: 48px; + border-radius: var(--radius-lg); + font-family: var(--font-headline); + font-size: 16px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; +} + +.order-detail-action-outline { + flex: 1; + border: 1px solid var(--color-heritage-red); + background: transparent; + color: var(--color-heritage-red); +} + +.order-detail-action-primary { + flex: 1.5; + border: none; + background: var(--color-heritage-red); + color: var(--color-on-primary); + box-shadow: 0 4px 12px rgba(166, 29, 36, 0.2); +} + +.order-detail-action-primary:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.order-address-modal-overlay { + position: fixed; + inset: 0; + z-index: 120; + background: rgba(26, 26, 26, 0.6); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-page); +} + +.order-address-modal { + width: 100%; + max-width: 400px; + background: var(--color-surface); + border-radius: var(--radius-lg); + padding: 24px; + box-shadow: var(--shadow-card); +} + +.order-address-modal-head { + display: flex; + align-items: center; + gap: 8px; + color: var(--color-heritage-red); + margin-bottom: 16px; +} + +.order-address-modal-head h3 { + font-family: var(--font-headline); + font-size: 18px; + font-weight: 600; +} + +.order-address-modal-body { + background: var(--color-surface-container); + border-radius: var(--radius-lg); + padding: 16px; + font-size: 14px; + line-height: 1.5; + color: var(--color-on-surface-variant); +} + +.order-address-modal-target { + margin-top: 8px; + color: var(--color-on-surface); + font-weight: 500; +} + +.order-address-modal-actions { + display: flex; + gap: 12px; + margin-top: 20px; +} + +.order-address-modal-actions button { + flex: 1; + height: 44px; + border-radius: var(--radius-lg); + border: 1px solid var(--color-outline-variant); + background: var(--color-card); + font-size: 15px; + cursor: pointer; +} + +.order-address-modal-actions button.primary { + border: none; + background: var(--color-heritage-red); + color: var(--color-on-primary); + font-weight: 600; +} + /* ── 我的 - 个人中心(stitch user/18) ── */ .mine-page { min-height: 100vh; diff --git a/server/dukang-api/src/modules/trade/trade.controller.ts b/server/dukang-api/src/modules/trade/trade.controller.ts index 19ed3a0..80c5533 100644 --- a/server/dukang-api/src/modules/trade/trade.controller.ts +++ b/server/dukang-api/src/modules/trade/trade.controller.ts @@ -46,6 +46,11 @@ export class TradeController { ) { return this.tradeService.updateAddress(user.actorId, BigInt(id), body); } + + @Post(':id/confirm-receive') + confirmReceive(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.tradeService.confirmReceive(user.actorId, BigInt(id)); + } } @Controller('partner/orders') diff --git a/server/dukang-api/src/modules/trade/trade.service.ts b/server/dukang-api/src/modules/trade/trade.service.ts index 4e3dc3d..15f5b3c 100644 --- a/server/dukang-api/src/modules/trade/trade.service.ts +++ b/server/dukang-api/src/modules/trade/trade.service.ts @@ -247,6 +247,16 @@ export class TradeService { return serializeBigInt(updated); } + async confirmReceive(userId: bigint, orderId: bigint) { + const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } }); + if (!order) throw new NotFoundException('订单不存在'); + if (order.status !== 'PENDING_RECEIVE') { + throw new BadRequestException('当前状态不可确认收货'); + } + await this.applyStatusTransition(order.id, order.status, 'COMPLETED', 'USER'); + return this.getOrder(userId, orderId); + } + async listPartnerOrders(partnerAccountId: bigint, page = 1, pageSize = 20) { const account = await this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: partnerAccountId }, @@ -296,7 +306,12 @@ export class TradeService { return this.getPartnerOrder(partnerAccountId, orderId); } - async applyStatusTransition(orderId: bigint, fromStatus: string, targetStatus: string) { + async applyStatusTransition( + orderId: bigint, + fromStatus: string, + targetStatus: string, + operator = 'MOCK', + ) { const order = await this.prisma.order.findUnique({ where: { id: orderId } }); if (!order) return; const currentStatus = fromStatus || order.status; @@ -324,7 +339,7 @@ export class TradeService { orderId, fromStatus: currentStatus, toStatus: targetStatus, - operator: 'MOCK', + operator, }, }); });