优化抢单大厅布局
This commit is contained in:
@@ -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 { 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 { useState } from 'react'
|
||||||
|
|
||||||
import { fetchWorkerHallOrders, grabWorkerOrder } from '@/services/worker'
|
import {
|
||||||
import type { WorkOrder } from '@/types/worker-platform'
|
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() {
|
export default function WorkerHallPage() {
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
@@ -14,6 +40,7 @@ export default function WorkerHallPage() {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [grabbingId, setGrabbingId] = useState(0)
|
const [grabbingId, setGrabbingId] = useState(0)
|
||||||
|
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
||||||
|
|
||||||
const ordersQuery = useQuery({
|
const ordersQuery = useQuery({
|
||||||
queryKey: ['worker-hall-orders', keyword, page, pageSize],
|
queryKey: ['worker-hall-orders', keyword, page, pageSize],
|
||||||
@@ -21,15 +48,82 @@ export default function WorkerHallPage() {
|
|||||||
retry: false,
|
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 orders = ordersQuery.data?.data.items || []
|
||||||
const pagination = ordersQuery.data?.data.pagination
|
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) {
|
async function submitGrab(order: WorkOrder) {
|
||||||
setGrabbingId(order.workOrderId)
|
setGrabbingId(order.workOrderId)
|
||||||
try {
|
try {
|
||||||
await grabWorkerOrder(order.workOrderId)
|
await grabWorkerOrder(order.workOrderId)
|
||||||
message.success('抢单成功')
|
message.success('抢单成功,已进入我的订单')
|
||||||
await queryClient.invalidateQueries({ queryKey: ['worker-hall-orders'] })
|
setDetailOrder(null)
|
||||||
|
await refreshAll()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '抢单失败')
|
message.error(error instanceof Error ? error.message : '抢单失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -42,15 +136,56 @@ export default function WorkerHallPage() {
|
|||||||
setPage(1)
|
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 (
|
return (
|
||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<div className="worker-page-head">
|
<div className="worker-page-head">
|
||||||
<Typography.Title level={3}>抢单大厅</Typography.Title>
|
<Typography.Title level={3}>抢单大厅</Typography.Title>
|
||||||
<Space.Compact>
|
<Space wrap className="worker-hall-toolbar">
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={
|
||||||
|
ordersQuery.isFetching ||
|
||||||
|
profileQuery.isFetching ||
|
||||||
|
hallSummaryQuery.isFetching
|
||||||
|
}
|
||||||
|
onClick={() => refreshAll()}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Space.Compact className="worker-hall-search">
|
||||||
<Input
|
<Input
|
||||||
allowClear
|
allowClear
|
||||||
value={keywordInput}
|
value={keywordInput}
|
||||||
placeholder="搜索商品或订单号"
|
placeholder="搜索商品名称 / 订单号 / 工单号"
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const nextKeyword = event.target.value
|
const nextKeyword = event.target.value
|
||||||
@@ -63,82 +198,353 @@ export default function WorkerHallPage() {
|
|||||||
onPressEnter={applyKeywordSearch}
|
onPressEnter={applyKeywordSearch}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
type="primary"
|
||||||
icon={<SearchOutlined />}
|
icon={<SearchOutlined />}
|
||||||
loading={ordersQuery.isFetching}
|
loading={ordersQuery.isFetching}
|
||||||
onClick={applyKeywordSearch}
|
onClick={applyKeywordSearch}
|
||||||
>
|
>
|
||||||
查询
|
搜索
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
loading={ordersQuery.isFetching}
|
|
||||||
onClick={() => ordersQuery.refetch()}
|
|
||||||
>
|
|
||||||
刷新
|
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ordersQuery.isLoading ? <Spin /> : null}
|
<Card className="worker-hall-board-card" bordered={false}>
|
||||||
{ordersQuery.error ? (
|
<div className="worker-hall-summary-strip">
|
||||||
<Card>
|
{summaryItems.map((item) => (
|
||||||
<Typography.Text type="danger">
|
<div key={item.label} className="worker-hall-summary-item">
|
||||||
{ordersQuery.error instanceof Error ? ordersQuery.error.message : '读取抢单大厅失败'}
|
<span className="worker-hall-summary-label">{item.label}</span>
|
||||||
</Typography.Text>
|
<strong className="worker-hall-summary-value">{item.value}</strong>
|
||||||
</Card>
|
<small className="worker-hall-summary-note">{item.note}</small>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{shouldWarn ? (
|
||||||
|
<Alert
|
||||||
|
className="worker-hall-warning"
|
||||||
|
showIcon
|
||||||
|
type="warning"
|
||||||
|
message={
|
||||||
|
remainingSlots !== null && remainingSlots <= 0
|
||||||
|
? '当前已达到接单上限,请先处理现有工单'
|
||||||
|
: `当前还有 ${problemCount} 单问题单待处理,建议优先收口`
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!ordersQuery.isLoading && orders.length === 0 ? (
|
{ordersQuery.isLoading ? (
|
||||||
<Empty description="暂无可抢订单" />
|
<div className="worker-hall-loading">
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : ordersQuery.error ? (
|
||||||
|
<Empty
|
||||||
|
description={
|
||||||
|
ordersQuery.error instanceof Error
|
||||||
|
? ordersQuery.error.message
|
||||||
|
: '读取抢单大厅失败'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : orders.length === 0 ? (
|
||||||
|
<Empty
|
||||||
|
description={
|
||||||
|
keyword
|
||||||
|
? `没有找到与 “${keyword}” 相关的可抢工单`
|
||||||
|
: '当前暂无可抢订单,可稍后刷新再看'
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="worker-order-grid">
|
<>
|
||||||
{orders.map((order) => (
|
<div className="worker-hall-card-grid">
|
||||||
<Card key={order.workOrderId} className="worker-order-card" bordered={false}>
|
{orders.map((order) => {
|
||||||
<Space direction="vertical" className="full-width" size={8}>
|
const requirementFields = getRequirementFields(order)
|
||||||
<Space className="worker-order-card-head">
|
const disabledReason = resolveGrabDisabledReason(
|
||||||
<Typography.Text strong ellipsis>
|
order,
|
||||||
{order.productName}
|
worker,
|
||||||
</Typography.Text>
|
remainingSlots,
|
||||||
<Tag color="blue">{order.categoryName || '默认分类'}</Tag>
|
)
|
||||||
</Space>
|
const zeroDeposit = Number(order.freezeDepositAmount || 0) <= 0
|
||||||
<Typography.Text type="secondary">订单号:{order.platformOrderId}</Typography.Text>
|
|
||||||
<Typography.Title level={4} className="worker-order-money">
|
return (
|
||||||
{formatMoney(order.rewardAmount)}
|
<article key={order.workOrderId} className="worker-hall-card">
|
||||||
</Typography.Title>
|
<div className="worker-hall-card-head">
|
||||||
|
<div className="worker-hall-card-title">
|
||||||
|
<Typography.Link onClick={() => setDetailOrder(order)}>
|
||||||
|
{order.productName || order.workOrderNo}
|
||||||
|
</Typography.Link>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
所需冻结押金:{formatMoney(order.freezeDepositAmount)}
|
{order.categoryName || '默认分类'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="worker-hall-card-badges">
|
||||||
|
<Tag color={zeroDeposit ? 'green' : 'gold'}>
|
||||||
|
{zeroDeposit ? '免押金' : '需押金'}
|
||||||
|
</Tag>
|
||||||
|
<Tag color="blue">
|
||||||
|
{formatRequirementShortLabel(requirementFields)}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-hall-card-order">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
工单号:{order.workOrderNo}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
订单号:{order.platformOrderId || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-hall-card-main">
|
||||||
|
<div className="worker-hall-card-amount">
|
||||||
|
{formatMoney(order.rewardAmount)}
|
||||||
|
</div>
|
||||||
|
<div className="worker-hall-card-side">
|
||||||
|
<span>所需保证金:{formatMoney(order.freezeDepositAmount)}</span>
|
||||||
|
<span>发布时间:{formatDateTime(resolvePublishedTime(order))}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-hall-card-meta">
|
||||||
|
<div className="worker-hall-card-meta-row">
|
||||||
|
<span>资料要求</span>
|
||||||
|
<strong>{formatRequirementPreview(requirementFields)}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="worker-hall-card-meta-row">
|
||||||
|
<span>抢单判断</span>
|
||||||
|
<strong
|
||||||
|
className={
|
||||||
|
disabledReason
|
||||||
|
? 'worker-hall-card-danger'
|
||||||
|
: 'worker-hall-card-safe'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{disabledReason || buildGrabReadyHint(order)}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="worker-hall-card-actions">
|
||||||
|
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(order)}>
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
disabled={Boolean(disabledReason)}
|
||||||
loading={grabbingId === order.workOrderId}
|
loading={grabbingId === order.workOrderId}
|
||||||
onClick={() => submitGrab(order)}
|
onClick={() => submitGrab(order)}
|
||||||
>
|
>
|
||||||
立即抢单
|
立即抢单
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</article>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{(pagination?.total || 0) > 0 ? (
|
{(pagination?.total || 0) > 0 ? (
|
||||||
|
<div className="worker-hall-pagination">
|
||||||
<Pagination
|
<Pagination
|
||||||
current={pagination?.page || page}
|
current={pagination?.page || page}
|
||||||
pageSize={pagination?.pageSize || pageSize}
|
pageSize={pagination?.pageSize || pageSize}
|
||||||
total={pagination?.total || 0}
|
total={pagination?.total || 0}
|
||||||
showSizeChanger
|
showSizeChanger
|
||||||
pageSizeOptions={[10, 20, 50, 100]}
|
pageSizeOptions={[20, 50, 100]}
|
||||||
showTotal={(total) => `共 ${total} 条`}
|
showTotal={(total) => `共 ${total} 条`}
|
||||||
onChange={(nextPage, nextPageSize) => {
|
onChange={(nextPage, nextPageSize) => {
|
||||||
setPage(nextPage)
|
setPage(nextPage)
|
||||||
setPageSize(nextPageSize)
|
setPageSize(nextPageSize)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={
|
||||||
|
detailOrder
|
||||||
|
? `${detailOrder.productName || '工单详情'} · ${detailOrder.workOrderNo}`
|
||||||
|
: '工单详情'
|
||||||
|
}
|
||||||
|
width={760}
|
||||||
|
open={Boolean(detailOrder)}
|
||||||
|
destroyOnHidden
|
||||||
|
onClose={() => setDetailOrder(null)}
|
||||||
|
extra={
|
||||||
|
detailOrder ? (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={Boolean(detailDisabledReason)}
|
||||||
|
loading={grabbingId === detailOrder.workOrderId}
|
||||||
|
onClick={() => submitGrab(detailOrder)}
|
||||||
|
>
|
||||||
|
立即抢单
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{detailOrder ? (
|
||||||
|
<div className="worker-hall-detail-stack">
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type={detailDisabledReason ? 'warning' : 'success'}
|
||||||
|
message={detailDisabledReason || buildGrabReadyHint(detailOrder)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card title="基础信息" size="small">
|
||||||
|
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
|
||||||
|
<Descriptions.Item label="商品">
|
||||||
|
{detailOrder.productName || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="分类">
|
||||||
|
{detailOrder.categoryName || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="工单号">
|
||||||
|
{detailOrder.workOrderNo}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="平台订单号">
|
||||||
|
{detailOrder.platformOrderId || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="接单奖励">
|
||||||
|
{formatMoney(detailOrder.rewardAmount)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="冻结押金">
|
||||||
|
{formatMoney(detailOrder.freezeDepositAmount)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="发布时间">
|
||||||
|
{formatDateTime(resolvePublishedTime(detailOrder))}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="更新时间">
|
||||||
|
{formatDateTime(detailOrder.updatedAt)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="资料要求">
|
||||||
|
{formatRequirementShortLabel(getRequirementFields(detailOrder))}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="接单资格">
|
||||||
|
{detailDisabledReason || '当前可直接抢单'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="资料要求" size="small">
|
||||||
|
{renderRequirementFieldValues(detailOrder)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Drawer>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 <Typography.Text type="secondary">当前工单未配置资料要求</Typography.Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Descriptions column={1} bordered size="small">
|
||||||
|
{fields.map((field) => (
|
||||||
|
<Descriptions.Item key={field.key} label={field.label}>
|
||||||
|
<Space size={6} wrap>
|
||||||
|
<Typography.Text code>{field.key}</Typography.Text>
|
||||||
|
<Tag color={field.required ? 'orange' : 'default'}>
|
||||||
|
{field.required ? '必填' : '选填'}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
</Descriptions.Item>
|
||||||
|
))}
|
||||||
|
</Descriptions>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown> {
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
|
||||||
function formatMoney(value: number | undefined) {
|
function formatMoney(value: number | undefined) {
|
||||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1475,6 +1475,27 @@ select {
|
|||||||
.table-toolbar {
|
.table-toolbar {
|
||||||
display: grid;
|
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) {
|
@media (max-width: 720px) {
|
||||||
@@ -1668,24 +1689,177 @@ select {
|
|||||||
margin: 0;
|
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;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.worker-order-card .ant-card-body {
|
.worker-hall-summary-strip {
|
||||||
height: 100%;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.worker-order-card-head {
|
.worker-hall-summary-item {
|
||||||
width: 100%;
|
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;
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.worker-order-money {
|
.worker-hall-card-title {
|
||||||
margin: 4px 0 0;
|
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;
|
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 {
|
.worker-orders-overview-grid {
|
||||||
@@ -2079,12 +2253,32 @@ select {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.worker-order-grid {
|
.worker-hall-summary-strip {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.worker-hall-toolbar,
|
||||||
.worker-orders-toolbar {
|
.worker-orders-toolbar {
|
||||||
align-items: stretch;
|
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 {
|
.worker-orders-search {
|
||||||
@@ -2096,10 +2290,6 @@ select {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.worker-profile-hero-head {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.worker-profile-hero-side {
|
.worker-profile-hero-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|||||||
Reference in New Issue
Block a user