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 <[email protected]>
This commit is contained in:
2026-07-11 14:54:58 +08:00
co-authored by Claude Fable 5
parent f5a7249969
commit ddf07bbf58
10 changed files with 2211 additions and 2 deletions
+188
View File
@@ -0,0 +1,188 @@
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 { ExcDemand } from '@/api/types'
import { fetchExcDemand, fetchExcDemands } 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 {
DEMAND_STATUS_LABELS,
DISPATCH_MODE_LABELS,
DemandStatusBadge,
formatAmount,
formatTime,
} from '@/features/exc/labels'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export const Route = createFileRoute('/_authed/exc/demands')({
component: ExcDemandsPage,
})
function ExcDemandsPage() {
const [status, setStatus] = useState<string>('all')
const [mode, setMode] = useState<string>('all')
const [detailId, setDetailId] = useState<number | null>(null)
const filters: Record<string, string> = {}
if (status !== 'all') filters.status = status
if (mode !== 'all') filters.dispatch_mode = mode
const list = useListPage('exc-demands', fetchExcDemands, filters)
const detail = useQuery({
queryKey: ['exc-demand', detailId],
queryFn: () => fetchExcDemand(detailId as number),
enabled: detailId !== null,
})
const demand = detail.data?.data
const columns: ColumnDef<ExcDemand>[] = [
{
header: '需求',
cell: ({ row }) => (
<Button
variant="link"
className="h-auto max-w-72 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 }) => <DemandStatusBadge status={row.original.status} />,
},
{
header: '模式',
cell: ({ row }) =>
row.original.dispatch_mode ? DISPATCH_MODE_LABELS[row.original.dispatch_mode] : '—',
},
{
header: '买家',
cell: ({ row }) => row.original.buyer?.display_name ?? '—',
},
{
header: '参考价',
cell: ({ row }) => formatAmount(row.original.ref_amount, row.original.currency_code),
},
{
header: '报价/派单',
cell: ({ row }) => `${row.original.quotes_count} / ${row.original.dispatches_count}`,
},
{
header: '匹配服务商',
cell: ({ row }) =>
row.original.matched_provider_uid ? (
<code className="font-mono text-xs">{row.original.matched_provider_uid}</code>
) : (
'—'
),
},
{
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(DEMAND_STATUS_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={mode} onValueChange={setMode}>
<SelectTrigger size="sm" className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{Object.entries(DISPATCH_MODE_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</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={demand?.title ?? `需求 #${detailId ?? ''}`}
description={demand ? `状态 ${DEMAND_STATUS_LABELS[demand.status] ?? demand.status}` : undefined}
fields={[
{ label: '摘要', value: demand?.summary ?? '—' },
{ label: '买家', value: demand?.buyer?.display_name ?? '—' },
{ label: '参考价', value: formatAmount(demand?.ref_amount, demand?.currency_code) },
{ label: '报价截止', value: formatTime(demand?.quote_deadline) },
{ label: '需求时间', value: formatTime(demand?.required_at) },
{ label: '匹配时间', value: formatTime(demand?.matched_at) },
]}
jsonBlocks={[
{ label: '关联服务', value: demand?.related_services?.length ? demand.related_services : null },
{
label: `报价(${demand?.quotes?.length ?? 0}`,
value: demand?.quotes?.length
? demand.quotes.map((q) => ({
user: q.user?.display_name ?? q.user_uid,
price: formatAmount(q.price, q.currency_code),
message: q.message,
}))
: null,
},
{
label: `派单(${demand?.dispatches?.length ?? 0}`,
value: demand?.dispatches?.length
? demand.dispatches.map((d) => ({
provider: d.provider?.display_name ?? '—',
round: d.round_no,
status: d.status,
score: d.score,
responded_at: d.responded_at,
}))
: null,
},
{ label: '地址', value: demand?.address ?? null },
]}
/>
</div>
)
}
+139
View File
@@ -0,0 +1,139 @@
import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import type { ExcDispatch } from '@/api/types'
import { fetchExcDispatches } 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 { DISPATCH_STATUS_LABELS, DispatchStatusBadge, formatTime } from '@/features/exc/labels'
import { Badge } from '@/components/ui/badge'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export const Route = createFileRoute('/_authed/exc/dispatches')({
component: ExcDispatchesPage,
})
function ExcDispatchesPage() {
const [status, setStatus] = useState<string>('all')
const [round, setRound] = useState<string>('all')
const filters: Record<string, string> = {}
if (status !== 'all') filters.status = status
if (round !== 'all') filters.round_no = round
const list = useListPage('exc-dispatches', fetchExcDispatches, filters)
const columns: ColumnDef<ExcDispatch>[] = [
{
header: '需求',
cell: ({ row }) => (
<div className="max-w-56">
<div className="truncate">{row.original.demand?.title ?? '—'}</div>
<code className="font-mono text-xs text-muted-foreground">
#{row.original.demand?.id ?? '—'}
</code>
</div>
),
},
{
header: '服务商',
cell: ({ row }) => (
<div>
<div className="text-sm">{row.original.provider?.display_name ?? '—'}</div>
<code className="font-mono text-xs text-muted-foreground">
{row.original.provider?.uid ?? ''}
</code>
</div>
),
},
{
header: '轮次',
cell: ({ row }) => <Badge variant="secondary">R{row.original.round_no}</Badge>,
},
{
header: '状态',
cell: ({ row }) => <DispatchStatusBadge status={row.original.status} />,
},
{
header: '评分',
cell: ({ row }) => (row.original.score != null ? row.original.score.toFixed(3) : '—'),
},
{
header: '距离/ETA',
cell: ({ row }) => {
const distance =
row.original.distance_meters != null
? `${(row.original.distance_meters / 1000).toFixed(1)}km`
: '—'
const eta = row.original.eta_minutes != null ? `${row.original.eta_minutes}min` : '—'
return `${distance} / ${eta}`
},
},
{
header: '自动抢单',
cell: ({ row }) => (row.original.auto_grab_eligible ? '是' : '否'),
},
{
header: '发送时间',
cell: ({ row }) => formatTime(row.original.sent_at),
},
{
header: '响应时间',
cell: ({ row }) => formatTime(row.original.responded_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(DISPATCH_STATUS_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={round} onValueChange={setRound}>
<SelectTrigger size="sm" className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="0">R0</SelectItem>
<SelectItem value="1">R1</SelectItem>
<SelectItem value="2">R2</SelectItem>
<SelectItem value="3">R3</SelectItem>
</SelectContent>
</Select>
</div>
}
/>
<DataTable
columns={columns}
data={list.rows}
pagination={list.pagination}
loading={list.loading}
emptyText="没有派单记录"
onPageChange={list.setPage}
onPageSizeChange={list.setPageSize}
/>
</div>
)
}
+197
View File
@@ -0,0 +1,197 @@
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>
)
}
+170
View File
@@ -0,0 +1,170 @@
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 { ExcProvider } from '@/api/types'
import { fetchExcProvider, fetchExcProviders } 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 {
PROVIDER_STATUS_LABELS,
ProviderStatusBadge,
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/providers')({
component: ExcProvidersPage,
})
function ExcProvidersPage() {
const [status, setStatus] = useState<string>('all')
const [verified, setVerified] = useState<string>('all')
const [detailUid, setDetailUid] = useState<string | null>(null)
const filters: Record<string, string> = {}
if (status !== 'all') filters.status = status
if (verified !== 'all') filters.is_verified = verified
const list = useListPage('exc-providers', fetchExcProviders, filters)
const detail = useQuery({
queryKey: ['exc-provider', detailUid],
queryFn: () => fetchExcProvider(detailUid as string),
enabled: detailUid !== null,
})
const provider = detail.data?.data
const columns: ColumnDef<ExcProvider>[] = [
{
header: '服务商',
cell: ({ row }) => (
<Button
variant="link"
className="h-auto justify-start p-0 text-left"
onClick={() => setDetailUid(row.original.user_uid)}
>
<div className="min-w-0">
<div className="font-medium">{row.original.user?.display_name ?? '—'}</div>
<code className="font-mono text-xs text-muted-foreground">{row.original.user_uid}</code>
</div>
</Button>
),
},
{
header: '状态',
cell: ({ row }) => <ProviderStatusBadge status={row.original.status} />,
},
{
header: '认证/禁用',
cell: ({ row }) => (
<div className="flex gap-1">
{row.original.is_verified && (
<Badge className="bg-blue-500/12 text-blue-700 dark:text-blue-400"></Badge>
)}
{row.original.is_disabled && (
<Badge className="bg-red-500/12 text-red-700 dark:text-red-400"></Badge>
)}
{!row.original.is_verified && !row.original.is_disabled && '—'}
</div>
),
},
{
header: '评分',
cell: ({ row }) => (row.original.rating != null ? row.original.rating.toFixed(1) : '—'),
},
{
header: '完成单数',
cell: ({ row }) => row.original.completed_jobs ?? 0,
},
{
header: '当前订单',
cell: ({ row }) =>
row.original.current_service_order_id ? `#${row.original.current_service_order_id}` : '—',
},
{
header: '最近定位',
cell: ({ row }) => formatTime(row.original.last_location_at),
},
{
header: '上线时间',
cell: ({ row }) => formatTime(row.original.online_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(PROVIDER_STATUS_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={verified} onValueChange={setVerified}>
<SelectTrigger size="sm" className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="1"></SelectItem>
<SelectItem value="0"></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={detailUid !== null}
onOpenChange={(open) => !open && setDetailUid(null)}
title={provider?.user?.display_name ?? detailUid ?? ''}
description={provider ? `状态 ${PROVIDER_STATUS_LABELS[provider.status] ?? provider.status}` : undefined}
fields={[
{ label: 'UID', value: <code className="font-mono text-xs">{provider?.user_uid}</code> },
{ label: '评分', value: provider?.rating != null ? provider.rating.toFixed(1) : '—' },
{ label: '完成单数', value: provider?.completed_jobs ?? 0 },
{ label: '已认证', value: provider?.is_verified ? '是' : '否' },
{ label: '已禁用', value: provider?.is_disabled ? '是' : '否' },
{ label: '当前订单', value: provider?.current_service_order_id ? `#${provider.current_service_order_id}` : '—' },
{ label: '上线时间', value: formatTime(provider?.online_at) },
{ label: '离线时间', value: formatTime(provider?.offline_at) },
{ label: '最近定位', value: formatTime(provider?.last_location_at) },
]}
jsonBlocks={[
{ label: '派单设置', value: provider?.dispatch_setting ?? null },
{ label: '状态日志(最近10条)', value: provider?.status_logs?.length ? provider.status_logs : null },
]}
/>
</div>
)
}