feat(storefront): 新增 PC 订单中心与订单操作

This commit is contained in:
yml2213
2026-10-30 13:56:44 +08:00
parent 55e914edd1
commit e13eda20a2
13 changed files with 1668 additions and 101 deletions
@@ -23,6 +23,13 @@ import {
submitCheckout,
submitHandoff,
} from '@/features/orders/api'
import {
DISPUTE_TYPES,
getOrderActionAvailability,
splitEvidenceLines,
toPositiveNumber,
yuanToCent,
} from '@/features/orders/orderActions'
import type { UserOrderDetail } from '@/features/orders/types'
import { useSessionStore } from '@/stores/session'
import { formatMoney } from '@/shared/utils/format'
@@ -46,32 +53,6 @@ interface MobileOrderActionPanelProps {
onChanged: () => void | Promise<void>
}
const disputeTypes = [
{ label: '无法登录', value: 'cannot_login' },
{ label: '账号异常', value: 'account_abnormal' },
{ label: '租金/押金争议', value: 'rent_dispute' },
{ label: '退款问题', value: 'refund_issue' },
{ label: '其他', value: 'other' },
]
function splitLines(value: string) {
return value
.split(/\r?\n/)
.map(item => item.trim())
.filter(Boolean)
}
function toCent(value: string) {
const amount = Number(value)
if (!Number.isFinite(amount) || amount < 0) return 0
return Math.round(amount * 100)
}
function toNumber(value: string) {
const amount = Number(value)
return Number.isFinite(amount) && amount >= 0 ? amount : 0
}
export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPanelProps) {
const user = useSessionStore(state => state.user)
const [busy, setBusy] = useState<BusyAction>(null)
@@ -95,63 +76,22 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
const [disputeDescription, setDisputeDescription] = useState('')
const [disputeEvidence, setDisputeEvidence] = useState('')
const isOwner = order.owner_id === user?.id || order.price_role === 'owner'
const isRenter = order.renter_id === user?.id || order.price_role === 'renter'
const checkout = order.checkout
const shortfall = checkout?.shortfall_cent ?? 0
const canPay = isRenter && order.status === 'pending_payment'
const canSubmitHandoff =
isOwner &&
order.status === 'pending_handoff' &&
['pending_owner', 'owner_timeout'].includes(order.handoff_status)
const canConfirmReceive =
isRenter && order.status === 'pending_handoff' && order.handoff_status === 'pending_renter_confirm'
const canSubmitCheckout =
isRenter &&
['renting', 'overdue'].includes(order.status) &&
['received', 'return_overdue'].includes(order.handoff_status) &&
!checkout
const ownerCheckoutPending =
isOwner &&
(order.status === 'pending_checkout_confirm' ||
(order.status === 'abnormal' &&
[
'owner_checkout_confirm_timeout',
'owner_return_confirm_timeout',
'pending_owner_checkout',
].includes(order.handoff_status)))
const renterCheckoutPending = isRenter && order.status === 'pending_checkout_accept'
const canConfirmCheckout =
ownerCheckoutPending && Boolean(checkout) && checkout?.can_accept !== false && shortfall <= 0
const canAcceptCheckout =
renterCheckoutPending && Boolean(checkout) && checkout?.can_accept !== false && shortfall <= 0
const canCounterCheckout = Boolean(checkout?.can_counter)
const canCancelOrder = isRenter && ['pending_payment', 'pending_handoff'].includes(order.status)
const canOpenDispute =
(isOwner || isRenter) &&
!order.active_dispute &&
!['completed', 'cancelled', 'closed', 'disputing', 'checkout_disputing'].includes(order.status)
const canCancelDispute =
Boolean(order.active_dispute) &&
order.active_dispute?.initiator_type !== 'admin' &&
order.active_dispute?.initiator_id === user?.id &&
['disputing', 'checkout_disputing'].includes(order.status)
const checkoutDisputeStage = ['pending_checkout_confirm', 'pending_checkout_accept'].includes(
order.status
)
const hasAction =
canPay ||
canSubmitHandoff ||
canConfirmReceive ||
canSubmitCheckout ||
canConfirmCheckout ||
canAcceptCheckout ||
canCounterCheckout ||
canCancelOrder ||
canOpenDispute ||
canCancelDispute
const {
canPay,
canSubmitHandoff,
canConfirmReceive,
canSubmitCheckout,
canConfirmCheckout,
canAcceptCheckout,
canCounterCheckout,
canCancelOrder,
canOpenDispute,
canCancelDispute,
checkoutDisputeStage,
hasAction,
} = getOrderActionAvailability(order, user?.id)
const run = async (action: BusyAction, fn: () => Promise<void>) => {
setBusy(action)
@@ -266,10 +206,10 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
await run('checkout', async () => {
await submitCheckout(order.id, {
content: checkoutContent.trim(),
consumable_amount_cent: toCent(consumableAmount),
coin_consumed_m: toNumber(coinConsumed),
other_amount_cent: toCent(otherAmount),
evidence_urls: splitLines(checkoutEvidence),
consumable_amount_cent: yuanToCent(consumableAmount),
coin_consumed_m: toPositiveNumber(coinConsumed),
other_amount_cent: yuanToCent(otherAmount),
evidence_urls: splitEvidenceLines(checkoutEvidence),
})
setCheckoutOpen(false)
Toast.show({ icon: 'success', content: '结账已发起,等待对方确认' })
@@ -328,11 +268,11 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
await counterCheckout(order.id, {
content: counterReason.trim(),
reason: counterReason.trim(),
consumable_amount_cent: toCent(counterConsumable),
coin_consumed_m: toNumber(counterCoin),
other_amount_cent: toCent(counterDeduct),
deposit_deduct_amount_cent: toCent(counterDeduct),
evidence_urls: splitLines(counterEvidence),
consumable_amount_cent: yuanToCent(counterConsumable),
coin_consumed_m: toPositiveNumber(counterCoin),
other_amount_cent: yuanToCent(counterDeduct),
deposit_deduct_amount_cent: yuanToCent(counterDeduct),
evidence_urls: splitEvidenceLines(counterEvidence),
})
setCounterOpen(false)
Toast.show({ icon: 'success', content: '结账方案已提交' })
@@ -361,7 +301,7 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
await createOrderDispute(order.id, {
type: checkoutDisputeStage ? 'checkout_dispute' : disputeType,
description: disputeDescription.trim(),
evidence_urls: splitLines(disputeEvidence),
evidence_urls: splitEvidenceLines(disputeEvidence),
})
setDisputeOpen(false)
Toast.show({ icon: 'success', content: '申诉已提交' })
@@ -657,7 +597,7 @@ export function MobileOrderActionPanel({ order, onChanged }: MobileOrderActionPa
<div className="mobile-order-sheet__field">
<span>申诉类型</span>
<Selector
options={disputeTypes}
options={DISPUTE_TYPES}
value={[disputeType]}
onChange={values => setDisputeType(values[0] ?? 'cannot_login')}
/>