feat(storefront): 新增 PC 订单中心与订单操作
This commit is contained in:
@@ -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')}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user