247 lines
7.1 KiB
TypeScript
247 lines
7.1 KiB
TypeScript
import { useState } from 'react'
|
|
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
|
import {
|
|
Alert,
|
|
Button,
|
|
Card,
|
|
Select,
|
|
Space,
|
|
Table,
|
|
Tag,
|
|
Typography,
|
|
type TableColumnsType,
|
|
} from 'antd'
|
|
import { ReloadOutlined, RightOutlined } from '@ant-design/icons'
|
|
import { Link, useNavigate } from 'react-router-dom'
|
|
|
|
import { BrandLogo } from '@/components/BrandLogo'
|
|
import { fetchMyOrders } from '@/features/orders/api'
|
|
import { handoffStatusMeta, orderStatusMeta, roleLabel } from '@/features/orders/labels'
|
|
import type { UserOrderListItem } from '@/features/orders/types'
|
|
import { useSessionStore } from '@/stores/session'
|
|
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
|
|
|
const PAGE_SIZE = 20
|
|
|
|
const statusOptions = [
|
|
'pending_payment',
|
|
'pending_handoff',
|
|
'renting',
|
|
'overdue',
|
|
'pending_checkout_confirm',
|
|
'pending_checkout_accept',
|
|
'checkout_disputing',
|
|
'disputing',
|
|
'completed',
|
|
'cancelled',
|
|
'closed',
|
|
'abnormal',
|
|
].map(value => ({ value, label: orderStatusMeta(value).label }))
|
|
|
|
function buildColumns(navigate: (id: number) => void): TableColumnsType<UserOrderListItem> {
|
|
return [
|
|
{
|
|
title: '订单',
|
|
dataIndex: 'order_no',
|
|
width: 300,
|
|
fixed: 'left',
|
|
render: (_, row) => (
|
|
<div className="pc-order-cell">
|
|
<button type="button" className="pc-order-link" onClick={() => navigate(row.id)}>
|
|
{row.order_no}
|
|
</button>
|
|
<strong>{row.title}</strong>
|
|
<span>
|
|
<Tag bordered={false}>{roleLabel(row.role)}</Tag>
|
|
<span className="pc-order-muted">{row.listing_no}</span>
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '商品',
|
|
key: 'listing',
|
|
width: 220,
|
|
render: (_, row) => (
|
|
<div className="pc-order-cell">
|
|
<span>{formatText(row.server_region)}</span>
|
|
<span className="pc-order-muted">{formatText(row.login_platform)}</span>
|
|
<span className="pc-order-muted">
|
|
{row.rental_days ? `${row.rental_days} 天` : '按账号规则'}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '金额',
|
|
key: 'amount',
|
|
width: 180,
|
|
align: 'right',
|
|
render: (_, row) => (
|
|
<div className="pc-order-amount">
|
|
<strong>{formatMoney(row.display_amount_cent)}</strong>
|
|
<span>押金 {formatMoney(row.deposit_amount_cent)}</span>
|
|
{row.deposit_waived_amount_cent > 0 ? (
|
|
<span>减免 {formatMoney(row.deposit_waived_amount_cent)}</span>
|
|
) : null}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
key: 'status',
|
|
width: 190,
|
|
render: (_, row) => (
|
|
<Space size={4} wrap>
|
|
<Tag color={orderStatusMeta(row.status).tone}>{orderStatusMeta(row.status).label}</Tag>
|
|
<Tag color={handoffStatusMeta(row.handoff_status).tone} bordered={false}>
|
|
{handoffStatusMeta(row.handoff_status).label}
|
|
</Tag>
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: '时间',
|
|
key: 'time',
|
|
width: 190,
|
|
render: (_, row) => (
|
|
<div className="pc-order-cell">
|
|
<span>下单 {formatDateTime(row.created_at)}</span>
|
|
<span className="pc-order-muted">支付截止 {formatDateTime(row.payment_deadline_at)}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '',
|
|
key: 'actions',
|
|
width: 90,
|
|
fixed: 'right',
|
|
render: (_, row) => (
|
|
<Button type="link" onClick={() => navigate(row.id)}>
|
|
详情 <RightOutlined />
|
|
</Button>
|
|
),
|
|
},
|
|
]
|
|
}
|
|
|
|
export function OrderListPage() {
|
|
const navigate = useNavigate()
|
|
const token = useSessionStore(state => state.token)
|
|
const hydrated = useSessionStore(state => state.hydrated)
|
|
const [status, setStatus] = useState('')
|
|
const [page, setPage] = useState(1)
|
|
|
|
const { data, isPending, isFetching, isError, error, refetch } = useQuery({
|
|
queryKey: ['pc-my-orders', status, page],
|
|
queryFn: () => fetchMyOrders(page, PAGE_SIZE, status),
|
|
placeholderData: keepPreviousData,
|
|
enabled: Boolean(token),
|
|
})
|
|
|
|
const columns = buildColumns(id => navigate(`/orders/${id}`))
|
|
const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录。'
|
|
|
|
if (!hydrated) {
|
|
return (
|
|
<div className="pc-orders-page">
|
|
<Card loading />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!token) {
|
|
return (
|
|
<div className="pc-orders-page">
|
|
<Card className="pc-orders-login">
|
|
<BrandLogo height={48} variant="seal" />
|
|
<Typography.Title level={3}>登录后查看我的订单</Typography.Title>
|
|
<Typography.Paragraph type="secondary">
|
|
订单、押金、交接与结账记录只对交易双方可见。
|
|
</Typography.Paragraph>
|
|
<Button
|
|
type="primary"
|
|
size="large"
|
|
onClick={() => navigate('/login?redirect=/orders')}
|
|
>
|
|
去登录
|
|
</Button>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="pc-orders-page">
|
|
<header className="pc-orders-page__head">
|
|
<div>
|
|
<span className="pc-orders-page__eyebrow">MY ORDERS</span>
|
|
<Typography.Title level={2} className="brand-title">
|
|
我的订单
|
|
</Typography.Title>
|
|
<Typography.Text type="secondary">
|
|
查看租用进度、支付、交接、结账与申诉记录。
|
|
</Typography.Text>
|
|
</div>
|
|
<Space>
|
|
<Select
|
|
allowClear
|
|
style={{ width: 190 }}
|
|
value={status || undefined}
|
|
placeholder="全部状态"
|
|
options={statusOptions}
|
|
onChange={value => {
|
|
setStatus(value ?? '')
|
|
setPage(1)
|
|
}}
|
|
/>
|
|
<Button icon={<ReloadOutlined />} onClick={() => void refetch()} loading={isFetching}>
|
|
刷新
|
|
</Button>
|
|
</Space>
|
|
</header>
|
|
|
|
{isError ? (
|
|
<Alert
|
|
type="error"
|
|
showIcon
|
|
className="pc-orders-page__alert"
|
|
message="订单没有加载出来"
|
|
description={errorText}
|
|
action={
|
|
<Button size="small" onClick={() => void refetch()}>
|
|
重试
|
|
</Button>
|
|
}
|
|
/>
|
|
) : null}
|
|
|
|
<Card className="pc-orders-card">
|
|
<Table<UserOrderListItem>
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data?.items ?? []}
|
|
loading={isPending || isFetching}
|
|
scroll={{ x: 'max-content' }}
|
|
pagination={{
|
|
current: data?.page ?? page,
|
|
pageSize: data?.page_size ?? PAGE_SIZE,
|
|
total: data?.total ?? 0,
|
|
showSizeChanger: false,
|
|
showTotal: (count, range) => `第 ${range[0]}-${range[1]} 条 / 共 ${count} 条`,
|
|
onChange: next => setPage(next),
|
|
}}
|
|
locale={{
|
|
emptyText: (
|
|
<div className="pc-orders-empty">
|
|
<p>还没有订单</p>
|
|
<Link to="/#listings">去看看柜选</Link>
|
|
</div>
|
|
),
|
|
}}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|