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>
This commit is contained in:
40
src/api/modules/exc.ts
Normal file
40
src/api/modules/exc.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { api } from '@/api/client'
|
||||
import type {
|
||||
ApiResponse,
|
||||
ExcDemand,
|
||||
ExcDispatch,
|
||||
ExcOrder,
|
||||
ExcProvider,
|
||||
ListParams,
|
||||
PaginatedResponse,
|
||||
} from '@/api/types'
|
||||
|
||||
const BASE = '/api/admin/v1/exc'
|
||||
|
||||
export function fetchExcDemands(params: ListParams) {
|
||||
return api.get<PaginatedResponse<ExcDemand>>(`${BASE}/demands`, params)
|
||||
}
|
||||
|
||||
export function fetchExcDemand(id: number) {
|
||||
return api.get<ApiResponse<ExcDemand>>(`${BASE}/demands/${id}`)
|
||||
}
|
||||
|
||||
export function fetchExcDispatches(params: ListParams) {
|
||||
return api.get<PaginatedResponse<ExcDispatch>>(`${BASE}/dispatches`, params)
|
||||
}
|
||||
|
||||
export function fetchExcOrders(params: ListParams) {
|
||||
return api.get<PaginatedResponse<ExcOrder>>(`${BASE}/orders`, params)
|
||||
}
|
||||
|
||||
export function fetchExcOrder(id: number) {
|
||||
return api.get<ApiResponse<ExcOrder>>(`${BASE}/orders/${id}`)
|
||||
}
|
||||
|
||||
export function fetchExcProviders(params: ListParams) {
|
||||
return api.get<PaginatedResponse<ExcProvider>>(`${BASE}/providers`, params)
|
||||
}
|
||||
|
||||
export function fetchExcProvider(uid: string) {
|
||||
return api.get<ApiResponse<ExcProvider>>(`${BASE}/providers/${uid}`)
|
||||
}
|
||||
1075
src/api/types.gen.ts
1075
src/api/types.gen.ts
File diff suppressed because it is too large
Load Diff
184
src/api/types.ts
184
src/api/types.ts
@ -351,3 +351,187 @@ export interface RetranslateResult {
|
||||
translation_version_id: number
|
||||
queued_blocks: number
|
||||
}
|
||||
|
||||
// --- Exc 专长交易(只读监控)---
|
||||
|
||||
export interface ExcUser {
|
||||
uid: string
|
||||
display_name?: string | null
|
||||
}
|
||||
|
||||
export type ExcDemandStatus =
|
||||
| 'draft'
|
||||
| 'open'
|
||||
| 'quotation_closed'
|
||||
| 'order_placed'
|
||||
| 'fulfilled'
|
||||
| 'broadcasting'
|
||||
| 'matched'
|
||||
| 'converted'
|
||||
| 'expired'
|
||||
| 'canceled'
|
||||
|
||||
export type ExcDispatchMode = 'instant_accept' | 'quote' | 'scheduled_booking' | 'manual_selection'
|
||||
|
||||
export interface ExcDemand {
|
||||
kind?: string
|
||||
id: number
|
||||
title?: string | null
|
||||
status: ExcDemandStatus
|
||||
dispatch_mode?: ExcDispatchMode | null
|
||||
service_type?: string | null
|
||||
buyer?: ExcUser | null
|
||||
category_id?: string | null
|
||||
currency_code?: string | null
|
||||
ref_amount?: number | null
|
||||
matched_provider_uid?: string | null
|
||||
matched_at?: string | null
|
||||
quotes_count: number
|
||||
dispatches_count: number
|
||||
required_at?: string | null
|
||||
published_at?: string | null
|
||||
created_at?: string
|
||||
// detail-only
|
||||
summary?: string | null
|
||||
content?: string | null
|
||||
address?: unknown
|
||||
quote_deadline?: string | null
|
||||
related_services?: Array<{ id: number; title?: string | null; category_name?: string | null }>
|
||||
quotes?: Array<{
|
||||
id: number
|
||||
user_uid: string
|
||||
price: number
|
||||
currency_code?: string
|
||||
message?: string | null
|
||||
user?: ExcUser | null
|
||||
created_at?: string
|
||||
}>
|
||||
dispatches?: ExcDemandDispatchRow[]
|
||||
}
|
||||
|
||||
export interface ExcDemandDispatchRow {
|
||||
id: number
|
||||
provider?: ExcUser | null
|
||||
round_no: number
|
||||
status: ExcDispatchStatus
|
||||
score?: number | null
|
||||
distance_meters?: number | null
|
||||
eta_minutes?: number | null
|
||||
responded_at?: string | null
|
||||
}
|
||||
|
||||
export type ExcDispatchStatus =
|
||||
| 'sent'
|
||||
| 'delivered'
|
||||
| 'viewed'
|
||||
| 'accepted'
|
||||
| 'rejected'
|
||||
| 'ignored'
|
||||
| 'expired'
|
||||
| 'taken_by_other'
|
||||
|
||||
export interface ExcDispatch {
|
||||
kind?: string
|
||||
id: number
|
||||
demand?: { id: number; title?: string | null } | null
|
||||
provider?: ExcUser | null
|
||||
round_no: number
|
||||
status: ExcDispatchStatus
|
||||
score?: number | null
|
||||
similarity?: number | null
|
||||
distance_meters?: number | null
|
||||
eta_minutes?: number | null
|
||||
auto_grab_eligible: boolean
|
||||
sent_at?: string | null
|
||||
viewed_at?: string | null
|
||||
responded_at?: string | null
|
||||
expires_at?: string | null
|
||||
}
|
||||
|
||||
export type ExcOrderStatus =
|
||||
| 'created'
|
||||
| 'confirmed'
|
||||
| 'started'
|
||||
| 'consumed'
|
||||
| 'paid'
|
||||
| 'fulfilled'
|
||||
| 'canceled'
|
||||
|
||||
export interface ExcOrder {
|
||||
kind?: string
|
||||
id: number
|
||||
title?: string | null
|
||||
status: ExcOrderStatus
|
||||
payment_status?: 'UNPAID' | 'PAID' | null
|
||||
buyer?: ExcUser | null
|
||||
provider?: ExcUser | null
|
||||
currency_code?: string | null
|
||||
total_amount?: number | null
|
||||
actual_total_amount?: number | null
|
||||
platform_fee_amount?: number | null
|
||||
provider_net_amount?: number | null
|
||||
commission_rate?: number | null
|
||||
cancellation_fee_amount?: number | null
|
||||
paid_at?: string | null
|
||||
settled_at?: string | null
|
||||
escrow_release_at?: string | null
|
||||
escrow_released_at?: string | null
|
||||
order_date?: string | null
|
||||
created_at?: string
|
||||
// detail-only
|
||||
buyer_remarks?: string | null
|
||||
pay_order_no?: string | null
|
||||
paid_channel?: string | null
|
||||
items?: Array<{
|
||||
id: number
|
||||
name?: string | null
|
||||
status?: string
|
||||
price?: number
|
||||
quantity?: number
|
||||
line_total?: number
|
||||
service_type?: string
|
||||
}>
|
||||
transactions?: Array<{
|
||||
id: number
|
||||
previous_status?: string | null
|
||||
new_status?: string
|
||||
changed_by?: string | null
|
||||
changed_by_type?: string | null
|
||||
reason?: string | null
|
||||
changed_at?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type ExcProviderStatus = 'offline' | 'online' | 'inactive' | 'busy'
|
||||
|
||||
export interface ExcProvider {
|
||||
kind?: string
|
||||
user?: ExcUser | null
|
||||
user_uid: string
|
||||
status: ExcProviderStatus
|
||||
is_disabled: boolean
|
||||
is_verified: boolean
|
||||
rating?: number | null
|
||||
completed_jobs?: number | null
|
||||
online_at?: string | null
|
||||
offline_at?: string | null
|
||||
last_location_at?: string | null
|
||||
current_service_order_id?: number | null
|
||||
created_at?: string
|
||||
// detail-only
|
||||
dispatch_setting?: {
|
||||
auto_grab_enabled?: boolean
|
||||
service_whitelist?: string[]
|
||||
min_price?: number | null
|
||||
max_radius_meters?: number | null
|
||||
max_eta_minutes?: number | null
|
||||
daily_accept_cap?: number | null
|
||||
accepted_today?: number
|
||||
} | null
|
||||
status_logs?: Array<{
|
||||
from_status?: string | null
|
||||
to_status?: string
|
||||
reason?: string | null
|
||||
created_at?: string
|
||||
}>
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import {
|
||||
Bot,
|
||||
ClipboardList,
|
||||
Flag,
|
||||
FolderTree,
|
||||
Hash,
|
||||
@ -12,9 +13,12 @@ import {
|
||||
MessageSquareWarning,
|
||||
Newspaper,
|
||||
Palette,
|
||||
Receipt,
|
||||
ScrollText,
|
||||
Send,
|
||||
Smartphone,
|
||||
Sparkles,
|
||||
UserCheck,
|
||||
} from 'lucide-react'
|
||||
|
||||
export interface NavItem {
|
||||
@ -55,6 +59,15 @@ export const NAV_GROUPS: NavGroup[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '专长交易',
|
||||
items: [
|
||||
{ label: '需求监控', to: '/exc/demands', icon: ClipboardList },
|
||||
{ label: '派单监控', to: '/exc/dispatches', icon: Send },
|
||||
{ label: '订单监控', to: '/exc/orders', icon: Receipt },
|
||||
{ label: '服务商监控', to: '/exc/providers', icon: UserCheck },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '内容审核',
|
||||
items: [
|
||||
|
||||
123
src/features/exc/labels.tsx
Normal file
123
src/features/exc/labels.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import type {
|
||||
ExcDemandStatus,
|
||||
ExcDispatchMode,
|
||||
ExcDispatchStatus,
|
||||
ExcOrderStatus,
|
||||
ExcProviderStatus,
|
||||
} from '@/api/types'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
export const DEMAND_STATUS_LABELS: Record<ExcDemandStatus, string> = {
|
||||
draft: '草稿',
|
||||
open: '开放中',
|
||||
quotation_closed: '报价截止',
|
||||
order_placed: '已下单',
|
||||
fulfilled: '已完成',
|
||||
broadcasting: '派单中',
|
||||
matched: '已匹配',
|
||||
converted: '已转单',
|
||||
expired: '已过期',
|
||||
canceled: '已取消',
|
||||
}
|
||||
|
||||
export const DISPATCH_MODE_LABELS: Record<ExcDispatchMode, string> = {
|
||||
instant_accept: '即时抢单',
|
||||
quote: '报价',
|
||||
scheduled_booking: '预约',
|
||||
manual_selection: '手动指派',
|
||||
}
|
||||
|
||||
export const DISPATCH_STATUS_LABELS: Record<ExcDispatchStatus, string> = {
|
||||
sent: '已发送',
|
||||
delivered: '已送达',
|
||||
viewed: '已查看',
|
||||
accepted: '已接单',
|
||||
rejected: '已拒绝',
|
||||
ignored: '未响应',
|
||||
expired: '已过期',
|
||||
taken_by_other: '他人接单',
|
||||
}
|
||||
|
||||
export const ORDER_STATUS_LABELS: Record<ExcOrderStatus, string> = {
|
||||
created: '已创建',
|
||||
confirmed: '已确认',
|
||||
started: '服务中',
|
||||
consumed: '已消费',
|
||||
paid: '已支付',
|
||||
fulfilled: '已完成',
|
||||
canceled: '已取消',
|
||||
}
|
||||
|
||||
export const PROVIDER_STATUS_LABELS: Record<ExcProviderStatus, string> = {
|
||||
offline: '离线',
|
||||
online: '在线',
|
||||
inactive: '不活跃',
|
||||
busy: '忙碌',
|
||||
}
|
||||
|
||||
const POSITIVE = 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-400'
|
||||
const ACTIVE = 'bg-blue-500/12 text-blue-700 dark:text-blue-400'
|
||||
const WARN = 'bg-amber-500/12 text-amber-700 dark:text-amber-400'
|
||||
const NEGATIVE = 'bg-red-500/12 text-red-700 dark:text-red-400'
|
||||
|
||||
const DEMAND_STATUS_CLASSES: Partial<Record<ExcDemandStatus, string>> = {
|
||||
open: ACTIVE,
|
||||
broadcasting: WARN,
|
||||
matched: POSITIVE,
|
||||
converted: POSITIVE,
|
||||
order_placed: POSITIVE,
|
||||
fulfilled: POSITIVE,
|
||||
expired: NEGATIVE,
|
||||
canceled: NEGATIVE,
|
||||
}
|
||||
|
||||
const DISPATCH_STATUS_CLASSES: Partial<Record<ExcDispatchStatus, string>> = {
|
||||
accepted: POSITIVE,
|
||||
rejected: NEGATIVE,
|
||||
expired: NEGATIVE,
|
||||
taken_by_other: WARN,
|
||||
}
|
||||
|
||||
const ORDER_STATUS_CLASSES: Partial<Record<ExcOrderStatus, string>> = {
|
||||
started: ACTIVE,
|
||||
paid: POSITIVE,
|
||||
fulfilled: POSITIVE,
|
||||
canceled: NEGATIVE,
|
||||
}
|
||||
|
||||
const PROVIDER_STATUS_CLASSES: Partial<Record<ExcProviderStatus, string>> = {
|
||||
online: POSITIVE,
|
||||
busy: WARN,
|
||||
inactive: NEGATIVE,
|
||||
}
|
||||
|
||||
function statusBadge(label: string, className?: string) {
|
||||
return className ? <Badge className={className}>{label}</Badge> : <Badge variant="secondary">{label}</Badge>
|
||||
}
|
||||
|
||||
export function DemandStatusBadge({ status }: { status: ExcDemandStatus }) {
|
||||
return statusBadge(DEMAND_STATUS_LABELS[status] ?? status, DEMAND_STATUS_CLASSES[status])
|
||||
}
|
||||
|
||||
export function DispatchStatusBadge({ status }: { status: ExcDispatchStatus }) {
|
||||
return statusBadge(DISPATCH_STATUS_LABELS[status] ?? status, DISPATCH_STATUS_CLASSES[status])
|
||||
}
|
||||
|
||||
export function OrderStatusBadge({ status }: { status: ExcOrderStatus }) {
|
||||
return statusBadge(ORDER_STATUS_LABELS[status] ?? status, ORDER_STATUS_CLASSES[status])
|
||||
}
|
||||
|
||||
export function ProviderStatusBadge({ status }: { status: ExcProviderStatus }) {
|
||||
return statusBadge(PROVIDER_STATUS_LABELS[status] ?? status, PROVIDER_STATUS_CLASSES[status])
|
||||
}
|
||||
|
||||
/** Amounts are stored in cents. */
|
||||
export function formatAmount(cents?: number | null, currency?: string | null): string {
|
||||
if (cents == null) return '—'
|
||||
const value = (cents / 100).toFixed(2)
|
||||
return currency === 'CNY' || !currency ? `¥${value}` : `${currency} ${value}`
|
||||
}
|
||||
|
||||
export function formatTime(value?: string | null): string {
|
||||
return value ? new Date(value).toLocaleString('zh-CN') : '—'
|
||||
}
|
||||
@ -27,6 +27,10 @@ import { Route as AuthedStudioRobotAuthorsRouteImport } from './routes/_authed/s
|
||||
import { Route as AuthedStudioCategoryBriefsRouteImport } from './routes/_authed/studio/category-briefs'
|
||||
import { Route as AuthedStudioArticlesRouteImport } from './routes/_authed/studio/articles'
|
||||
import { Route as AuthedStudioAgentTokensRouteImport } from './routes/_authed/studio/agent-tokens'
|
||||
import { Route as AuthedExcProvidersRouteImport } from './routes/_authed/exc/providers'
|
||||
import { Route as AuthedExcOrdersRouteImport } from './routes/_authed/exc/orders'
|
||||
import { Route as AuthedExcDispatchesRouteImport } from './routes/_authed/exc/dispatches'
|
||||
import { Route as AuthedExcDemandsRouteImport } from './routes/_authed/exc/demands'
|
||||
import { Route as AuthedCategoriesPendingRouteImport } from './routes/_authed/categories/pending'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
@ -122,6 +126,26 @@ const AuthedStudioAgentTokensRoute = AuthedStudioAgentTokensRouteImport.update({
|
||||
path: '/studio/agent-tokens',
|
||||
getParentRoute: () => AuthedRoute,
|
||||
} as any)
|
||||
const AuthedExcProvidersRoute = AuthedExcProvidersRouteImport.update({
|
||||
id: '/exc/providers',
|
||||
path: '/exc/providers',
|
||||
getParentRoute: () => AuthedRoute,
|
||||
} as any)
|
||||
const AuthedExcOrdersRoute = AuthedExcOrdersRouteImport.update({
|
||||
id: '/exc/orders',
|
||||
path: '/exc/orders',
|
||||
getParentRoute: () => AuthedRoute,
|
||||
} as any)
|
||||
const AuthedExcDispatchesRoute = AuthedExcDispatchesRouteImport.update({
|
||||
id: '/exc/dispatches',
|
||||
path: '/exc/dispatches',
|
||||
getParentRoute: () => AuthedRoute,
|
||||
} as any)
|
||||
const AuthedExcDemandsRoute = AuthedExcDemandsRouteImport.update({
|
||||
id: '/exc/demands',
|
||||
path: '/exc/demands',
|
||||
getParentRoute: () => AuthedRoute,
|
||||
} as any)
|
||||
const AuthedCategoriesPendingRoute = AuthedCategoriesPendingRouteImport.update({
|
||||
id: '/categories/pending',
|
||||
path: '/categories/pending',
|
||||
@ -139,6 +163,10 @@ export interface FileRoutesByFullPath {
|
||||
'/robots': typeof AuthedRobotsRoute
|
||||
'/sid': typeof AuthedSidRoute
|
||||
'/categories/pending': typeof AuthedCategoriesPendingRoute
|
||||
'/exc/demands': typeof AuthedExcDemandsRoute
|
||||
'/exc/dispatches': typeof AuthedExcDispatchesRoute
|
||||
'/exc/orders': typeof AuthedExcOrdersRoute
|
||||
'/exc/providers': typeof AuthedExcProvidersRoute
|
||||
'/studio/agent-tokens': typeof AuthedStudioAgentTokensRoute
|
||||
'/studio/articles': typeof AuthedStudioArticlesRoute
|
||||
'/studio/category-briefs': typeof AuthedStudioCategoryBriefsRoute
|
||||
@ -159,6 +187,10 @@ export interface FileRoutesByTo {
|
||||
'/sid': typeof AuthedSidRoute
|
||||
'/': typeof AuthedIndexRoute
|
||||
'/categories/pending': typeof AuthedCategoriesPendingRoute
|
||||
'/exc/demands': typeof AuthedExcDemandsRoute
|
||||
'/exc/dispatches': typeof AuthedExcDispatchesRoute
|
||||
'/exc/orders': typeof AuthedExcOrdersRoute
|
||||
'/exc/providers': typeof AuthedExcProvidersRoute
|
||||
'/studio/agent-tokens': typeof AuthedStudioAgentTokensRoute
|
||||
'/studio/articles': typeof AuthedStudioArticlesRoute
|
||||
'/studio/category-briefs': typeof AuthedStudioCategoryBriefsRoute
|
||||
@ -181,6 +213,10 @@ export interface FileRoutesById {
|
||||
'/_authed/sid': typeof AuthedSidRoute
|
||||
'/_authed/': typeof AuthedIndexRoute
|
||||
'/_authed/categories/pending': typeof AuthedCategoriesPendingRoute
|
||||
'/_authed/exc/demands': typeof AuthedExcDemandsRoute
|
||||
'/_authed/exc/dispatches': typeof AuthedExcDispatchesRoute
|
||||
'/_authed/exc/orders': typeof AuthedExcOrdersRoute
|
||||
'/_authed/exc/providers': typeof AuthedExcProvidersRoute
|
||||
'/_authed/studio/agent-tokens': typeof AuthedStudioAgentTokensRoute
|
||||
'/_authed/studio/articles': typeof AuthedStudioArticlesRoute
|
||||
'/_authed/studio/category-briefs': typeof AuthedStudioCategoryBriefsRoute
|
||||
@ -203,6 +239,10 @@ export interface FileRouteTypes {
|
||||
| '/robots'
|
||||
| '/sid'
|
||||
| '/categories/pending'
|
||||
| '/exc/demands'
|
||||
| '/exc/dispatches'
|
||||
| '/exc/orders'
|
||||
| '/exc/providers'
|
||||
| '/studio/agent-tokens'
|
||||
| '/studio/articles'
|
||||
| '/studio/category-briefs'
|
||||
@ -223,6 +263,10 @@ export interface FileRouteTypes {
|
||||
| '/sid'
|
||||
| '/'
|
||||
| '/categories/pending'
|
||||
| '/exc/demands'
|
||||
| '/exc/dispatches'
|
||||
| '/exc/orders'
|
||||
| '/exc/providers'
|
||||
| '/studio/agent-tokens'
|
||||
| '/studio/articles'
|
||||
| '/studio/category-briefs'
|
||||
@ -244,6 +288,10 @@ export interface FileRouteTypes {
|
||||
| '/_authed/sid'
|
||||
| '/_authed/'
|
||||
| '/_authed/categories/pending'
|
||||
| '/_authed/exc/demands'
|
||||
| '/_authed/exc/dispatches'
|
||||
| '/_authed/exc/orders'
|
||||
| '/_authed/exc/providers'
|
||||
| '/_authed/studio/agent-tokens'
|
||||
| '/_authed/studio/articles'
|
||||
| '/_authed/studio/category-briefs'
|
||||
@ -387,6 +435,34 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthedStudioAgentTokensRouteImport
|
||||
parentRoute: typeof AuthedRoute
|
||||
}
|
||||
'/_authed/exc/providers': {
|
||||
id: '/_authed/exc/providers'
|
||||
path: '/exc/providers'
|
||||
fullPath: '/exc/providers'
|
||||
preLoaderRoute: typeof AuthedExcProvidersRouteImport
|
||||
parentRoute: typeof AuthedRoute
|
||||
}
|
||||
'/_authed/exc/orders': {
|
||||
id: '/_authed/exc/orders'
|
||||
path: '/exc/orders'
|
||||
fullPath: '/exc/orders'
|
||||
preLoaderRoute: typeof AuthedExcOrdersRouteImport
|
||||
parentRoute: typeof AuthedRoute
|
||||
}
|
||||
'/_authed/exc/dispatches': {
|
||||
id: '/_authed/exc/dispatches'
|
||||
path: '/exc/dispatches'
|
||||
fullPath: '/exc/dispatches'
|
||||
preLoaderRoute: typeof AuthedExcDispatchesRouteImport
|
||||
parentRoute: typeof AuthedRoute
|
||||
}
|
||||
'/_authed/exc/demands': {
|
||||
id: '/_authed/exc/demands'
|
||||
path: '/exc/demands'
|
||||
fullPath: '/exc/demands'
|
||||
preLoaderRoute: typeof AuthedExcDemandsRouteImport
|
||||
parentRoute: typeof AuthedRoute
|
||||
}
|
||||
'/_authed/categories/pending': {
|
||||
id: '/_authed/categories/pending'
|
||||
path: '/categories/pending'
|
||||
@ -407,6 +483,10 @@ interface AuthedRouteChildren {
|
||||
AuthedSidRoute: typeof AuthedSidRoute
|
||||
AuthedIndexRoute: typeof AuthedIndexRoute
|
||||
AuthedCategoriesPendingRoute: typeof AuthedCategoriesPendingRoute
|
||||
AuthedExcDemandsRoute: typeof AuthedExcDemandsRoute
|
||||
AuthedExcDispatchesRoute: typeof AuthedExcDispatchesRoute
|
||||
AuthedExcOrdersRoute: typeof AuthedExcOrdersRoute
|
||||
AuthedExcProvidersRoute: typeof AuthedExcProvidersRoute
|
||||
AuthedStudioAgentTokensRoute: typeof AuthedStudioAgentTokensRoute
|
||||
AuthedStudioArticlesRoute: typeof AuthedStudioArticlesRoute
|
||||
AuthedStudioCategoryBriefsRoute: typeof AuthedStudioCategoryBriefsRoute
|
||||
@ -427,6 +507,10 @@ const AuthedRouteChildren: AuthedRouteChildren = {
|
||||
AuthedSidRoute: AuthedSidRoute,
|
||||
AuthedIndexRoute: AuthedIndexRoute,
|
||||
AuthedCategoriesPendingRoute: AuthedCategoriesPendingRoute,
|
||||
AuthedExcDemandsRoute: AuthedExcDemandsRoute,
|
||||
AuthedExcDispatchesRoute: AuthedExcDispatchesRoute,
|
||||
AuthedExcOrdersRoute: AuthedExcOrdersRoute,
|
||||
AuthedExcProvidersRoute: AuthedExcProvidersRoute,
|
||||
AuthedStudioAgentTokensRoute: AuthedStudioAgentTokensRoute,
|
||||
AuthedStudioArticlesRoute: AuthedStudioArticlesRoute,
|
||||
AuthedStudioCategoryBriefsRoute: AuthedStudioCategoryBriefsRoute,
|
||||
|
||||
188
src/routes/_authed/exc/demands.tsx
Normal file
188
src/routes/_authed/exc/demands.tsx
Normal 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
src/routes/_authed/exc/dispatches.tsx
Normal file
139
src/routes/_authed/exc/dispatches.tsx
Normal 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
src/routes/_authed/exc/orders.tsx
Normal file
197
src/routes/_authed/exc/orders.tsx
Normal 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
src/routes/_authed/exc/providers.tsx
Normal file
170
src/routes/_authed/exc/providers.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user