feat(admin): 新增后台订单列表与详情抽屉
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Button, message, Space, Tag, Typography } from 'antd'
|
||||
import { CopyOutlined } from '@ant-design/icons'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { fetchAdminOrder } from '@/features/admin/api'
|
||||
import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
|
||||
import { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import {
|
||||
checkoutStatusMap,
|
||||
handoffModeMap,
|
||||
handoffStatusMeta,
|
||||
offlineSettlementStatusMap,
|
||||
orderStatusMap,
|
||||
readStatusMeta,
|
||||
refundStatusMap,
|
||||
settlementModeMap,
|
||||
settlementStatusMap,
|
||||
} from '@/features/admin/labels'
|
||||
import { emptyText, formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
import type { AdminOrderDetail } from '@/features/admin/types'
|
||||
|
||||
interface AdminOrderDetailDrawerProps {
|
||||
orderId: number | null
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function buildSections(order: AdminOrderDetail): DetailSection[] {
|
||||
const sections: DetailSection[] = [
|
||||
{
|
||||
key: 'overview',
|
||||
title: '订单概览',
|
||||
fields: [
|
||||
{ key: 'order_no', label: '订单号', value: <span className="admin-mono">{order.order_no}</span> },
|
||||
{
|
||||
key: 'status',
|
||||
label: '订单状态',
|
||||
value: <StatusTag meta={readStatusMeta(orderStatusMap, order.status)} />,
|
||||
},
|
||||
{ key: 'created_at', label: '创建时间', value: formatDateTime(order.created_at) },
|
||||
{ key: 'updated_at', label: '更新时间', value: formatDateTime(order.updated_at) },
|
||||
{ key: 'payment_deadline', label: '支付截止', value: formatDateTime(order.payment_deadline_at) },
|
||||
{ key: 'listing_no', label: '商品编号', value: formatText(order.listing_no) },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'account',
|
||||
title: '商品与账号',
|
||||
fields: [
|
||||
{ key: 'title', label: '商品标题', value: formatText(order.title), span: 2 },
|
||||
{ key: 'server_region', label: '区服', value: formatText(order.server_region) },
|
||||
{ key: 'login_platform', label: '登录平台', value: formatText(order.login_platform) },
|
||||
{ key: 'account_source', label: '账号来源', value: formatText(order.account_source) },
|
||||
{
|
||||
key: 'duration',
|
||||
label: '预计租期',
|
||||
value: order.estimated_duration_hours ? `${order.estimated_duration_hours} 小时` : emptyText,
|
||||
},
|
||||
{ key: 'estimated_end', label: '预计结束', value: formatDateTime(order.estimated_end_at) },
|
||||
{ key: 'rented_at', label: '起租时间', value: formatDateTime(order.rented_at) },
|
||||
{
|
||||
key: 'distribution_site',
|
||||
label: '来源分站',
|
||||
value: order.distribution_site_id ? `#${order.distribution_site_id}` : '主站',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'parties',
|
||||
title: '交易双方',
|
||||
fields: [
|
||||
{
|
||||
key: 'owner',
|
||||
label: '号主',
|
||||
value: (
|
||||
<Space size={6} wrap>
|
||||
<span>{formatText(order.owner_phone)}</span>
|
||||
<Typography.Text type="secondary">ID {order.owner_id}</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'renter',
|
||||
label: '租客',
|
||||
value: (
|
||||
<Space size={6} wrap>
|
||||
<span>{formatText(order.renter_phone)}</span>
|
||||
<Typography.Text type="secondary">ID {order.renter_id}</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ key: 'track_code', label: '来源追踪码', value: formatText(order.source_track_code) },
|
||||
{ key: 'account_id', label: '账号 ID', value: formatText(order.account_id) },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'amounts',
|
||||
title: '金额明细',
|
||||
fields: [
|
||||
{ key: 'display', label: '展示金额', value: formatMoney(order.display_amount_cent) },
|
||||
{ key: 'rent', label: '租客租金', value: formatMoney(order.rent_amount_cent) },
|
||||
{ key: 'owner_rent', label: '号主租金', value: formatMoney(order.owner_rent_amount_cent) },
|
||||
{ key: 'platform_fee', label: '平台服务费', value: formatMoney(order.platform_fee_cent) },
|
||||
{ key: 'deposit', label: '实收押金', value: formatMoney(order.deposit_amount_cent) },
|
||||
{ key: 'deposit_original', label: '原始押金', value: formatMoney(order.deposit_original_amount_cent) },
|
||||
{ key: 'deposit_waived', label: '减免押金', value: formatMoney(order.deposit_waived_amount_cent) },
|
||||
{
|
||||
key: 'refund',
|
||||
label: '退款',
|
||||
value: (
|
||||
<Space size={6} wrap>
|
||||
<StatusTag meta={readStatusMeta(refundStatusMap, order.refund_status || 'none')} />
|
||||
<span>{formatMoney(order.refund_amount_cent)}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'fulfillment',
|
||||
title: '交接与结算',
|
||||
fields: [
|
||||
{
|
||||
key: 'handoff_mode',
|
||||
label: '订单类型',
|
||||
value: <StatusTag meta={readStatusMeta(handoffModeMap, order.handoff_mode)} />,
|
||||
},
|
||||
{ key: 'handoff_status', label: '交接状态', value: <StatusTag meta={handoffStatusMeta(order.handoff_status, order)} /> },
|
||||
{
|
||||
key: 'settlement_mode',
|
||||
label: '结算方式',
|
||||
value: <StatusTag meta={readStatusMeta(settlementModeMap, order.settlement_mode)} />,
|
||||
},
|
||||
{
|
||||
key: 'settlement_status',
|
||||
label: '结算状态',
|
||||
value: <StatusTag meta={readStatusMeta(settlementStatusMap, order.settlement_status)} />,
|
||||
},
|
||||
{
|
||||
key: 'offline_status',
|
||||
label: '线下结算',
|
||||
value: (
|
||||
<StatusTag
|
||||
meta={readStatusMeta(
|
||||
offlineSettlementStatusMap,
|
||||
order.offline_settlement_status || 'none'
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ key: 'offline_amount', label: '线下结算金额', value: formatMoney(order.offline_settlement_amount_cent) },
|
||||
{ key: 'offline_remark', label: '线下结算备注', value: formatText(order.offline_settlement_remark), span: 2 },
|
||||
{ key: 'offline_at', label: '线下结算时间', value: formatDateTime(order.offline_settled_at) },
|
||||
{
|
||||
key: 'deposit_hold',
|
||||
label: '押金冻结',
|
||||
value: (
|
||||
<Space size={6} wrap>
|
||||
<span>{formatText(order.deposit_hold_status)}</span>
|
||||
<span>{formatMoney(order.deposit_hold_amount_cent)}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ key: 'deposit_hold_reason', label: '冻结原因', value: formatText(order.deposit_hold_reason), span: 2 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
if (order.checkout) {
|
||||
const checkout = order.checkout
|
||||
sections.push({
|
||||
key: 'checkout',
|
||||
title: '结账单',
|
||||
fields: [
|
||||
{
|
||||
key: 'checkout_status',
|
||||
label: '结账状态',
|
||||
value: <StatusTag meta={readStatusMeta(checkoutStatusMap, checkout.status)} />,
|
||||
},
|
||||
{
|
||||
key: 'checkout_round',
|
||||
label: '协商轮次',
|
||||
value: `${checkout.round_count} / ${checkout.max_rounds}`,
|
||||
},
|
||||
{ key: 'checkout_consumable', label: '消耗品费用', value: formatMoney(checkout.consumable_amount_cent) },
|
||||
{ key: 'checkout_coin', label: '哈夫币消耗', value: `${checkout.coin_consumed_m} M` },
|
||||
{ key: 'checkout_other', label: '其他费用', value: formatMoney(checkout.other_amount_cent) },
|
||||
{ key: 'checkout_deduct', label: '押金扣除', value: formatMoney(checkout.deposit_deduct_amount_cent) },
|
||||
{ key: 'checkout_renter_refund', label: '租客可退', value: formatMoney(checkout.renter_refund_amount_cent) },
|
||||
{ key: 'checkout_owner_income', label: '号主到手', value: formatMoney(checkout.owner_income_amount_cent) },
|
||||
{
|
||||
key: 'checkout_shortfall',
|
||||
label: '差额',
|
||||
value: (
|
||||
<Space size={6} wrap>
|
||||
<span>短收 {formatMoney(checkout.shortfall_cent)}</span>
|
||||
<span>超收 {formatMoney(checkout.overshoot_amount_cent)}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ key: 'checkout_time', label: '最近更新', value: formatDateTime(checkout.updated_at) },
|
||||
{
|
||||
key: 'checkout_content',
|
||||
label: '结账内容',
|
||||
span: 2,
|
||||
value: <div className="admin-detail__pre">{checkout.content || emptyText}</div>,
|
||||
},
|
||||
{
|
||||
key: 'checkout_evidence',
|
||||
label: '凭证',
|
||||
span: 2,
|
||||
value:
|
||||
(checkout.evidence_urls?.length ?? 0) > 0 ? (
|
||||
<Space size={4} wrap>
|
||||
{checkout.evidence_urls.map((url, index) => (
|
||||
<a key={url} href={url} target="_blank" rel="noreferrer">
|
||||
<Tag>凭证 {index + 1}</Tag>
|
||||
</a>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
emptyText
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
if (order.active_dispute) {
|
||||
sections.push({
|
||||
key: 'dispute',
|
||||
title: '进行中的申诉',
|
||||
fields: [
|
||||
{ key: 'dispute_id', label: '申诉单号', value: `#${order.active_dispute.id}` },
|
||||
{ key: 'dispute_type', label: '申诉类型', value: formatText(order.active_dispute.type) },
|
||||
{ key: 'dispute_status', label: '状态', value: formatText(order.active_dispute.status) },
|
||||
{
|
||||
key: 'dispute_initiator',
|
||||
label: '发起方',
|
||||
value: `${order.active_dispute.initiator_type} · ${order.active_dispute.initiator_id}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
export function AdminOrderDetailDrawer({ orderId, open, onClose }: AdminOrderDetailDrawerProps) {
|
||||
const query = useQuery({
|
||||
queryKey: ['admin-order', orderId],
|
||||
queryFn: () => fetchAdminOrder(orderId as number),
|
||||
enabled: open && Boolean(orderId),
|
||||
staleTime: 15_000,
|
||||
})
|
||||
|
||||
const order = query.data
|
||||
const errorMessage = query.error instanceof Error ? query.error.message : null
|
||||
|
||||
const copyOrderNo = async () => {
|
||||
if (!order) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(order.order_no)
|
||||
message.success('订单号已复制')
|
||||
} catch {
|
||||
message.warning('当前环境不支持剪贴板,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DetailDrawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
loading={query.isPending && Boolean(orderId)}
|
||||
error={query.isError ? errorMessage || '后台会话可能已失效,请重试。' : null}
|
||||
onRetry={() => void query.refetch()}
|
||||
title={order?.order_no ? <span className="admin-mono">{order.order_no}</span> : '订单详情'}
|
||||
subtitle={
|
||||
order ? (
|
||||
<span>
|
||||
{order.title} · 创建于 {dayjs(order.created_at).format('YYYY-MM-DD HH:mm')}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
tags={
|
||||
order ? (
|
||||
<Space size={6} wrap>
|
||||
<StatusTag meta={readStatusMeta(orderStatusMap, order.status)} />
|
||||
<StatusTag meta={handoffStatusMeta(order.handoff_status, order)} />
|
||||
<StatusTag meta={readStatusMeta(settlementStatusMap, order.settlement_status)} />
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
sections={order ? buildSections(order) : []}
|
||||
footer={
|
||||
<Space>
|
||||
<Button icon={<CopyOutlined />} onClick={() => void copyOrderNo()} disabled={!order}>
|
||||
复制订单号
|
||||
</Button>
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
|
||||
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { fetchAdminOrders } from '@/features/admin/api'
|
||||
import { AdminOrderDetailDrawer } from '@/features/admin/AdminOrderDetailDrawer'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import { DataTable } from '@/features/admin/components/DataTable'
|
||||
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
|
||||
import { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import {
|
||||
handoffModeOptions,
|
||||
handoffStatusMeta,
|
||||
handoffStatusOptions,
|
||||
offlineSettlementStatusOptions,
|
||||
orderStatusMap,
|
||||
orderStatusOptions,
|
||||
readStatusMeta,
|
||||
settlementStatusMap,
|
||||
settlementStatusOptions,
|
||||
} from '@/features/admin/labels'
|
||||
import type { AdminOrderListItem, AdminOrderQuery } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
const emptyFilters: AdminOrderQuery = {
|
||||
keyword: '',
|
||||
status: '',
|
||||
handoff_status: '',
|
||||
handoff_mode: '',
|
||||
settlement_status: '',
|
||||
offline_settlement_status: '',
|
||||
}
|
||||
|
||||
function buildColumns(
|
||||
onOpen: (row: AdminOrderListItem) => void,
|
||||
canViewDetail: boolean
|
||||
): TableColumnsType<AdminOrderListItem> {
|
||||
return [
|
||||
{
|
||||
title: '订单',
|
||||
dataIndex: 'order_no',
|
||||
width: 200,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
{canViewDetail ? (
|
||||
<button type="button" className="admin-order-link" onClick={() => onOpen(row)}>
|
||||
{row.order_no}
|
||||
</button>
|
||||
) : (
|
||||
<span className="admin-mono">{row.order_no}</span>
|
||||
)}
|
||||
<span>{formatDateTime(row.created_at)}</span>
|
||||
<span>商品 {formatText(row.listing_no || row.listing_id)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'title',
|
||||
width: 210,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<strong>{formatText(row.title)}</strong>
|
||||
<span>
|
||||
{formatText(row.server_region)} · {formatText(row.login_platform)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '租客 / 号主',
|
||||
key: 'parties',
|
||||
width: 190,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>租客 {formatText(row.renter_phone)}</span>
|
||||
{row.handoff_mode === 'platform' || row.settlement_mode === 'platform_managed' ? (
|
||||
<span className="admin-stack__accent">号主 平台代管</span>
|
||||
) : (
|
||||
<span>号主 {formatText(row.owner_phone)}</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
key: 'amount',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-amount">
|
||||
<strong>{formatMoney(row.rent_amount_cent)}</strong>
|
||||
<span>押金 {formatMoney(row.deposit_amount_cent)}</span>
|
||||
{typeof row.owner_income_amount_cent === 'number' ? (
|
||||
<span className="admin-amount__accent">
|
||||
到手 {formatMoney(row.owner_income_amount_cent)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '订单状态',
|
||||
dataIndex: 'status',
|
||||
width: 170,
|
||||
render: value => <StatusTag meta={readStatusMeta(orderStatusMap, value)} />,
|
||||
},
|
||||
{
|
||||
title: '交接',
|
||||
key: 'handoff',
|
||||
width: 180,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack admin-stack--tiny">
|
||||
<StatusTag meta={handoffStatusMeta(row.handoff_status, row)} />
|
||||
{row.handoff_mode === 'platform' || row.settlement_mode === 'platform_managed' ? (
|
||||
<Tag color="geekblue">平台代管</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '结算',
|
||||
dataIndex: 'settlement_status',
|
||||
width: 140,
|
||||
render: value => <StatusTag meta={readStatusMeta(settlementStatusMap, value)} />,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 96,
|
||||
fixed: 'right',
|
||||
render: (_, row) =>
|
||||
canViewDetail ? (
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => onOpen(row)}>
|
||||
详情
|
||||
</Button>
|
||||
) : (
|
||||
<Typography.Text type="secondary">无详情权限</Typography.Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function AdminOrdersPage() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const initialKeyword = searchParams.get('keyword')?.trim() ?? ''
|
||||
const initialUserId = Number(searchParams.get('user_id')) || undefined
|
||||
|
||||
const [draft, setDraft] = useState<AdminOrderQuery>(() => ({
|
||||
...emptyFilters,
|
||||
keyword: initialKeyword,
|
||||
user_id: initialUserId,
|
||||
}))
|
||||
const [applied, setApplied] = useState<AdminOrderQuery>(() => ({
|
||||
...emptyFilters,
|
||||
keyword: initialKeyword,
|
||||
user_id: initialUserId,
|
||||
}))
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [detailId, setDetailId] = useState<number | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canViewDetail = Boolean(permissions?.includes('*') || permissions?.includes('order:view'))
|
||||
|
||||
const { data, isPending, isFetching, isError, error, refetch } = useQuery({
|
||||
queryKey: ['admin-orders', applied, page, pageSize],
|
||||
queryFn: () => fetchAdminOrders({ ...applied, page, page_size: pageSize }),
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
buildColumns(row => {
|
||||
setDetailId(row.id)
|
||||
setDetailOpen(true)
|
||||
}, canViewDetail),
|
||||
[canViewDetail]
|
||||
)
|
||||
|
||||
const patchFilter = (patch: Partial<AdminOrderQuery>) => {
|
||||
setDraft(current => ({ ...current, ...patch }))
|
||||
}
|
||||
|
||||
const applyFilters = () => {
|
||||
setApplied({ ...draft, keyword: draft.keyword?.trim() ?? '' })
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
setDraft(emptyFilters)
|
||||
setApplied(emptyFilters)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const hasActiveFilters = Object.entries(draft).some(
|
||||
([, value]) => value !== '' && value != null
|
||||
)
|
||||
|
||||
const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。'
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 交易"
|
||||
title="订单管理"
|
||||
description="全量订单、交接进度、金额与结算状态统一在这里核对;点击订单号查看详情。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">共 {data?.total ?? 0} 单</Tag>
|
||||
{applied.user_id ? (
|
||||
<Tag color="purple">筛选用户 #{applied.user_id}</Tag>
|
||||
) : null}
|
||||
{isFetching && !isPending ? <Tag color="processing">正在更新</Tag> : null}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void refetch()} loading={isFetching}>
|
||||
刷新数据
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
onSearch={applyFilters}
|
||||
onReset={hasActiveFilters ? resetFilters : undefined}
|
||||
loading={isFetching}
|
||||
>
|
||||
<FilterField label="关键词" width={300}>
|
||||
<Input
|
||||
allowClear
|
||||
value={draft.keyword}
|
||||
placeholder="订单号 / 商品编号 / 账号 / 手机号"
|
||||
onChange={event => patchFilter({ keyword: event.target.value })}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="订单状态" width={180}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.status || undefined}
|
||||
placeholder="全部状态"
|
||||
options={orderStatusOptions}
|
||||
onChange={value => patchFilter({ status: value ?? '' })}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="交接状态" width={180}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.handoff_status || undefined}
|
||||
placeholder="全部交接"
|
||||
options={handoffStatusOptions}
|
||||
onChange={value => patchFilter({ handoff_status: value ?? '' })}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="订单类型" width={150}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.handoff_mode || undefined}
|
||||
placeholder="全部类型"
|
||||
options={handoffModeOptions}
|
||||
onChange={value => patchFilter({ handoff_mode: value ?? '' })}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="结算状态" width={160}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.settlement_status || undefined}
|
||||
placeholder="全部结算"
|
||||
options={settlementStatusOptions}
|
||||
onChange={value => patchFilter({ settlement_status: value ?? '' })}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="线下结算" width={160}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.offline_settlement_status || undefined}
|
||||
placeholder="全部"
|
||||
options={offlineSettlementStatusOptions}
|
||||
onChange={value => patchFilter({ offline_settlement_status: value ?? '' })}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
{isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="订单列表没有加载出来"
|
||||
description={errorText}
|
||||
action={
|
||||
<Button size="small" onClick={() => void refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminOrderListItem>
|
||||
title={<span>订单列表</span>}
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
{data ? `第 ${data.page} 页 · 当前 ${data.items.length} 条` : '加载中'}
|
||||
</Typography.Text>
|
||||
}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
loading={isFetching}
|
||||
total={data?.total ?? 0}
|
||||
page={data?.page ?? page}
|
||||
pageSize={data?.page_size ?? pageSize}
|
||||
onPageChange={(nextPage, nextPageSize) => {
|
||||
setPage(nextPageSize !== pageSize ? 1 : nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
}}
|
||||
onRowClick={
|
||||
canViewDetail
|
||||
? row => {
|
||||
setDetailId(row.id)
|
||||
setDetailOpen(true)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
emptyDescription="没有匹配的订单,换个筛选条件试试"
|
||||
/>
|
||||
|
||||
{canViewDetail ? (
|
||||
<AdminOrderDetailDrawer
|
||||
orderId={detailId}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import type { ApiResponse } from '@/shared/api/types'
|
||||
import type {
|
||||
AdminCaptcha,
|
||||
AdminLoginData,
|
||||
AdminOrderDetail,
|
||||
AdminOrderPage,
|
||||
AdminOrderQuery,
|
||||
AdminUser,
|
||||
DashboardData,
|
||||
} from '@/features/admin/types'
|
||||
@@ -47,3 +50,26 @@ export async function fetchAdminDashboard() {
|
||||
const { data } = await adminClient.get<ApiResponse<DashboardData>>('/admin/dashboard')
|
||||
return data.data
|
||||
}
|
||||
|
||||
function compactParams(query: AdminOrderQuery) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(query).filter(([, value]) => value !== '' && value != null)
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchAdminOrders(query: AdminOrderQuery) {
|
||||
const { data } = await adminClient.get<ApiResponse<Partial<AdminOrderPage>>>('/admin/orders', {
|
||||
params: compactParams(query),
|
||||
})
|
||||
return {
|
||||
items: data.data.items ?? [],
|
||||
total: data.data.total ?? 0,
|
||||
page: data.data.page ?? query.page ?? 1,
|
||||
page_size: data.data.page_size ?? query.page_size ?? 20,
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAdminOrder(id: number | string) {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminOrderDetail>>(`/admin/orders/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Typography } from 'antd'
|
||||
|
||||
interface AdminPageHeaderProps {
|
||||
eyebrow?: string
|
||||
title: string
|
||||
description?: ReactNode
|
||||
meta?: ReactNode
|
||||
extra?: ReactNode
|
||||
}
|
||||
|
||||
/** 后台页面统一抬头:眉标 + 标题 + 说明 + 右侧操作。 */
|
||||
export function AdminPageHeader({
|
||||
eyebrow = 'JIEYOU OPS',
|
||||
title,
|
||||
description,
|
||||
meta,
|
||||
extra,
|
||||
}: AdminPageHeaderProps) {
|
||||
return (
|
||||
<header className="admin-page__head">
|
||||
<div className="admin-page__head-main">
|
||||
<Typography.Text className="admin-page__eyebrow">{eyebrow}</Typography.Text>
|
||||
<Typography.Title level={2} className="brand-title">
|
||||
{title}
|
||||
</Typography.Title>
|
||||
{description ? (
|
||||
<Typography.Text type="secondary" className="admin-page__desc">
|
||||
{description}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{meta ? <div className="admin-page__meta">{meta}</div> : null}
|
||||
</div>
|
||||
{extra ? <div className="admin-page__actions">{extra}</div> : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Card, Empty, Table, type TableColumnsType, type TableProps } from 'antd'
|
||||
|
||||
interface DataTableProps<T extends object> {
|
||||
title?: ReactNode
|
||||
extra?: ReactNode
|
||||
columns: TableColumnsType<T>
|
||||
dataSource: T[]
|
||||
rowKey: TableProps<T>['rowKey']
|
||||
loading?: boolean
|
||||
total?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
onPageChange?: (page: number, pageSize: number) => void
|
||||
onRowClick?: (row: T) => void
|
||||
emptyDescription?: ReactNode
|
||||
scroll?: TableProps<T>['scroll']
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台表格原型:统一卡片容器、空态、分页文案和行点击样式。
|
||||
* 列定义仍由页面提供,保证每页可以按业务定制。
|
||||
*/
|
||||
export function DataTable<T extends object>({
|
||||
title,
|
||||
extra,
|
||||
columns,
|
||||
dataSource,
|
||||
rowKey,
|
||||
loading,
|
||||
total = 0,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
onPageChange,
|
||||
onRowClick,
|
||||
emptyDescription = '没有符合条件的数据',
|
||||
scroll = { x: 'max-content' },
|
||||
className,
|
||||
}: DataTableProps<T>) {
|
||||
return (
|
||||
<Card className={['admin-table-card', className].filter(Boolean).join(' ')} title={title} extra={extra}>
|
||||
<Table<T>
|
||||
rowKey={rowKey}
|
||||
size="middle"
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={loading}
|
||||
scroll={scroll}
|
||||
onRow={
|
||||
onRowClick
|
||||
? row => ({
|
||||
onClick: () => onRowClick(row),
|
||||
className: 'admin-table__row--clickable',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyDescription} />,
|
||||
}}
|
||||
pagination={
|
||||
onPageChange
|
||||
? {
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50'],
|
||||
showTotal: (count, range) => `第 ${range[0]}-${range[1]} 条 / 共 ${count} 条`,
|
||||
onChange: onPageChange,
|
||||
}
|
||||
: false
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Alert, Button, Descriptions, Drawer, Empty, Skeleton } from 'antd'
|
||||
|
||||
export interface DetailField {
|
||||
key: string
|
||||
label: ReactNode
|
||||
value: ReactNode
|
||||
span?: number
|
||||
}
|
||||
|
||||
export interface DetailSection {
|
||||
key: string
|
||||
title: string
|
||||
icon?: ReactNode
|
||||
extra?: ReactNode
|
||||
fields: DetailField[]
|
||||
}
|
||||
|
||||
interface DetailDrawerProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
title: ReactNode
|
||||
subtitle?: ReactNode
|
||||
tags?: ReactNode
|
||||
loading?: boolean
|
||||
error?: string | null
|
||||
onRetry?: () => void
|
||||
sections?: DetailSection[]
|
||||
footer?: ReactNode
|
||||
width?: number
|
||||
emptyDescription?: ReactNode
|
||||
}
|
||||
|
||||
/** 详情抽屉原型:统一标题区、段落标题、描述列表和加载/错误态。 */
|
||||
export function DetailDrawer({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
subtitle,
|
||||
tags,
|
||||
loading,
|
||||
error,
|
||||
onRetry,
|
||||
sections = [],
|
||||
footer,
|
||||
width = 760,
|
||||
emptyDescription = '暂无可展示的详情',
|
||||
}: DetailDrawerProps) {
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
width={width}
|
||||
footer={footer}
|
||||
destroyOnHidden
|
||||
className="admin-detail-drawer"
|
||||
title={
|
||||
<div className="admin-detail-drawer__head">
|
||||
<div className="admin-detail-drawer__title">{title}</div>
|
||||
{subtitle ? <div className="admin-detail-drawer__subtitle">{subtitle}</div> : null}
|
||||
{tags ? <div className="admin-detail-drawer__tags">{tags}</div> : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<Skeleton active paragraph={{ rows: 12 }} />
|
||||
) : error ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message="详情没有加载出来"
|
||||
description={error}
|
||||
action={
|
||||
onRetry ? (
|
||||
<Button size="small" onClick={onRetry}>
|
||||
重试
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
) : sections.length === 0 ? (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyDescription} />
|
||||
) : (
|
||||
<div className="admin-detail">
|
||||
{sections.map(section => (
|
||||
<section className="admin-detail__section" key={section.key}>
|
||||
<div className="admin-detail__section-head">
|
||||
<h4>
|
||||
{section.icon}
|
||||
{section.title}
|
||||
</h4>
|
||||
{section.extra}
|
||||
</div>
|
||||
<Descriptions
|
||||
size="small"
|
||||
colon={false}
|
||||
column={2}
|
||||
items={section.fields.map(field => ({
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
children: field.value,
|
||||
span: field.span,
|
||||
}))}
|
||||
/>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Button, Flex } from 'antd'
|
||||
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
|
||||
interface FilterBarProps {
|
||||
children: ReactNode
|
||||
onSearch?: () => void
|
||||
onReset?: () => void
|
||||
loading?: boolean
|
||||
extra?: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页筛选条原型:字段区自适应换行,操作区固定在右侧。
|
||||
* 后续 40+ 个后台列表页复用它,避免每页各写一套间距与按钮顺序。
|
||||
*/
|
||||
export function FilterBar({ children, onSearch, onReset, loading, extra }: FilterBarProps) {
|
||||
return (
|
||||
<form
|
||||
className="admin-filter-bar"
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
onSearch?.()
|
||||
}}
|
||||
>
|
||||
<div className="admin-filter-bar__fields">{children}</div>
|
||||
<Flex className="admin-filter-bar__actions" gap={8} align="flex-end">
|
||||
{extra}
|
||||
{onReset ? (
|
||||
<Button icon={<ReloadOutlined />} onClick={onReset} disabled={loading}>
|
||||
重置
|
||||
</Button>
|
||||
) : null}
|
||||
{onSearch ? (
|
||||
<Button type="primary" htmlType="submit" icon={<SearchOutlined />} loading={loading}>
|
||||
查询
|
||||
</Button>
|
||||
) : null}
|
||||
</Flex>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
interface FilterFieldProps {
|
||||
label: string
|
||||
children: ReactNode
|
||||
width?: number
|
||||
}
|
||||
|
||||
export function FilterField({ label, children, width }: FilterFieldProps) {
|
||||
return (
|
||||
<div className="admin-filter-field" style={width ? { width } : undefined}>
|
||||
<span className="admin-filter-field__label">{label}</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Tag } from 'antd'
|
||||
|
||||
import type { StatusMeta } from '@/features/admin/labels'
|
||||
|
||||
interface StatusTagProps {
|
||||
meta: StatusMeta
|
||||
icon?: ReactNode
|
||||
bordered?: boolean
|
||||
}
|
||||
|
||||
export function StatusTag({ meta, icon, bordered = false }: StatusTagProps) {
|
||||
return (
|
||||
<Tag color={meta.tone} bordered={bordered} className="admin-status-tag">
|
||||
{icon}
|
||||
{meta.label}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
export type TagTone =
|
||||
| 'default'
|
||||
| 'success'
|
||||
| 'processing'
|
||||
| 'error'
|
||||
| 'warning'
|
||||
| 'gold'
|
||||
| 'blue'
|
||||
| 'cyan'
|
||||
| 'orange'
|
||||
| 'red'
|
||||
| 'purple'
|
||||
| 'geekblue'
|
||||
| 'magenta'
|
||||
| 'volcano'
|
||||
| 'green'
|
||||
|
||||
export interface StatusMeta {
|
||||
label: string
|
||||
tone: TagTone
|
||||
}
|
||||
|
||||
export interface StatusOption {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
function toOptions(map: Record<string, StatusMeta>): StatusOption[] {
|
||||
return Object.entries(map).map(([value, meta]) => ({ value, label: meta.label }))
|
||||
}
|
||||
|
||||
export const orderStatusMap: Record<string, StatusMeta> = {
|
||||
pending_payment: { label: '待支付', tone: 'gold' },
|
||||
pending_handoff: { label: '待交接', tone: 'blue' },
|
||||
renting: { label: '使用中', tone: 'cyan' },
|
||||
overdue: { label: '已逾期', tone: 'volcano' },
|
||||
pending_checkout_confirm: { label: '待号主确认结账', tone: 'orange' },
|
||||
pending_checkout_accept: { label: '待租客确认修正', tone: 'orange' },
|
||||
checkout_disputing: { label: '结账争议中', tone: 'red' },
|
||||
disputing: { label: '申诉中', tone: 'red' },
|
||||
completed: { label: '已完成', tone: 'success' },
|
||||
cancelled: { label: '租客已取消', tone: 'default' },
|
||||
closed: { label: '客服已关闭', tone: 'default' },
|
||||
abnormal: { label: '客服介入', tone: 'magenta' },
|
||||
}
|
||||
|
||||
export const handoffStatusMap: Record<string, StatusMeta> = {
|
||||
none: { label: '未开始', tone: 'default' },
|
||||
pending_owner: { label: '待号主交接', tone: 'blue' },
|
||||
owner_timeout: { label: '号主交接超时', tone: 'volcano' },
|
||||
pending_renter_confirm: { label: '待租客确认', tone: 'blue' },
|
||||
received: { label: '租客已收号', tone: 'cyan' },
|
||||
return_overdue: { label: '归还逾期', tone: 'volcano' },
|
||||
pending_owner_checkout: { label: '待号主确认结账', tone: 'orange' },
|
||||
pending_renter_checkout: { label: '待租客确认修正', tone: 'orange' },
|
||||
checkout_disputed: { label: '结账争议中', tone: 'red' },
|
||||
returned: { label: '已归还', tone: 'success' },
|
||||
owner_return_confirm_timeout: { label: '号主确认结账超时', tone: 'red' },
|
||||
owner_checkout_confirm_timeout: { label: '号主确认结账超时', tone: 'red' },
|
||||
renter_confirm_timeout: { label: '租客确认超时', tone: 'red' },
|
||||
cancelled: { label: '租客已取消', tone: 'default' },
|
||||
admin_closed: { label: '客服已关闭', tone: 'default' },
|
||||
admin_abnormal: { label: '客服介入', tone: 'magenta' },
|
||||
arbitrated: { label: '已仲裁', tone: 'purple' },
|
||||
}
|
||||
|
||||
export const settlementStatusMap: Record<string, StatusMeta> = {
|
||||
unsettled: { label: '未结算', tone: 'default' },
|
||||
pending: { label: '待结算', tone: 'gold' },
|
||||
frozen: { label: '冻结中', tone: 'magenta' },
|
||||
settled: { label: '已结算', tone: 'success' },
|
||||
refunded: { label: '已退款', tone: 'blue' },
|
||||
cancelled: { label: '结算已取消', tone: 'default' },
|
||||
closed: { label: '结算已关闭', tone: 'default' },
|
||||
disputed: { label: '争议中', tone: 'red' },
|
||||
arbitrated: { label: '已仲裁', tone: 'purple' },
|
||||
}
|
||||
|
||||
export const offlineSettlementStatusMap: Record<string, StatusMeta> = {
|
||||
none: { label: '无需线下结算', tone: 'default' },
|
||||
pending: { label: '待确认线下结算', tone: 'gold' },
|
||||
settled: { label: '已确认线下结算', tone: 'success' },
|
||||
}
|
||||
|
||||
export const handoffModeMap: Record<string, StatusMeta> = {
|
||||
owner: { label: '正常订单', tone: 'default' },
|
||||
platform: { label: '平台代管', tone: 'geekblue' },
|
||||
}
|
||||
|
||||
export const settlementModeMap: Record<string, StatusMeta> = {
|
||||
owner_wallet: { label: '号主钱包', tone: 'default' },
|
||||
platform_managed: { label: '平台代管', tone: 'geekblue' },
|
||||
}
|
||||
|
||||
export const checkoutStatusMap: Record<string, StatusMeta> = {
|
||||
submitted: { label: '待确认', tone: 'gold' },
|
||||
countered: { label: '已修正待确认', tone: 'orange' },
|
||||
accepted: { label: '已接受', tone: 'success' },
|
||||
disputed: { label: '争议中', tone: 'red' },
|
||||
settled: { label: '仲裁已结案', tone: 'purple' },
|
||||
}
|
||||
|
||||
export const refundStatusMap: Record<string, StatusMeta> = {
|
||||
none: { label: '未退款', tone: 'default' },
|
||||
pending: { label: '待退款', tone: 'gold' },
|
||||
pending_review: { label: '待客服审核退款', tone: 'orange' },
|
||||
refunding: { label: '退款中', tone: 'processing' },
|
||||
refunded: { label: '已退款', tone: 'success' },
|
||||
failed: { label: '退款失败', tone: 'red' },
|
||||
}
|
||||
|
||||
export function readStatusMeta(map: Record<string, StatusMeta>, value?: string | null): StatusMeta {
|
||||
if (!value) return { label: '—', tone: 'default' }
|
||||
return map[value] ?? { label: value, tone: 'default' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台代管订单的交接动作由客服执行,展示文案需要区别于普通号主订单。
|
||||
*/
|
||||
export function handoffStatusMeta(
|
||||
status?: string | null,
|
||||
order?: { handoff_mode?: string; settlement_mode?: string } | null
|
||||
) {
|
||||
const platformManaged =
|
||||
order?.handoff_mode === 'platform' || order?.settlement_mode === 'platform_managed'
|
||||
if (!platformManaged || !status) return readStatusMeta(handoffStatusMap, status)
|
||||
const platformCopy: Record<string, StatusMeta> = {
|
||||
pending_owner: { label: '待客服交接', tone: 'blue' },
|
||||
owner_timeout: { label: '客服交接超时', tone: 'volcano' },
|
||||
pending_owner_checkout: { label: '待客服确认结账', tone: 'orange' },
|
||||
owner_checkout_confirm_timeout: { label: '客服确认结账超时', tone: 'red' },
|
||||
}
|
||||
return platformCopy[status] ?? readStatusMeta(handoffStatusMap, status)
|
||||
}
|
||||
|
||||
export const orderStatusOptions = toOptions(orderStatusMap)
|
||||
|
||||
export const handoffStatusOptions = [
|
||||
'pending_owner',
|
||||
'owner_timeout',
|
||||
'pending_renter_confirm',
|
||||
'received',
|
||||
'return_overdue',
|
||||
'pending_owner_checkout',
|
||||
'pending_renter_checkout',
|
||||
'checkout_disputed',
|
||||
'returned',
|
||||
'admin_closed',
|
||||
'admin_abnormal',
|
||||
].map(value => ({ value, label: handoffStatusMap[value].label }))
|
||||
|
||||
export const handoffModeOptions = toOptions(handoffModeMap)
|
||||
export const settlementStatusOptions = toOptions(settlementStatusMap)
|
||||
export const offlineSettlementStatusOptions = ['pending', 'settled'].map(value => ({
|
||||
value,
|
||||
label: offlineSettlementStatusMap[value].label,
|
||||
}))
|
||||
@@ -62,3 +62,149 @@ export interface DashboardData {
|
||||
recent_orders: RecentOrder[]
|
||||
generated_at: string
|
||||
}
|
||||
|
||||
export interface AdminOrderQuery {
|
||||
page?: number
|
||||
page_size?: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
handoff_status?: string
|
||||
handoff_mode?: string
|
||||
settlement_status?: string
|
||||
offline_settlement_status?: string
|
||||
user_id?: number
|
||||
}
|
||||
|
||||
/** 列表接口只返回表格需要的字段,敏感详情必须再请求详情接口。 */
|
||||
export interface AdminOrderListItem {
|
||||
id: number
|
||||
order_no: string
|
||||
listing_id: number
|
||||
listing_no: string
|
||||
owner_phone?: string
|
||||
renter_phone?: string
|
||||
title: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rent_amount_cent: number
|
||||
deposit_amount_cent: number
|
||||
status: string
|
||||
handoff_status: string
|
||||
handoff_mode: string
|
||||
settlement_mode: string
|
||||
settlement_status: string
|
||||
created_at: string
|
||||
owner_income_amount_cent?: number | null
|
||||
}
|
||||
|
||||
export interface AdminOrderPage {
|
||||
items: AdminOrderListItem[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface AdminCheckoutInfo {
|
||||
id: number
|
||||
order_id: number
|
||||
initiated_by: number
|
||||
status: string
|
||||
round_count: number
|
||||
turn: string
|
||||
max_rounds: number
|
||||
content: string
|
||||
evidence_urls: string[]
|
||||
rent_amount_cent?: number | null
|
||||
owner_rent_amount_cent?: number | null
|
||||
platform_fee_cent?: number | null
|
||||
deposit_amount_cent: number
|
||||
consumable_amount_cent: number
|
||||
coin_consumed_m: number
|
||||
other_amount_cent: number
|
||||
deposit_deduct_amount_cent: number
|
||||
renter_refund_amount_cent?: number | null
|
||||
owner_income_amount_cent?: number | null
|
||||
shortfall_cent: number
|
||||
overshoot_amount_cent: number
|
||||
owner_adjustment_reason?: string
|
||||
owner_adjusted_at?: string | null
|
||||
renter_confirmed_at?: string | null
|
||||
renter_rejected_at?: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminActionState {
|
||||
enabled: boolean
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface AdminOrderActions {
|
||||
reset_handoff?: AdminActionState
|
||||
platform_handoff?: AdminActionState
|
||||
force_handoff?: AdminActionState
|
||||
platform_checkout_confirm?: AdminActionState
|
||||
platform_checkout_counter?: AdminActionState
|
||||
platform_checkout_dispute?: AdminActionState
|
||||
platform_offline_settlement?: AdminActionState
|
||||
}
|
||||
|
||||
export interface AdminActiveDispute {
|
||||
id: number
|
||||
initiator_id: number
|
||||
initiator_type: string
|
||||
initiator_admin_id?: number
|
||||
type: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface AdminOrderDetail {
|
||||
id: number
|
||||
order_no: string
|
||||
listing_id: number
|
||||
listing_no: string
|
||||
account_id: number
|
||||
owner_id: number
|
||||
renter_id: number
|
||||
owner_phone?: string
|
||||
renter_phone?: string
|
||||
title: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rented_at?: string | null
|
||||
estimated_duration_hours: number
|
||||
estimated_end_at?: string | null
|
||||
display_amount_cent: number
|
||||
rent_amount_cent?: number | null
|
||||
owner_rent_amount_cent?: number | null
|
||||
platform_fee_cent?: number | null
|
||||
deposit_amount_cent: number
|
||||
deposit_original_amount_cent: number
|
||||
deposit_waived_amount_cent: number
|
||||
account_snapshot?: Record<string, unknown> | null
|
||||
account_source: string
|
||||
status: string
|
||||
handoff_status: string
|
||||
handoff_mode: string
|
||||
settlement_mode: string
|
||||
settlement_status: string
|
||||
offline_settlement_status?: string
|
||||
offline_settlement_amount_cent: number
|
||||
offline_settlement_remark?: string
|
||||
offline_settled_at?: string | null
|
||||
refund_status?: string
|
||||
refund_amount_cent: number
|
||||
deposit_hold_status?: string
|
||||
deposit_hold_amount_cent: number
|
||||
deposit_hold_reason?: string
|
||||
deposit_held_at?: string | null
|
||||
deposit_hold_released_at?: string | null
|
||||
active_dispute?: AdminActiveDispute | null
|
||||
checkout?: AdminCheckoutInfo | null
|
||||
admin_actions?: AdminOrderActions | null
|
||||
payment_deadline_at?: string | null
|
||||
distribution_site_id?: number | null
|
||||
source_track_code?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user