Files
hfb_jysh/frontend/src/features/orders/OrderListPage.tsx
T

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>
)
}