diff --git a/src/api/modules/exc.ts b/src/api/modules/exc.ts new file mode 100644 index 0000000..5ac3942 --- /dev/null +++ b/src/api/modules/exc.ts @@ -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>(`${BASE}/demands`, params) +} + +export function fetchExcDemand(id: number) { + return api.get>(`${BASE}/demands/${id}`) +} + +export function fetchExcDispatches(params: ListParams) { + return api.get>(`${BASE}/dispatches`, params) +} + +export function fetchExcOrders(params: ListParams) { + return api.get>(`${BASE}/orders`, params) +} + +export function fetchExcOrder(id: number) { + return api.get>(`${BASE}/orders/${id}`) +} + +export function fetchExcProviders(params: ListParams) { + return api.get>(`${BASE}/providers`, params) +} + +export function fetchExcProvider(uid: string) { + return api.get>(`${BASE}/providers/${uid}`) +} diff --git a/src/api/types.gen.ts b/src/api/types.gen.ts index 9e0e482..5b45ce8 100644 --- a/src/api/types.gen.ts +++ b/src/api/types.gen.ts @@ -629,7 +629,7 @@ export interface paths { */ language_code?: string; /** - * @example draft + * @example awaiting_publish * @enum {string} */ status?: "draft" | "awaiting_publish" | "published"; @@ -849,6 +849,1077 @@ export interface paths { patch?: never; trace?: never; }; + "/api/admin/v1/exc/demands": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** [需求监控] 需求列表 */ + get: { + parameters: { + query?: { + /** + * @description 可选。按状态过滤: draft, open, quotation_closed, order_placed, fulfilled, broadcasting, matched, converted, expired, canceled。 示例: open + * @example architecto + */ + status?: string; + /** + * @description 可选。按派单模式过滤: instant_accept, quote, scheduled_booking, manual_selection。 示例: instant_accept + * @example architecto + */ + dispatch_mode?: string; + /** + * @description 可选。按买家过滤。 示例: 01HXXXX + * @example architecto + */ + user_uid?: string; + /** + * @description 可选。按标题模糊搜索。 示例: 空调 + * @example architecto + */ + q?: string; + /** + * @description 可选。页码。 示例: 1 + * @example 16 + */ + page?: number; + /** + * @description 可选。每页数量,最大100。 示例: 20 + * @example 16 + */ + page_size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** @example architecto */ + status?: string | null; + /** + * @example instant_accept + * @enum {string|null} + */ + dispatch_mode?: "instant_accept" | "quote" | "scheduled_booking" | "manual_selection" | null; + /** @example architecto */ + user_uid?: string | null; + /** + * @description Must not be greater than 100 characters. + * @example n + */ + q?: string | null; + /** + * @description Must be at least 1. + * @example 67 + */ + page?: number | null; + /** + * @description Must be at least 1. Must not be greater than 100. + * @example 16 + */ + page_size?: number | null; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + success?: boolean; + /** + * @example [ + * { + * "kind": "service_demand", + * "id": 1, + * "title": "清洗空调", + * "status": "broadcasting", + * "dispatch_mode": "instant_accept", + * "service_type": "onsite", + * "buyer": { + * "uid": "01HXXXX", + * "display_name": "Alice" + * }, + * "category_id": "9f1c...", + * "currency_code": "CNY", + * "ref_amount": 15000, + * "matched_provider_uid": null, + * "matched_at": null, + * "quotes_count": 0, + * "dispatches_count": 6, + * "required_at": "2026-07-12T10:00:00Z", + * "published_at": "2026-07-11T09:00:00Z", + * "created_at": "2026-07-11T08:00:00Z" + * } + * ] + */ + data?: { + /** @example service_demand */ + kind?: string; + /** @example 1 */ + id?: number; + /** @example 清洗空调 */ + title?: string; + /** @example broadcasting */ + status?: string; + /** @example instant_accept */ + dispatch_mode?: string; + /** @example onsite */ + service_type?: string; + buyer?: { + /** @example 01HXXXX */ + uid?: string; + /** @example Alice */ + display_name?: string; + }; + /** @example 9f1c... */ + category_id?: string; + /** @example CNY */ + currency_code?: string; + /** @example 15000 */ + ref_amount?: number; + /** @example null */ + matched_provider_uid?: string; + /** @example null */ + matched_at?: string; + /** @example 0 */ + quotes_count?: number; + /** @example 6 */ + dispatches_count?: number; + /** @example 2026-07-12T10:00:00Z */ + required_at?: string; + /** @example 2026-07-11T09:00:00Z */ + published_at?: string; + /** @example 2026-07-11T08:00:00Z */ + created_at?: string; + }[]; + pagination?: { + /** @example 1 */ + current_page?: number; + /** @example null */ + next_page?: string; + /** @example 20 */ + page_size?: number; + /** @example null */ + prev_page?: string; + /** @example 1 */ + total_count?: number; + /** @example 1 */ + total_pages?: number; + }; + }; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example false */ + success?: boolean; + /** @example FORBIDDEN */ + code?: string; + /** @example 无权限访问 */ + message?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/admin/v1/exc/demands/{id}": { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description 需求 ID。 + * @example 1 + */ + id: number; + }; + cookie?: never; + }; + /** + * [需求监控] 需求详情 + * @description 含关联服务、报价与派单明细。 + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description 需求 ID。 + * @example 1 + */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + success?: boolean; + data?: { + /** @example service_demand */ + kind?: string; + /** @example 1 */ + id?: number; + /** @example 清洗空调 */ + title?: string; + /** @example matched */ + status?: string; + /** @example 两台挂机深度清洗 */ + summary?: string; + address?: { + /** @example 广州 */ + city?: string; + }; + /** @example null */ + quote_deadline?: string; + /** @example [] */ + related_services?: unknown[]; + /** @example [] */ + quotes?: unknown[]; + /** + * @example [ + * { + * "id": 3, + * "provider": { + * "uid": "01HYYYY", + * "display_name": "Bob" + * }, + * "round_no": 1, + * "status": "accepted", + * "score": 0.87, + * "responded_at": "2026-07-11T09:01:00Z" + * } + * ] + */ + dispatches?: { + /** @example 3 */ + id?: number; + provider?: { + /** @example 01HYYYY */ + uid?: string; + /** @example Bob */ + display_name?: string; + }; + /** @example 1 */ + round_no?: number; + /** @example accepted */ + status?: string; + /** @example 0.87 */ + score?: number; + /** @example 2026-07-11T09:01:00Z */ + responded_at?: string; + }[]; + }; + }; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example false */ + success?: boolean; + /** @example NOT_FOUND */ + code?: string; + /** @example 请求的资源未找到。 */ + message?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/admin/v1/exc/dispatches": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** [派单监控] 派单列表 */ + get: { + parameters: { + query?: { + /** + * @description 可选。按状态过滤: sent, delivered, viewed, accepted, rejected, ignored, expired, taken_by_other。 示例: accepted + * @example architecto + */ + status?: string; + /** + * @description 可选。按需求过滤。 示例: 1 + * @example 16 + */ + demand_id?: number; + /** + * @description 可选。按服务商过滤。 示例: 01HYYYY + * @example architecto + */ + provider_uid?: string; + /** + * @description 可选。按轮次过滤。 示例: 1 + * @example 16 + */ + round_no?: number; + /** + * @description 可选。页码。 示例: 1 + * @example 16 + */ + page?: number; + /** + * @description 可选。每页数量,最大100。 示例: 20 + * @example 16 + */ + page_size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** + * @example sent + * @enum {string|null} + */ + status?: "sent" | "delivered" | "viewed" | "accepted" | "rejected" | "ignored" | "expired" | "taken_by_other" | null; + /** @example 16 */ + demand_id?: number | null; + /** @example architecto */ + provider_uid?: string | null; + /** + * @description Must be at least 0. + * @example 39 + */ + round_no?: number | null; + /** + * @description Must be at least 1. + * @example 67 + */ + page?: number | null; + /** + * @description Must be at least 1. Must not be greater than 100. + * @example 16 + */ + page_size?: number | null; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + success?: boolean; + /** + * @example [ + * { + * "kind": "demand_dispatch", + * "id": 3, + * "demand": { + * "id": 1, + * "title": "清洗空调" + * }, + * "provider": { + * "uid": "01HYYYY", + * "display_name": "Bob" + * }, + * "round_no": 1, + * "status": "accepted", + * "score": 0.87, + * "similarity": 0.91, + * "distance_meters": 1200.5, + * "eta_minutes": 15, + * "auto_grab_eligible": true, + * "sent_at": "2026-07-11T09:00:00Z", + * "viewed_at": null, + * "responded_at": "2026-07-11T09:01:00Z", + * "expires_at": "2026-07-11T09:05:00Z" + * } + * ] + */ + data?: { + /** @example demand_dispatch */ + kind?: string; + /** @example 3 */ + id?: number; + demand?: { + /** @example 1 */ + id?: number; + /** @example 清洗空调 */ + title?: string; + }; + provider?: { + /** @example 01HYYYY */ + uid?: string; + /** @example Bob */ + display_name?: string; + }; + /** @example 1 */ + round_no?: number; + /** @example accepted */ + status?: string; + /** @example 0.87 */ + score?: number; + /** @example 0.91 */ + similarity?: number; + /** @example 1200.5 */ + distance_meters?: number; + /** @example 15 */ + eta_minutes?: number; + /** @example true */ + auto_grab_eligible?: boolean; + /** @example 2026-07-11T09:00:00Z */ + sent_at?: string; + /** @example null */ + viewed_at?: string; + /** @example 2026-07-11T09:01:00Z */ + responded_at?: string; + /** @example 2026-07-11T09:05:00Z */ + expires_at?: string; + }[]; + pagination?: { + /** @example 1 */ + current_page?: number; + /** @example null */ + next_page?: string; + /** @example 20 */ + page_size?: number; + /** @example null */ + prev_page?: string; + /** @example 1 */ + total_count?: number; + /** @example 1 */ + total_pages?: number; + }; + }; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example false */ + success?: boolean; + /** @example FORBIDDEN */ + code?: string; + /** @example 无权限访问 */ + message?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/admin/v1/exc/orders": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** [订单监控] 订单列表 */ + get: { + parameters: { + query?: { + /** + * @description 可选。按订单状态过滤: created, confirmed, started, consumed, paid, fulfilled, canceled。 示例: paid + * @example architecto + */ + status?: string; + /** + * @description 可选。按支付状态过滤: UNPAID, PAID。 示例: PAID + * @example architecto + */ + payment_status?: string; + /** + * @description 可选。按买家过滤。 示例: 01HXXXX + * @example architecto + */ + buyer_uid?: string; + /** + * @description 可选。按服务商过滤。 示例: 01HYYYY + * @example architecto + */ + provider_uid?: string; + /** + * @description 可选。页码。 示例: 1 + * @example 16 + */ + page?: number; + /** + * @description 可选。每页数量,最大100。 示例: 20 + * @example 16 + */ + page_size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** + * @example confirmed + * @enum {string|null} + */ + status?: "created" | "confirmed" | "started" | "consumed" | "paid" | "fulfilled" | "canceled" | null; + /** + * @example UNPAID + * @enum {string|null} + */ + payment_status?: "UNPAID" | "PAID" | null; + /** @example architecto */ + buyer_uid?: string | null; + /** @example architecto */ + provider_uid?: string | null; + /** + * @description Must be at least 1. + * @example 22 + */ + page?: number | null; + /** + * @description Must be at least 1. Must not be greater than 100. + * @example 7 + */ + page_size?: number | null; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + success?: boolean; + /** + * @example [ + * { + * "kind": "service_order", + * "id": 1, + * "title": "清洗空调", + * "status": "paid", + * "payment_status": "PAID", + * "buyer": { + * "uid": "01HXXXX", + * "display_name": "Alice" + * }, + * "provider": { + * "uid": "01HYYYY", + * "display_name": "Bob" + * }, + * "currency_code": "CNY", + * "total_amount": 15000, + * "actual_total_amount": 15000, + * "platform_fee_amount": 1500, + * "provider_net_amount": 13500, + * "commission_rate": 0.1, + * "cancellation_fee_amount": null, + * "paid_at": "2026-07-11T12:00:00Z", + * "settled_at": null, + * "escrow_release_at": "2026-07-14T12:00:00Z", + * "escrow_released_at": null, + * "order_date": "2026-07-11T10:00:00Z", + * "created_at": "2026-07-11T10:00:00Z" + * } + * ] + */ + data?: { + /** @example service_order */ + kind?: string; + /** @example 1 */ + id?: number; + /** @example 清洗空调 */ + title?: string; + /** @example paid */ + status?: string; + /** @example PAID */ + payment_status?: string; + buyer?: { + /** @example 01HXXXX */ + uid?: string; + /** @example Alice */ + display_name?: string; + }; + provider?: { + /** @example 01HYYYY */ + uid?: string; + /** @example Bob */ + display_name?: string; + }; + /** @example CNY */ + currency_code?: string; + /** @example 15000 */ + total_amount?: number; + /** @example 15000 */ + actual_total_amount?: number; + /** @example 1500 */ + platform_fee_amount?: number; + /** @example 13500 */ + provider_net_amount?: number; + /** @example 0.1 */ + commission_rate?: number; + /** @example null */ + cancellation_fee_amount?: string; + /** @example 2026-07-11T12:00:00Z */ + paid_at?: string; + /** @example null */ + settled_at?: string; + /** @example 2026-07-14T12:00:00Z */ + escrow_release_at?: string; + /** @example null */ + escrow_released_at?: string; + /** @example 2026-07-11T10:00:00Z */ + order_date?: string; + /** @example 2026-07-11T10:00:00Z */ + created_at?: string; + }[]; + pagination?: { + /** @example 1 */ + current_page?: number; + /** @example null */ + next_page?: string; + /** @example 20 */ + page_size?: number; + /** @example null */ + prev_page?: string; + /** @example 1 */ + total_count?: number; + /** @example 1 */ + total_pages?: number; + }; + }; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example false */ + success?: boolean; + /** @example FORBIDDEN */ + code?: string; + /** @example 无权限访问 */ + message?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/admin/v1/exc/orders/{id}": { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description 订单 ID。 + * @example 1 + */ + id: number; + }; + cookie?: never; + }; + /** + * [订单监控] 订单详情 + * @description 含订单项与状态流转审计记录。 + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description 订单 ID。 + * @example 1 + */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + success?: boolean; + data?: { + /** @example service_order */ + kind?: string; + /** @example 1 */ + id?: number; + /** @example paid */ + status?: string; + /** @example [] */ + items?: unknown[]; + /** @example [] */ + transactions?: unknown[]; + }; + }; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example false */ + success?: boolean; + /** @example NOT_FOUND */ + code?: string; + /** @example 请求的资源未找到。 */ + message?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/admin/v1/exc/providers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** [服务商监控] 服务商列表 */ + get: { + parameters: { + query?: { + /** + * @description 可选。按在线状态过滤: offline, online, inactive, busy。 示例: online + * @example architecto + */ + status?: string; + /** + * @description 可选。按是否禁用过滤。 示例: false + * @example false + */ + is_disabled?: boolean; + /** + * @description 可选。按是否认证过滤。 示例: true + * @example false + */ + is_verified?: boolean; + /** + * @description 可选。页码。 示例: 1 + * @example 16 + */ + page?: number; + /** + * @description 可选。每页数量,最大100。 示例: 20 + * @example 16 + */ + page_size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** + * @example busy + * @enum {string|null} + */ + status?: "offline" | "online" | "inactive" | "busy" | null; + /** @example true */ + is_disabled?: boolean | null; + /** @example true */ + is_verified?: boolean | null; + /** + * @description Must be at least 1. + * @example 16 + */ + page?: number | null; + /** + * @description Must be at least 1. Must not be greater than 100. + * @example 22 + */ + page_size?: number | null; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + success?: boolean; + /** + * @example [ + * { + * "kind": "provider_profile", + * "user": { + * "uid": "01HYYYY", + * "display_name": "Bob" + * }, + * "status": "online", + * "is_disabled": false, + * "is_verified": true, + * "rating": 4.8, + * "completed_jobs": 32, + * "online_at": "2026-07-11T08:00:00Z", + * "offline_at": null, + * "last_location_at": "2026-07-11T09:30:00Z", + * "current_service_order_id": null, + * "created_at": "2026-06-01T08:00:00Z" + * } + * ] + */ + data?: { + /** @example provider_profile */ + kind?: string; + user?: { + /** @example 01HYYYY */ + uid?: string; + /** @example Bob */ + display_name?: string; + }; + /** @example online */ + status?: string; + /** @example false */ + is_disabled?: boolean; + /** @example true */ + is_verified?: boolean; + /** @example 4.8 */ + rating?: number; + /** @example 32 */ + completed_jobs?: number; + /** @example 2026-07-11T08:00:00Z */ + online_at?: string; + /** @example null */ + offline_at?: string; + /** @example 2026-07-11T09:30:00Z */ + last_location_at?: string; + /** @example null */ + current_service_order_id?: string; + /** @example 2026-06-01T08:00:00Z */ + created_at?: string; + }[]; + pagination?: { + /** @example 1 */ + current_page?: number; + /** @example null */ + next_page?: string; + /** @example 20 */ + page_size?: number; + /** @example null */ + prev_page?: string; + /** @example 1 */ + total_count?: number; + /** @example 1 */ + total_pages?: number; + }; + }; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example false */ + success?: boolean; + /** @example FORBIDDEN */ + code?: string; + /** @example 无权限访问 */ + message?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/admin/v1/exc/providers/{uid}": { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description 服务商用户 UID。 + * @example 01HYYYY + */ + uid: string; + }; + cookie?: never; + }; + /** + * [服务商监控] 服务商详情 + * @description 含派单设置与最近状态日志。 + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description 服务商用户 UID。 + * @example 01HYYYY + */ + uid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + success?: boolean; + data?: { + /** @example provider_profile */ + kind?: string; + user?: { + /** @example 01HYYYY */ + uid?: string; + /** @example Bob */ + display_name?: string; + }; + /** @example online */ + status?: string; + dispatch_setting?: { + /** @example true */ + auto_grab_enabled?: boolean; + }; + /** + * @example [ + * { + * "from_status": "offline", + * "to_status": "online", + * "reason": null, + * "created_at": "2026-07-11T08:00:00Z" + * } + * ] + */ + status_logs?: { + /** @example offline */ + from_status?: string; + /** @example online */ + to_status?: string; + /** @example null */ + reason?: string; + /** @example 2026-07-11T08:00:00Z */ + created_at?: string; + }[]; + }; + }; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example false */ + success?: boolean; + /** @example NOT_FOUND */ + code?: string; + /** @example 请求的资源未找到。 */ + message?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/admin/v1/notification/devices": { parameters: { query?: never; @@ -896,7 +1967,7 @@ export interface paths { * @enum {string|null} */ platform?: "ios" | "android" | "web" | "desktop" | null; - /** @example true */ + /** @example false */ is_online?: boolean | null; /** * @description Must be at least 1. diff --git a/src/api/types.ts b/src/api/types.ts index 6ee0b50..cbd8c72 100644 --- a/src/api/types.ts +++ b/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 + }> +} diff --git a/src/components/layout/nav.ts b/src/components/layout/nav.ts index 670c1c7..25dd977 100644 --- a/src/components/layout/nav.ts +++ b/src/components/layout/nav.ts @@ -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: [ diff --git a/src/features/exc/labels.tsx b/src/features/exc/labels.tsx new file mode 100644 index 0000000..df40584 --- /dev/null +++ b/src/features/exc/labels.tsx @@ -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 = { + draft: '草稿', + open: '开放中', + quotation_closed: '报价截止', + order_placed: '已下单', + fulfilled: '已完成', + broadcasting: '派单中', + matched: '已匹配', + converted: '已转单', + expired: '已过期', + canceled: '已取消', +} + +export const DISPATCH_MODE_LABELS: Record = { + instant_accept: '即时抢单', + quote: '报价', + scheduled_booking: '预约', + manual_selection: '手动指派', +} + +export const DISPATCH_STATUS_LABELS: Record = { + sent: '已发送', + delivered: '已送达', + viewed: '已查看', + accepted: '已接单', + rejected: '已拒绝', + ignored: '未响应', + expired: '已过期', + taken_by_other: '他人接单', +} + +export const ORDER_STATUS_LABELS: Record = { + created: '已创建', + confirmed: '已确认', + started: '服务中', + consumed: '已消费', + paid: '已支付', + fulfilled: '已完成', + canceled: '已取消', +} + +export const PROVIDER_STATUS_LABELS: Record = { + 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> = { + open: ACTIVE, + broadcasting: WARN, + matched: POSITIVE, + converted: POSITIVE, + order_placed: POSITIVE, + fulfilled: POSITIVE, + expired: NEGATIVE, + canceled: NEGATIVE, +} + +const DISPATCH_STATUS_CLASSES: Partial> = { + accepted: POSITIVE, + rejected: NEGATIVE, + expired: NEGATIVE, + taken_by_other: WARN, +} + +const ORDER_STATUS_CLASSES: Partial> = { + started: ACTIVE, + paid: POSITIVE, + fulfilled: POSITIVE, + canceled: NEGATIVE, +} + +const PROVIDER_STATUS_CLASSES: Partial> = { + online: POSITIVE, + busy: WARN, + inactive: NEGATIVE, +} + +function statusBadge(label: string, className?: string) { + return className ? {label} : {label} +} + +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') : '—' +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 9b2ddcc..f90a111 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -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, diff --git a/src/routes/_authed/exc/demands.tsx b/src/routes/_authed/exc/demands.tsx new file mode 100644 index 0000000..8eac68c --- /dev/null +++ b/src/routes/_authed/exc/demands.tsx @@ -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('all') + const [mode, setMode] = useState('all') + const [detailId, setDetailId] = useState(null) + + const filters: Record = {} + 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[] = [ + { + header: '需求', + cell: ({ row }) => ( + + ), + }, + { + header: '状态', + cell: ({ row }) => , + }, + { + 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 ? ( + {row.original.matched_provider_uid} + ) : ( + '—' + ), + }, + { + header: '创建时间', + cell: ({ row }) => formatTime(row.original.created_at), + }, + ] + + return ( +
+ + + +
+ } + /> + + !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 }, + ]} + /> + + ) +} diff --git a/src/routes/_authed/exc/dispatches.tsx b/src/routes/_authed/exc/dispatches.tsx new file mode 100644 index 0000000..d651715 --- /dev/null +++ b/src/routes/_authed/exc/dispatches.tsx @@ -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('all') + const [round, setRound] = useState('all') + + const filters: Record = {} + if (status !== 'all') filters.status = status + if (round !== 'all') filters.round_no = round + + const list = useListPage('exc-dispatches', fetchExcDispatches, filters) + + const columns: ColumnDef[] = [ + { + header: '需求', + cell: ({ row }) => ( +
+
{row.original.demand?.title ?? '—'}
+ + #{row.original.demand?.id ?? '—'} + +
+ ), + }, + { + header: '服务商', + cell: ({ row }) => ( +
+
{row.original.provider?.display_name ?? '—'}
+ + {row.original.provider?.uid ?? ''} + +
+ ), + }, + { + header: '轮次', + cell: ({ row }) => R{row.original.round_no}, + }, + { + header: '状态', + cell: ({ row }) => , + }, + { + 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 ( +
+ + + +
+ } + /> + + + ) +} diff --git a/src/routes/_authed/exc/orders.tsx b/src/routes/_authed/exc/orders.tsx new file mode 100644 index 0000000..e3a008c --- /dev/null +++ b/src/routes/_authed/exc/orders.tsx @@ -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('all') + const [payment, setPayment] = useState('all') + const [detailId, setDetailId] = useState(null) + + const filters: Record = {} + 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[] = [ + { + header: '订单', + cell: ({ row }) => ( + + ), + }, + { + header: '状态', + cell: ({ row }) => , + }, + { + header: '支付', + cell: ({ row }) => + row.original.payment_status === 'PAID' ? ( + 已支付 + ) : ( + 未支付 + ), + }, + { + header: '买家 / 服务商', + cell: ({ row }) => ( +
+
{row.original.buyer?.display_name ?? '—'}
+
{row.original.provider?.display_name ?? '—'}
+
+ ), + }, + { + 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 ( +
+ + + +
+ } + /> + + !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, + }, + ]} + /> + + ) +} diff --git a/src/routes/_authed/exc/providers.tsx b/src/routes/_authed/exc/providers.tsx new file mode 100644 index 0000000..c7b6a5c --- /dev/null +++ b/src/routes/_authed/exc/providers.tsx @@ -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('all') + const [verified, setVerified] = useState('all') + const [detailUid, setDetailUid] = useState(null) + + const filters: Record = {} + 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[] = [ + { + header: '服务商', + cell: ({ row }) => ( + + ), + }, + { + header: '状态', + cell: ({ row }) => , + }, + { + header: '认证/禁用', + cell: ({ row }) => ( +
+ {row.original.is_verified && ( + 已认证 + )} + {row.original.is_disabled && ( + 已禁用 + )} + {!row.original.is_verified && !row.original.is_disabled && '—'} +
+ ), + }, + { + 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 ( +
+ + + +
+ } + /> + + !open && setDetailUid(null)} + title={provider?.user?.display_name ?? detailUid ?? ''} + description={provider ? `状态 ${PROVIDER_STATUS_LABELS[provider.status] ?? provider.status}` : undefined} + fields={[ + { label: 'UID', value: {provider?.user_uid} }, + { 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 }, + ]} + /> + + ) +}