From 55ffb1e88bd7e18a0143f79393a5846280298b8a Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 28 Jul 2026 11:24:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8A=A2=E5=8D=95=E5=A4=A7?= =?UTF-8?q?=E5=8E=85=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/worker/WorkerHallPage.tsx | 576 +++++++++++++++--- apps/frontend/src/styles/main.css | 216 ++++++- 2 files changed, 694 insertions(+), 98 deletions(-) diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index 17144261..c2fe55c9 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -1,10 +1,36 @@ -import { ReloadOutlined, SearchOutlined } from '@ant-design/icons' +import { EyeOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { App, Button, Card, Empty, Input, Pagination, Space, Spin, Tag, Typography } from 'antd' +import { + Alert, + App, + Button, + Card, + Descriptions, + Drawer, + Empty, + Input, + Pagination, + Space, + Spin, + Tag, + Typography, +} from 'antd' import { useState } from 'react' -import { fetchWorkerHallOrders, grabWorkerOrder } from '@/services/worker' -import type { WorkOrder } from '@/types/worker-platform' +import { + fetchWorkerHallOrders, + fetchWorkerMyOrders, + fetchWorkerProfile, + grabWorkerOrder, +} from '@/services/worker' +import type { CollectField, WorkOrder, WorkerUser } from '@/types/worker-platform' +import { formatDateTime } from '@/utils/date-time' + +type HallSummary = { + activeCount: number + pendingAcceptanceCount: number + problemCount: number +} export default function WorkerHallPage() { const { message } = App.useApp() @@ -14,6 +40,7 @@ export default function WorkerHallPage() { const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [grabbingId, setGrabbingId] = useState(0) + const [detailOrder, setDetailOrder] = useState(null) const ordersQuery = useQuery({ queryKey: ['worker-hall-orders', keyword, page, pageSize], @@ -21,15 +48,82 @@ export default function WorkerHallPage() { retry: false, }) + const profileQuery = useQuery({ + queryKey: ['worker-profile'], + queryFn: () => fetchWorkerProfile(), + retry: false, + }) + + const hallSummaryQuery = useQuery({ + queryKey: ['worker-hall-summary'], + queryFn: async () => { + const [inProgressResponse, pendingAcceptanceResponse, problemResponse] = + await Promise.all([ + fetchWorkerMyOrders({ + status: 'in_progress', + page: 1, + pageSize: 1, + }), + fetchWorkerMyOrders({ + status: 'pending_acceptance', + page: 1, + pageSize: 1, + }), + fetchWorkerMyOrders({ + status: 'problem', + page: 1, + pageSize: 1, + }), + ]) + + const pendingAcceptanceCount = + pendingAcceptanceResponse.data.pagination.total + const problemCount = problemResponse.data.pagination.total + + return { + activeCount: + inProgressResponse.data.pagination.total + + pendingAcceptanceCount + + problemCount, + pendingAcceptanceCount, + problemCount, + } satisfies HallSummary + }, + retry: false, + }) + const orders = ordersQuery.data?.data.items || [] const pagination = ordersQuery.data?.data.pagination + const worker = profileQuery.data?.data.worker + const activeCount = hallSummaryQuery.data?.activeCount || 0 + const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 0 + const problemCount = hallSummaryQuery.data?.problemCount || 0 + const maxActiveOrders = Number(worker?.level?.permissions.maxActiveOrders || 0) + const remainingSlots = + maxActiveOrders > 0 ? Math.max(0, maxActiveOrders - activeCount) : null + const shouldWarn = + (remainingSlots !== null && remainingSlots <= 0) || problemCount > 0 + const detailDisabledReason = detailOrder + ? resolveGrabDisabledReason(detailOrder, worker, remainingSlots) + : '' + + async function refreshAll() { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['worker-hall-orders'] }), + queryClient.invalidateQueries({ queryKey: ['worker-hall-summary'] }), + queryClient.invalidateQueries({ queryKey: ['worker-profile'] }), + queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }), + queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }), + ]) + } async function submitGrab(order: WorkOrder) { setGrabbingId(order.workOrderId) try { await grabWorkerOrder(order.workOrderId) - message.success('抢单成功') - await queryClient.invalidateQueries({ queryKey: ['worker-hall-orders'] }) + message.success('抢单成功,已进入我的订单') + setDetailOrder(null) + await refreshAll() } catch (error) { message.error(error instanceof Error ? error.message : '抢单失败') } finally { @@ -42,103 +136,415 @@ export default function WorkerHallPage() { setPage(1) } + const summaryItems = [ + { + label: '当前余额', + value: worker ? formatMoney(worker.wallet.availableAmount) : '-', + note: '可用于冻结押金', + }, + { + label: '接单上限', + value: + remainingSlots === null + ? '未配置' + : `${activeCount}/${maxActiveOrders} 单`, + note: + remainingSlots === null + ? '当前等级待配置' + : `还能再接 ${remainingSlots} 单`, + }, + { + label: '冻结押金', + value: worker ? formatMoney(worker.wallet.frozenDepositAmount) : '-', + note: '当前已占用金额', + }, + { + label: '问题单', + value: `${problemCount} 单`, + note: problemCount ? '建议优先处理' : '当前暂无问题单', + }, + ] + return (
抢单大厅 - - } - onChange={(event) => { - const nextKeyword = event.target.value - setKeywordInput(nextKeyword) - if (!nextKeyword.trim()) { - setKeyword('') - setPage(1) - } - }} - onPressEnter={applyKeywordSearch} - /> - + - + + } + onChange={(event) => { + const nextKeyword = event.target.value + setKeywordInput(nextKeyword) + if (!nextKeyword.trim()) { + setKeyword('') + setPage(1) + } + }} + onPressEnter={applyKeywordSearch} + /> + + +
- {ordersQuery.isLoading ? : null} - {ordersQuery.error ? ( - - - {ordersQuery.error instanceof Error ? ordersQuery.error.message : '读取抢单大厅失败'} - - - ) : null} - - {!ordersQuery.isLoading && orders.length === 0 ? ( - - ) : ( -
- {orders.map((order) => ( - - - - - {order.productName} - - {order.categoryName || '默认分类'} - - 订单号:{order.platformOrderId} - - {formatMoney(order.rewardAmount)} - - - 所需冻结押金:{formatMoney(order.freezeDepositAmount)} - - - - + +
+ {summaryItems.map((item) => ( +
+ {item.label} + {item.value} + {item.note} +
))}
- )} - {(pagination?.total || 0) > 0 ? ( - `共 ${total} 条`} - onChange={(nextPage, nextPageSize) => { - setPage(nextPage) - setPageSize(nextPageSize) - }} - /> - ) : null} + {shouldWarn ? ( + + ) : null} + + {ordersQuery.isLoading ? ( +
+ +
+ ) : ordersQuery.error ? ( + + ) : orders.length === 0 ? ( + + ) : ( + <> +
+ {orders.map((order) => { + const requirementFields = getRequirementFields(order) + const disabledReason = resolveGrabDisabledReason( + order, + worker, + remainingSlots, + ) + const zeroDeposit = Number(order.freezeDepositAmount || 0) <= 0 + + return ( +
+
+
+ setDetailOrder(order)}> + {order.productName || order.workOrderNo} + + + {order.categoryName || '默认分类'} + +
+
+ + {zeroDeposit ? '免押金' : '需押金'} + + + {formatRequirementShortLabel(requirementFields)} + +
+
+ +
+ + 工单号:{order.workOrderNo} + + + 订单号:{order.platformOrderId || '-'} + +
+ +
+
+ {formatMoney(order.rewardAmount)} +
+
+ 所需保证金:{formatMoney(order.freezeDepositAmount)} + 发布时间:{formatDateTime(resolvePublishedTime(order))} +
+
+ +
+
+ 资料要求 + {formatRequirementPreview(requirementFields)} +
+
+ 抢单判断 + + {disabledReason || buildGrabReadyHint(order)} + +
+
+ +
+ + +
+
+ ) + })} +
+ + {(pagination?.total || 0) > 0 ? ( +
+ `共 ${total} 条`} + onChange={(nextPage, nextPageSize) => { + setPage(nextPage) + setPageSize(nextPageSize) + }} + /> +
+ ) : null} + + )} +
+ + setDetailOrder(null)} + extra={ + detailOrder ? ( + + ) : null + } + > + {detailOrder ? ( +
+ + + + + + {detailOrder.productName || '-'} + + + {detailOrder.categoryName || '-'} + + + {detailOrder.workOrderNo} + + + {detailOrder.platformOrderId || '-'} + + + {formatMoney(detailOrder.rewardAmount)} + + + {formatMoney(detailOrder.freezeDepositAmount)} + + + {formatDateTime(resolvePublishedTime(detailOrder))} + + + {formatDateTime(detailOrder.updatedAt)} + + + {formatRequirementShortLabel(getRequirementFields(detailOrder))} + + + {detailDisabledReason || '当前可直接抢单'} + + + + + + {renderRequirementFieldValues(detailOrder)} + +
+ ) : null} +
) } +function getRequirementFields(order: WorkOrder | null): CollectField[] { + if (!order) return [] + const rawFields = Array.isArray(order.requirement?.fields) + ? order.requirement.fields + : [] + return rawFields + .map((item) => { + const source = asRecord(item) + const key = String(source.key || '').trim() + if (!key) return null + return { + key, + label: String(source.label || key).trim(), + required: source.required !== false, + } + }) + .filter((item): item is CollectField => Boolean(item)) +} + +function renderRequirementFieldValues(order: WorkOrder) { + const fields = getRequirementFields(order) + if (fields.length === 0) { + return 当前工单未配置资料要求 + } + + return ( + + {fields.map((field) => ( + + + {field.key} + + {field.required ? '必填' : '选填'} + + + + ))} + + ) +} + +function resolveGrabDisabledReason( + order: WorkOrder, + worker: WorkerUser | undefined, + remainingSlots: number | null, +) { + if (remainingSlots !== null && remainingSlots <= 0) { + return '已达到当前等级最大同时接单量' + } + + if (!worker) { + return '' + } + + const availableAmount = Number(worker.wallet.availableAmount || 0) + const freezeAmount = Number(order.freezeDepositAmount || 0) + if (availableAmount < freezeAmount) { + return `余额不足,还差 ${formatMoney(freezeAmount - availableAmount)}` + } + + return '' +} + +function buildGrabReadyHint(order: WorkOrder) { + const freezeAmount = Number(order.freezeDepositAmount || 0) + if (freezeAmount <= 0) { + return '当前工单无需冻结押金,可直接抢单' + } + return `抢单后会冻结 ${formatMoney(freezeAmount)}` +} + +function formatRequirementShortLabel(fields: CollectField[]) { + if (fields.length === 0) { + return '资料简单' + } + const requiredCount = fields.filter((field) => field.required).length + return `${requiredCount} 项必填 / 共 ${fields.length} 项` +} + +function formatRequirementPreview(fields: CollectField[]) { + if (fields.length === 0) { + return '当前工单未配置额外资料字段' + } + + const preview = fields + .slice(0, 3) + .map((field) => field.label) + .filter(Boolean) + .join('、') + + if (fields.length <= 3) { + return preview + } + + return `${preview} 等 ${fields.length} 项` +} + +function resolvePublishedTime(order: WorkOrder) { + return order.publishedAt || order.createdAt || order.updatedAt +} + +function asRecord(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : {} +} + function formatMoney(value: number | undefined) { return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}` } diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index abae1f7f..196cbced 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -1475,6 +1475,27 @@ select { .table-toolbar { display: grid; } + + .worker-profile-hero-head { + flex-direction: column; + } + + .worker-hall-toolbar { + width: 100%; + justify-content: flex-start; + } + + .worker-hall-search { + width: 100%; + } + + .worker-hall-summary-strip { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .worker-hall-card-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } @media (max-width: 720px) { @@ -1668,24 +1689,177 @@ select { margin: 0; } -.worker-order-grid { +.worker-hall-head-copy { + display: grid; + gap: 4px; +} + +.worker-hall-head-copy .ant-typography { + margin: 0; +} + +.worker-hall-toolbar { + justify-content: flex-end; +} + +.worker-hall-search { + width: min(460px, 100%); +} + +.worker-hall-board-card .ant-card-body { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; } -.worker-order-card .ant-card-body { - height: 100%; +.worker-hall-summary-strip { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 12px; } -.worker-order-card-head { - width: 100%; +.worker-hall-summary-item { + min-width: 0; + padding: 12px 14px; + border-radius: 12px; + border: 1px solid #e2e8f0; + background: #f8fafc; + display: grid; + gap: 4px; +} + +.worker-hall-summary-label { + color: #64748b; + font-size: 12px; +} + +.worker-hall-summary-value { + color: #0f172a; + font-size: 22px; + line-height: 1.15; +} + +.worker-hall-summary-note { + color: #94a3b8; + font-size: 12px; + line-height: 1.5; +} + +.worker-hall-warning { + margin-bottom: 0; +} + +.worker-hall-loading { + min-height: 220px; + display: grid; + place-items: center; +} + +.worker-hall-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); + gap: 16px; +} + +.worker-hall-card { + min-width: 0; + padding: 18px 18px 16px; + border-radius: 16px; + background: #ffffff; + border: 1px solid #edf0f5; + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06); + display: grid; + gap: 14px; +} + +.worker-hall-card-head { + display: flex; justify-content: space-between; + align-items: flex-start; + gap: 12px; } -.worker-order-money { - margin: 4px 0 0; +.worker-hall-card-title { + display: grid; + gap: 6px; + min-width: 0; +} + +.worker-hall-card-title .ant-typography { + margin: 0; +} + +.worker-hall-card-badges { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 6px; +} + +.worker-hall-card-order { + display: grid; + gap: 4px; +} + +.worker-hall-card-main { + display: grid; + gap: 8px; +} + +.worker-hall-card-amount { color: #f97316; + font-size: 34px; + line-height: 1; + font-weight: 700; +} + +.worker-hall-card-side { + display: grid; + gap: 4px; + color: #94a3b8; + font-size: 12px; +} + +.worker-hall-card-meta { + display: grid; + gap: 10px; +} + +.worker-hall-card-meta-row { + display: grid; + gap: 6px; +} + +.worker-hall-card-meta-row span { + color: #64748b; + font-size: 12px; +} + +.worker-hall-card-meta-row strong { + color: #334155; + font-weight: 500; + line-height: 1.5; + overflow-wrap: anywhere; +} + +.worker-hall-card-danger { + color: #dc2626 !important; +} + +.worker-hall-card-safe { + color: #16a34a !important; +} + +.worker-hall-card-actions { + display: flex; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; + margin-top: auto; +} + +.worker-hall-detail-stack { + display: grid; + gap: 16px; } .worker-orders-overview-grid { @@ -2079,12 +2253,32 @@ select { flex-direction: column; } - .worker-order-grid { + .worker-hall-summary-strip { grid-template-columns: 1fr; } + .worker-hall-toolbar, .worker-orders-toolbar { align-items: stretch; + flex-direction: column; + } + + .worker-hall-search { + width: 100%; + } + + .worker-hall-card-grid { + grid-template-columns: 1fr; + } + + .worker-hall-card { + padding: 16px; + } + + .worker-hall-card-head, + .worker-hall-card-actions { + align-items: stretch; + flex-direction: column; } .worker-orders-search { @@ -2096,10 +2290,6 @@ select { justify-content: space-between; } - .worker-profile-hero-head { - flex-direction: column; - } - .worker-profile-hero-side { width: 100%; justify-content: flex-start;