273 lines
10 KiB
TypeScript
273 lines
10 KiB
TypeScript
import { useState, type ReactNode } from 'react'
|
|
import { Button, ErrorBlock, List, Skeleton, Tag, Toast } from 'antd-mobile'
|
|
import { LeftOutline } from 'antd-mobile-icons'
|
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
|
|
import { BrandLogo } from '@/components/BrandLogo'
|
|
import { useHandoffRecords, useMyOrder } from '@/features/orders/api'
|
|
import { fetchOrderConversation } from '@/features/messages/api'
|
|
import { MobileOrderActionPanel } from '@/features/mobile/MobileOrderActionPanel'
|
|
import {
|
|
checkoutStatusMeta,
|
|
handoffStatusMeta,
|
|
orderStatusMeta,
|
|
settlementStatusMeta,
|
|
} from '@/features/orders/labels'
|
|
import { useSessionStore } from '@/stores/session'
|
|
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
|
|
|
const handoffRecordTypeMap: Record<string, string> = {
|
|
owner_handoff: '号主交接',
|
|
platform_handoff: '平台代交接',
|
|
renter_checkout: '结账记录',
|
|
renter_return: '归还记录',
|
|
}
|
|
|
|
function StatusLine({ label, children }: { label: string; children: ReactNode }) {
|
|
return (
|
|
<div className="mobile-order-detail__status-line">
|
|
<span>{label}</span>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function MobileOrderDetailPage() {
|
|
const { id } = useParams()
|
|
const navigate = useNavigate()
|
|
const queryClient = useQueryClient()
|
|
const token = useSessionStore(state => state.token)
|
|
const hydrated = useSessionStore(state => state.hydrated)
|
|
const query = useMyOrder(id, Boolean(token))
|
|
const handoffQuery = useHandoffRecords(id, Boolean(token))
|
|
const order = query.data
|
|
const handoffRecords = handoffQuery.data ?? []
|
|
const [openingChat, setOpeningChat] = useState(false)
|
|
|
|
const openOrderChat = async () => {
|
|
if (!order || openingChat) return
|
|
setOpeningChat(true)
|
|
try {
|
|
const conversation = await fetchOrderConversation(order.id)
|
|
navigate(`/m/messages/${conversation.id}`)
|
|
} catch (error) {
|
|
Toast.show({
|
|
icon: 'fail',
|
|
content: error instanceof Error ? error.message : '账号群暂时无法打开',
|
|
})
|
|
} finally {
|
|
setOpeningChat(false)
|
|
}
|
|
}
|
|
|
|
if (!hydrated) {
|
|
return (
|
|
<div className="mobile-order-detail">
|
|
<Skeleton.Title animated />
|
|
<Skeleton.Paragraph lineCount={8} animated />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!token) {
|
|
return (
|
|
<div className="mobile-order-detail">
|
|
<div className="mobile-order-detail__back">
|
|
<button type="button" onClick={() => navigate('/m/orders')}>
|
|
<LeftOutline /> 返回订单
|
|
</button>
|
|
</div>
|
|
<div className="mobile-orders__login">
|
|
<BrandLogo height={40} variant="seal" />
|
|
<h2>登录后查看订单详情</h2>
|
|
<p>交接记录与结账明细只对交易双方可见。</p>
|
|
<Button block color="primary" onClick={() => navigate(`/m/login?redirect=/m/orders/${id}`)}>
|
|
去登录
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (query.isPending) {
|
|
return (
|
|
<div className="mobile-order-detail">
|
|
<Skeleton.Title animated />
|
|
<Skeleton.Paragraph lineCount={8} animated />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (query.isError || !order) {
|
|
return (
|
|
<div className="mobile-order-detail">
|
|
<div className="mobile-order-detail__back">
|
|
<button type="button" onClick={() => navigate('/m/orders')}>
|
|
<LeftOutline /> 返回订单
|
|
</button>
|
|
</div>
|
|
<ErrorBlock
|
|
status="disconnected"
|
|
title="订单详情没有加载出来"
|
|
description="订单可能已关闭,或当前账号无权查看。"
|
|
>
|
|
<Button color="primary" size="small" onClick={() => void query.refetch()}>
|
|
重新加载
|
|
</Button>
|
|
</ErrorBlock>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const status = orderStatusMeta(order.status)
|
|
const handoff = handoffStatusMeta(order.handoff_status)
|
|
const settlement = settlementStatusMeta(order.settlement_status)
|
|
const checkout = order.checkout
|
|
|
|
return (
|
|
<div className="mobile-order-detail">
|
|
<div className="mobile-order-detail__back">
|
|
<button type="button" onClick={() => navigate('/m/orders')}>
|
|
<LeftOutline /> 返回订单
|
|
</button>
|
|
</div>
|
|
|
|
<section className="mobile-order-detail__hero">
|
|
<div className="mobile-order-detail__hero-top">
|
|
<span className="mobile-order-detail__no">{order.order_no}</span>
|
|
<Tag color={status.tone} fill="outline">
|
|
{status.label}
|
|
</Tag>
|
|
</div>
|
|
<h1>{order.title}</h1>
|
|
<div className="mobile-order-detail__status">
|
|
<StatusLine label="交接">
|
|
<Tag color={handoff.tone} fill="outline">
|
|
{handoff.label}
|
|
</Tag>
|
|
</StatusLine>
|
|
<StatusLine label="结算">
|
|
<Tag color={settlement.tone} fill="outline">
|
|
{settlement.label}
|
|
</Tag>
|
|
</StatusLine>
|
|
</div>
|
|
<div className="mobile-order-detail__hero-amount">
|
|
<strong>{formatMoney(order.display_amount_cent)}</strong>
|
|
<span>押金 {formatMoney(order.deposit_amount_cent)}</span>
|
|
</div>
|
|
</section>
|
|
|
|
<List header="商品信息" className="mobile-order-detail__list">
|
|
<List.Item extra={formatText(order.listing_no)}>商品编号</List.Item>
|
|
<List.Item extra={formatText(order.server_region)}>区服</List.Item>
|
|
<List.Item extra={formatText(order.login_platform)}>登录平台</List.Item>
|
|
<List.Item extra={order.estimated_duration_hours ? `${order.estimated_duration_hours} 小时` : '—'}>
|
|
预计租期
|
|
</List.Item>
|
|
</List>
|
|
|
|
<List header="金额明细" className="mobile-order-detail__list">
|
|
<List.Item extra={formatMoney(order.rent_amount_cent)}>租客租金</List.Item>
|
|
<List.Item extra={formatMoney(order.owner_rent_amount_cent)}>号主租金</List.Item>
|
|
<List.Item extra={formatMoney(order.platform_fee_cent)}>平台服务费</List.Item>
|
|
<List.Item extra={formatMoney(order.deposit_amount_cent)}>实收押金</List.Item>
|
|
{order.deposit_waived_amount_cent > 0 ? (
|
|
<List.Item extra={formatMoney(order.deposit_waived_amount_cent)}>押金减免</List.Item>
|
|
) : null}
|
|
{order.refund_amount_cent > 0 ? (
|
|
<List.Item extra={formatMoney(order.refund_amount_cent)}>已退金额</List.Item>
|
|
) : null}
|
|
</List>
|
|
|
|
<List header="时间" className="mobile-order-detail__list">
|
|
<List.Item extra={formatDateTime(order.created_at)}>下单时间</List.Item>
|
|
<List.Item extra={formatDateTime(order.rented_at)}>起租时间</List.Item>
|
|
<List.Item extra={formatDateTime(order.estimated_end_at)}>预计结束</List.Item>
|
|
<List.Item extra={formatDateTime(order.payment_deadline_at)}>支付截止</List.Item>
|
|
</List>
|
|
|
|
{checkout ? (
|
|
<List header="结账单" className="mobile-order-detail__list">
|
|
<List.Item extra={<Tag color={checkoutStatusMeta(checkout.status).tone}>{checkoutStatusMeta(checkout.status).label}</Tag>}>
|
|
结账状态
|
|
</List.Item>
|
|
<List.Item extra={`${checkout.round_count} / ${checkout.max_rounds}`}>协商轮次</List.Item>
|
|
{checkout.turn ? (
|
|
<List.Item extra={checkout.turn === 'owner' ? '号主处理' : '租客处理'}>
|
|
当前轮到
|
|
</List.Item>
|
|
) : null}
|
|
<List.Item extra={formatMoney(checkout.consumable_amount_cent)}>消耗品费用</List.Item>
|
|
<List.Item extra={`${checkout.coin_consumed_m} M`}>哈夫币消耗</List.Item>
|
|
<List.Item extra={formatMoney(checkout.other_amount_cent)}>其他费用</List.Item>
|
|
<List.Item extra={formatMoney(checkout.deposit_deduct_amount_cent)}>押金扣除</List.Item>
|
|
<List.Item extra={formatMoney(checkout.renter_refund_amount_cent)}>可退金额</List.Item>
|
|
<List.Item extra={formatMoney(checkout.owner_income_amount_cent)}>号主到手</List.Item>
|
|
{checkout.shortfall_cent > 0 ? (
|
|
<List.Item extra={<span className="mobile-order-detail__danger">{formatMoney(checkout.shortfall_cent)}</span>}>
|
|
押金缺口
|
|
</List.Item>
|
|
) : null}
|
|
{checkout.content ? (
|
|
<List.Item>
|
|
<div className="mobile-order-detail__checkout-content">{checkout.content}</div>
|
|
</List.Item>
|
|
) : null}
|
|
</List>
|
|
) : null}
|
|
|
|
{order.active_dispute ? (
|
|
<List header="申诉" className="mobile-order-detail__list">
|
|
<List.Item extra={`#${order.active_dispute.id}`}>申诉单号</List.Item>
|
|
<List.Item extra={formatText(order.active_dispute.type)}>类型</List.Item>
|
|
<List.Item extra={formatText(order.active_dispute.status)}>状态</List.Item>
|
|
</List>
|
|
) : null}
|
|
|
|
{handoffRecords.length > 0 ? (
|
|
<List header="交接与结账记录" className="mobile-order-detail__list">
|
|
{handoffRecords.map(record => (
|
|
<List.Item key={record.id}>
|
|
<div className="mobile-order-detail__record">
|
|
<div className="mobile-order-detail__record-head">
|
|
<strong>{handoffRecordTypeMap[record.type] ?? record.type}</strong>
|
|
<span>{formatDateTime(record.created_at)}</span>
|
|
</div>
|
|
<p>{record.content}</p>
|
|
<div className="mobile-order-detail__record-tags">
|
|
{record.confirmed_by_renter_at ? <Tag color="success">租客已确认</Tag> : null}
|
|
{record.confirmed_by_owner_at ? <Tag color="success">号主已确认</Tag> : null}
|
|
</div>
|
|
</div>
|
|
</List.Item>
|
|
))}
|
|
</List>
|
|
) : null}
|
|
|
|
<div className="mobile-order-detail__actions">
|
|
<Button
|
|
block
|
|
fill="outline"
|
|
color="primary"
|
|
loading={openingChat}
|
|
onClick={() => void openOrderChat()}
|
|
>
|
|
进入账号群
|
|
</Button>
|
|
<MobileOrderActionPanel
|
|
order={order}
|
|
onChanged={async () => {
|
|
await Promise.all([query.refetch(), handoffQuery.refetch()])
|
|
await queryClient.invalidateQueries({ queryKey: ['my-orders'] })
|
|
}}
|
|
/>
|
|
<Link to="/m/orders" className="mobile-order-detail__back-link">
|
|
返回我的订单
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|