Files
gig-admin/src/routes/_authed/exc/orders.tsx
kongkx ddf07bbf58 feat(exc): 专长交易 read-only monitoring pages
New nav group with four pages against the Exc admin API:
- /exc/demands — status/mode filters, detail sheet with related
  services, quotes and dispatch rounds
- /exc/dispatches — status/round filters
- /exc/orders — status/payment filters, detail sheet with items and
  状态流转 audit trail
- /exc/providers — status/verified filters, detail sheet with dispatch
  settings and status logs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:54:58 +08:00

198 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createFileRoute } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import type { ExcOrder } from '@/api/types'
import { fetchExcOrder, fetchExcOrders } from '@/api/modules/exc'
import { useListPage } from '@/lib/list-page'
import { DataTable } from '@/components/data-table/data-table'
import { PageHeader } from '@/components/page-header'
import { DetailSheet } from '@/features/studio/detail-sheet'
import {
ORDER_STATUS_LABELS,
OrderStatusBadge,
formatAmount,
formatTime,
} from '@/features/exc/labels'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export const Route = createFileRoute('/_authed/exc/orders')({
component: ExcOrdersPage,
})
function ExcOrdersPage() {
const [status, setStatus] = useState<string>('all')
const [payment, setPayment] = useState<string>('all')
const [detailId, setDetailId] = useState<number | null>(null)
const filters: Record<string, string> = {}
if (status !== 'all') filters.status = status
if (payment !== 'all') filters.payment_status = payment
const list = useListPage('exc-orders', fetchExcOrders, filters)
const detail = useQuery({
queryKey: ['exc-order', detailId],
queryFn: () => fetchExcOrder(detailId as number),
enabled: detailId !== null,
})
const order = detail.data?.data
const columns: ColumnDef<ExcOrder>[] = [
{
header: '订单',
cell: ({ row }) => (
<Button
variant="link"
className="h-auto max-w-64 justify-start p-0 text-left"
onClick={() => setDetailId(row.original.id)}
>
<div className="min-w-0">
<div className="truncate font-medium">{row.original.title ?? '—'}</div>
<code className="font-mono text-xs text-muted-foreground">#{row.original.id}</code>
</div>
</Button>
),
},
{
header: '状态',
cell: ({ row }) => <OrderStatusBadge status={row.original.status} />,
},
{
header: '支付',
cell: ({ row }) =>
row.original.payment_status === 'PAID' ? (
<Badge className="bg-emerald-500/12 text-emerald-700 dark:text-emerald-400"></Badge>
) : (
<Badge variant="secondary"></Badge>
),
},
{
header: '买家 / 服务商',
cell: ({ row }) => (
<div className="text-sm">
<div>{row.original.buyer?.display_name ?? '—'}</div>
<div className="text-muted-foreground">{row.original.provider?.display_name ?? '—'}</div>
</div>
),
},
{
header: '金额',
cell: ({ row }) =>
formatAmount(
row.original.actual_total_amount ?? row.original.total_amount,
row.original.currency_code,
),
},
{
header: '平台费',
cell: ({ row }) => formatAmount(row.original.platform_fee_amount, row.original.currency_code),
},
{
header: '结算',
cell: ({ row }) =>
row.original.settled_at
? `已结算 ${formatTime(row.original.settled_at)}`
: row.original.escrow_released_at
? '已放款'
: row.original.escrow_release_at
? `待放款 ${formatTime(row.original.escrow_release_at)}`
: '—',
},
{
header: '下单时间',
cell: ({ row }) => formatTime(row.original.created_at),
},
]
return (
<div>
<PageHeader
title="订单监控"
description="服务订单、支付与结算进度(只读)"
actions={
<div className="flex items-center gap-2">
<Select value={status} onValueChange={setStatus}>
<SelectTrigger size="sm" className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={payment} onValueChange={setPayment}>
<SelectTrigger size="sm" className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="UNPAID"></SelectItem>
<SelectItem value="PAID"></SelectItem>
</SelectContent>
</Select>
</div>
}
/>
<DataTable
columns={columns}
data={list.rows}
pagination={list.pagination}
loading={list.loading}
emptyText="没有订单记录"
onPageChange={list.setPage}
onPageSizeChange={list.setPageSize}
/>
<DetailSheet
open={detailId !== null}
onOpenChange={(open) => !open && setDetailId(null)}
title={order?.title ?? `订单 #${detailId ?? ''}`}
description={order ? `状态 ${ORDER_STATUS_LABELS[order.status] ?? order.status}` : undefined}
fields={[
{ label: '买家', value: order?.buyer?.display_name ?? '—' },
{ label: '服务商', value: order?.provider?.display_name ?? '—' },
{ label: '订单金额', value: formatAmount(order?.total_amount, order?.currency_code) },
{ label: '实付金额', value: formatAmount(order?.actual_total_amount, order?.currency_code) },
{ label: '平台费', value: formatAmount(order?.platform_fee_amount, order?.currency_code) },
{ label: '服务商净额', value: formatAmount(order?.provider_net_amount, order?.currency_code) },
{ label: '抽成比例', value: order?.commission_rate != null ? `${(order.commission_rate * 100).toFixed(1)}%` : '—' },
{ label: '支付单号', value: order?.pay_order_no ?? '—' },
{ label: '支付渠道', value: order?.paid_channel ?? '—' },
{ label: '支付时间', value: formatTime(order?.paid_at) },
{ label: '结算时间', value: formatTime(order?.settled_at) },
{ label: '放款时间', value: formatTime(order?.escrow_released_at) },
{ label: '买家备注', value: order?.buyer_remarks ?? '—' },
]}
jsonBlocks={[
{ label: `订单项(${order?.items?.length ?? 0}`, value: order?.items?.length ? order.items : null },
{
label: '状态流转',
value: order?.transactions?.length
? order.transactions.map((t) => ({
from: t.previous_status,
to: t.new_status,
by: t.changed_by,
by_type: t.changed_by_type,
reason: t.reason,
at: t.changed_at,
}))
: null,
},
]}
/>
</div>
)
}