perf(worker-hall): 优化百单大厅加载与渲染性能
This commit is contained in:
@@ -33,7 +33,7 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { WorkerContactModal } from '@/components/worker/WorkerContactModal'
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
isGrabConfirmEnabled,
|
||||
setGrabConfirmEnabled as setGrabConfirmEnabledStorage,
|
||||
} from '@/utils/worker-grab-confirm'
|
||||
import { patchWorkerWorkOrderInfiniteEnvelope } from '@/utils/worker-realtime-work-orders'
|
||||
import {
|
||||
resolveHallDepositAmount,
|
||||
resolveHallProductName,
|
||||
@@ -91,18 +92,9 @@ export default function WorkerHallPage() {
|
||||
const [sharingQuantity, setSharingQuantity] = useState(1)
|
||||
const [submittingSharing, setSubmittingSharing] = useState(false)
|
||||
const [failedImageKeys, setFailedImageKeys] = useState<Record<string | number, boolean>>({})
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const [grabConfirmEnabled, setGrabConfirmEnabled] = useState(isGrabConfirmEnabled)
|
||||
const [nowMs, setNowMs] = useState(() => Date.now())
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setNowMs(Date.now())
|
||||
}, 30_000)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
const categoriesQuery = useQuery({
|
||||
queryKey: ['worker-hall-categories'],
|
||||
queryFn: () => fetchWorkerHallCategories(),
|
||||
@@ -122,11 +114,11 @@ export default function WorkerHallPage() {
|
||||
pageSize: HALL_PAGE_SIZE,
|
||||
categoryId: effectiveCategoryId || undefined,
|
||||
}),
|
||||
getNextPageParam: (lastPage) => {
|
||||
getNextPageParam: (lastPage, allPages) => {
|
||||
const pagination = lastPage.data.pagination
|
||||
return pagination.page * pagination.pageSize < pagination.total
|
||||
? pagination.page + 1
|
||||
: undefined
|
||||
// 只有第一页会做 COUNT;后续页用第一页的 total 判断是否还有下一页。
|
||||
const total = Number(allPages[0]?.data.pagination.total || pagination.total || 0)
|
||||
return pagination.page * pagination.pageSize < total ? pagination.page + 1 : undefined
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 2_000,
|
||||
@@ -278,7 +270,29 @@ export default function WorkerHallPage() {
|
||||
ordersQuery.isFetchingNextPage,
|
||||
])
|
||||
|
||||
async function refreshAll() {
|
||||
const removeHallOrderLocally = useCallback(
|
||||
(workOrderId: number) => {
|
||||
queryClient.setQueriesData({ queryKey: ['worker-hall-orders'] }, (current: unknown) =>
|
||||
patchWorkerWorkOrderInfiniteEnvelope(current, {
|
||||
operation: 'remove',
|
||||
entityId: workOrderId,
|
||||
data: { workOrderId },
|
||||
}),
|
||||
)
|
||||
},
|
||||
[queryClient],
|
||||
)
|
||||
|
||||
const refreshAfterMutation = useCallback(async () => {
|
||||
// 抢单/拼单只改变「我的订单」和钱包,不再把已加载的 5~7 页大厅全部重拉一遍。
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-profile'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
|
||||
])
|
||||
}, [queryClient])
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
await Promise.all([
|
||||
ordersQuery.refetch(),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-hall-categories'] }),
|
||||
@@ -287,58 +301,81 @@ export default function WorkerHallPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-hall-leaderboard'] }),
|
||||
])
|
||||
}
|
||||
}, [ordersQuery.refetch, queryClient])
|
||||
|
||||
function toggleGrabConfirm(checked: boolean) {
|
||||
setGrabConfirmEnabled(checked)
|
||||
setGrabConfirmEnabledStorage(checked)
|
||||
}
|
||||
|
||||
function requestGrab(order: WorkOrder) {
|
||||
if (!grabConfirmEnabled) {
|
||||
void submitGrab(order)
|
||||
return
|
||||
}
|
||||
modal.confirm({
|
||||
title: '确认抢单',
|
||||
icon: <QuestionCircleOutlined />,
|
||||
content: `确认抢下「${resolveHallProductName(order)}」?奖励 ${formatMoney(
|
||||
resolveHallRewardAmount(order),
|
||||
)}`,
|
||||
okText: '确认抢单',
|
||||
cancelText: '取消',
|
||||
onOk: () => submitGrab(order),
|
||||
})
|
||||
}
|
||||
const submitGrab = useCallback(
|
||||
async (order: WorkOrder) => {
|
||||
setGrabbingId(order.workOrderId)
|
||||
try {
|
||||
await grabWorkerOrder(order.workOrderId)
|
||||
message.success('抢单成功,已进入我的订单')
|
||||
removeHallOrderLocally(order.workOrderId)
|
||||
await refreshAfterMutation()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '抢单失败')
|
||||
} finally {
|
||||
setGrabbingId(0)
|
||||
}
|
||||
},
|
||||
[message, refreshAfterMutation, removeHallOrderLocally],
|
||||
)
|
||||
|
||||
async function submitGrab(order: WorkOrder) {
|
||||
setGrabbingId(order.workOrderId)
|
||||
try {
|
||||
await grabWorkerOrder(order.workOrderId)
|
||||
message.success('抢单成功,已进入我的订单')
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '抢单失败')
|
||||
} finally {
|
||||
setGrabbingId(0)
|
||||
}
|
||||
}
|
||||
const requestGrab = useCallback(
|
||||
(order: WorkOrder) => {
|
||||
if (!grabConfirmEnabled) {
|
||||
void submitGrab(order)
|
||||
return
|
||||
}
|
||||
modal.confirm({
|
||||
title: '确认抢单',
|
||||
icon: <QuestionCircleOutlined />,
|
||||
content: `确认抢下「${resolveHallProductName(order)}」?奖励 ${formatMoney(
|
||||
resolveHallRewardAmount(order),
|
||||
)}`,
|
||||
okText: '确认抢单',
|
||||
cancelText: '取消',
|
||||
onOk: () => submitGrab(order),
|
||||
})
|
||||
},
|
||||
[grabConfirmEnabled, modal, submitGrab],
|
||||
)
|
||||
|
||||
function openSharingModal(order: WorkOrder) {
|
||||
const openSharingModal = useCallback((order: WorkOrder) => {
|
||||
setSharingOrder(order)
|
||||
setSharingQuantity(1)
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function submitSharing() {
|
||||
if (!sharingOrder) return
|
||||
setSubmittingSharing(true)
|
||||
try {
|
||||
await joinWorkerSharingOrder(sharingOrder.workOrderId, {
|
||||
const result = await joinWorkerSharingOrder(sharingOrder.workOrderId, {
|
||||
quantity: sharingQuantity,
|
||||
})
|
||||
message.success('拼单成功,已进入我的订单')
|
||||
const currentProgress = sharingOrder.sharingProgress
|
||||
queryClient.setQueriesData({ queryKey: ['worker-hall-orders'] }, (current: unknown) =>
|
||||
patchWorkerWorkOrderInfiniteEnvelope(current, {
|
||||
operation: 'upsert',
|
||||
entityId: sharingOrder.workOrderId,
|
||||
data: {
|
||||
...sharingOrder,
|
||||
myShare: { quantity: Number(result.data.share?.quantity || sharingQuantity) },
|
||||
sharingProgress: {
|
||||
joinedQuantity:
|
||||
Number(currentProgress?.joinedQuantity || 0) + Number(sharingQuantity || 0),
|
||||
pendingSubmissionCount: Number(currentProgress?.pendingSubmissionCount || 0) + 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
setSharingOrder(null)
|
||||
await refreshAll()
|
||||
await refreshAfterMutation()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '拼单失败')
|
||||
} finally {
|
||||
@@ -620,118 +657,18 @@ export default function WorkerHallPage() {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
orders.map((order) => {
|
||||
const disabledReason = resolveGrabDisabledReason(order, worker, remainingSlots)
|
||||
const productName = resolveHallProductName(order) || '未命名商品'
|
||||
const sharingRemaining = resolveSharingRemaining(order)
|
||||
|
||||
return (
|
||||
<article key={order.workOrderId} className="worker-hall-mobile-card">
|
||||
{/* 卡片头部: 分类 + 拼单/时限标签 + 订单号 */}
|
||||
<div className="worker-hall-mobile-card-top">
|
||||
<div className="worker-hall-mobile-card-tags">
|
||||
{order.pinnedAt ? (
|
||||
<Tag
|
||||
color="gold"
|
||||
icon={<PushpinOutlined />}
|
||||
className="worker-hall-mobile-tag"
|
||||
>
|
||||
置顶
|
||||
</Tag>
|
||||
) : null}
|
||||
{order.categoryName ? (
|
||||
<Tag color="blue" className="worker-hall-mobile-tag">
|
||||
{order.parentCategoryName
|
||||
? `${order.parentCategoryName} · ${order.categoryName}`
|
||||
: order.categoryName}
|
||||
</Tag>
|
||||
) : null}
|
||||
{isInternalWorker && order.publishedAt ? (
|
||||
<Tooltip title={`发布时间: ${formatDateTime(order.publishedAt)}`}>
|
||||
<Tag
|
||||
icon={<ClockCircleOutlined />}
|
||||
className="worker-hall-published-tag"
|
||||
>
|
||||
{formatPublishedElapsedTime(order.publishedAt, nowMs)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{order.sharing?.enabled ? (
|
||||
<Tag color="purple" className="worker-hall-mobile-tag">
|
||||
拼单 {order.sharingProgress?.joinedQuantity || 0}/
|
||||
{order.sharing.totalQuantity} · 剩{resolveSharingRemaining(order)}份
|
||||
</Tag>
|
||||
) : null}
|
||||
{Number(order.timeoutMinutes || 0) > 0 ? (
|
||||
<Tag
|
||||
color="orange"
|
||||
icon={<ClockCircleOutlined />}
|
||||
className="worker-hall-mobile-tag"
|
||||
>
|
||||
限时{order.timeoutMinutes}分
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="worker-hall-mobile-card-orderno">
|
||||
{getDisplayOrderNo(order)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 商品标题与保证金说明 */}
|
||||
<div className="worker-hall-mobile-card-main">
|
||||
<strong className="worker-hall-mobile-card-title" title={productName}>
|
||||
{productName}
|
||||
</strong>
|
||||
<div className="worker-hall-mobile-card-meta">
|
||||
<span>所需保证金: {formatMoney(resolveHallDepositAmount(order))}</span>
|
||||
{order.sharing?.enabled ? (
|
||||
<span> · 拼单单价 {formatMoney(order.sharing.unitReward)}/份</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部报酬金额与抢单按钮 */}
|
||||
<div className="worker-hall-mobile-card-footer">
|
||||
<div className="worker-hall-mobile-card-price-group">
|
||||
<span className="worker-hall-mobile-card-price-label">接单报酬</span>
|
||||
<strong className="worker-hall-mobile-card-price-val">
|
||||
{formatMoney(resolveHallRewardAmount(order))}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div className="worker-hall-mobile-card-btn-group">
|
||||
{order.sharing?.enabled &&
|
||||
(sharingRemaining > 0 || order.myShare) &&
|
||||
!disabledReason ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
className="worker-hall-sharing-button"
|
||||
disabled={Boolean(disabledReason) || Boolean(order.myShare)}
|
||||
onClick={() => openSharingModal(order)}
|
||||
>
|
||||
{order.myShare
|
||||
? `已拼 ${order.myShare.quantity} 份`
|
||||
: `拼单 ${formatMoney(order.sharing.unitReward)}/份`}
|
||||
</Button>
|
||||
) : null}
|
||||
{sharingRemaining > 0 || !order.sharing?.enabled ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
className="worker-hall-mobile-grab-btn"
|
||||
disabled={Boolean(disabledReason)}
|
||||
loading={grabbingId === order.workOrderId}
|
||||
onClick={() => requestGrab(order)}
|
||||
>
|
||||
{disabledReason || '立即抢单'}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})
|
||||
orders.map((order) => (
|
||||
<HallMobileCard
|
||||
key={order.workOrderId}
|
||||
order={order}
|
||||
worker={worker}
|
||||
remainingSlots={remainingSlots}
|
||||
isInternalWorker={isInternalWorker}
|
||||
grabbing={grabbingId === order.workOrderId}
|
||||
onGrab={requestGrab}
|
||||
onOpenSharing={openSharingModal}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{loadMoreIndicator}
|
||||
@@ -1058,132 +995,18 @@ export default function WorkerHallPage() {
|
||||
) : (
|
||||
<>
|
||||
<div className="worker-hall-card-grid">
|
||||
{orders.map((order) => {
|
||||
const disabledReason = resolveGrabDisabledReason(order, worker, remainingSlots)
|
||||
const productName = resolveHallProductName(order) || '未命名商品'
|
||||
const sharingRemaining = resolveSharingRemaining(order)
|
||||
const depositAmount = resolveHallDepositAmount(order)
|
||||
const rewardAmount = resolveHallRewardAmount(order)
|
||||
|
||||
return (
|
||||
<article key={order.workOrderId} className="worker-hall-card">
|
||||
<div className="worker-hall-card-header">
|
||||
<div className="worker-hall-card-tags">
|
||||
{order.pinnedAt ? (
|
||||
<Tag
|
||||
color="gold"
|
||||
icon={<PushpinOutlined />}
|
||||
className="worker-hall-pinned-tag"
|
||||
>
|
||||
置顶
|
||||
</Tag>
|
||||
) : null}
|
||||
<Tag className="worker-hall-category-tag">
|
||||
{order.parentCategoryName
|
||||
? `${order.parentCategoryName} · ${order.categoryName}`
|
||||
: order.categoryName || '默认分类'}
|
||||
</Tag>
|
||||
{isInternalWorker && order.publishedAt ? (
|
||||
<Tooltip title={`发布时间: ${formatDateTime(order.publishedAt)}`}>
|
||||
<Tag
|
||||
icon={<ClockCircleOutlined />}
|
||||
className="worker-hall-published-tag"
|
||||
>
|
||||
{formatPublishedElapsedTime(order.publishedAt, nowMs)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{order.sharing?.enabled ? <Tag color="purple">拼单</Tag> : null}
|
||||
{Number(order.timeoutMinutes || 0) > 0 ? (
|
||||
<Tag color="orange" icon={<ClockCircleOutlined />}>
|
||||
限时 {order.timeoutMinutes}m
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
<span
|
||||
className="worker-hall-card-orderno"
|
||||
title={`订单号:${getDisplayOrderNo(order)}`}
|
||||
>
|
||||
{getDisplayOrderNo(order)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="worker-hall-card-body">
|
||||
<strong className="worker-hall-card-title-text" title={productName}>
|
||||
{productName}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
{order.sharing?.enabled ? (
|
||||
<div className="worker-hall-card-sharing">
|
||||
<Typography.Text type="secondary">
|
||||
单价 {formatMoney(order.sharing.unitReward)} · 进度{' '}
|
||||
{order.sharingProgress?.joinedQuantity || 0}/
|
||||
{order.sharing.totalQuantity} · 剩 {resolveSharingRemaining(order)} 份
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="worker-hall-card-amount-row">
|
||||
<div className="worker-hall-card-amount-group">
|
||||
<span className="worker-hall-card-amount-label">接单报酬</span>
|
||||
<div className="worker-hall-card-amount">
|
||||
{formatMoney(rewardAmount)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="worker-hall-card-deposit-group">
|
||||
{depositAmount <= 0 ? (
|
||||
<span className="worker-hall-deposit-free-badge">
|
||||
<CheckCircleOutlined /> 免保证金
|
||||
</span>
|
||||
) : (
|
||||
<span className="worker-hall-deposit-badge">
|
||||
保证金 {formatMoney(depositAmount)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{disabledReason ? (
|
||||
<Typography.Text type="danger" className="worker-hall-card-hint">
|
||||
{disabledReason}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
|
||||
<div className="worker-hall-card-actions">
|
||||
<div className="worker-hall-card-grab-row">
|
||||
{order.sharing?.enabled &&
|
||||
(sharingRemaining > 0 || order.myShare) &&
|
||||
!disabledReason ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="worker-hall-sharing-button"
|
||||
disabled={Boolean(disabledReason) || Boolean(order.myShare)}
|
||||
onClick={() => openSharingModal(order)}
|
||||
>
|
||||
{order.myShare
|
||||
? `已拼 ${order.myShare.quantity} 份`
|
||||
: `拼单 ${formatMoney(order.sharing.unitReward)}/份`}
|
||||
</Button>
|
||||
) : null}
|
||||
{sharingRemaining > 0 || !order.sharing?.enabled ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="worker-hall-card-grab-btn"
|
||||
disabled={Boolean(disabledReason)}
|
||||
loading={grabbingId === order.workOrderId}
|
||||
onClick={() => requestGrab(order)}
|
||||
>
|
||||
{disabledReason || '立即抢单'}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
{orders.map((order) => (
|
||||
<HallDesktopCard
|
||||
key={order.workOrderId}
|
||||
order={order}
|
||||
worker={worker}
|
||||
remainingSlots={remainingSlots}
|
||||
isInternalWorker={isInternalWorker}
|
||||
grabbing={grabbingId === order.workOrderId}
|
||||
onGrab={requestGrab}
|
||||
onOpenSharing={openSharingModal}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loadMoreIndicator}
|
||||
@@ -1326,6 +1149,287 @@ export default function WorkerHallPage() {
|
||||
)
|
||||
}
|
||||
|
||||
type HallCardProps = {
|
||||
order: WorkOrder
|
||||
worker: WorkerUser | undefined
|
||||
remainingSlots: number | null
|
||||
isInternalWorker: boolean
|
||||
grabbing: boolean
|
||||
onGrab: (order: WorkOrder) => void
|
||||
onOpenSharing: (order: WorkOrder) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 已加载 100+ 单时,任何一条实时变更都会替换大厅缓存中的一条订单。
|
||||
* 卡片拆成 memo 组件后,只有真正变化的那条会重渲染,其余卡片直接复用。
|
||||
*/
|
||||
const HallDesktopCard = memo(function HallDesktopCard({
|
||||
order,
|
||||
worker,
|
||||
remainingSlots,
|
||||
isInternalWorker,
|
||||
grabbing,
|
||||
onGrab,
|
||||
onOpenSharing,
|
||||
}: HallCardProps) {
|
||||
const disabledReason = resolveGrabDisabledReason(order, worker, remainingSlots)
|
||||
const productName = resolveHallProductName(order) || '未命名商品'
|
||||
const sharingRemaining = resolveHallRemainingQuantity(order)
|
||||
const depositAmount = resolveHallDepositAmount(order)
|
||||
const rewardAmount = resolveHallRewardAmount(order)
|
||||
|
||||
return (
|
||||
<article className="worker-hall-card">
|
||||
<div className="worker-hall-card-header">
|
||||
<div className="worker-hall-card-tags">
|
||||
{order.pinnedAt ? (
|
||||
<Tag color="gold" icon={<PushpinOutlined />} className="worker-hall-pinned-tag">
|
||||
置顶
|
||||
</Tag>
|
||||
) : null}
|
||||
<Tag className="worker-hall-category-tag">
|
||||
{order.parentCategoryName
|
||||
? `${order.parentCategoryName} · ${order.categoryName}`
|
||||
: order.categoryName || '默认分类'}
|
||||
</Tag>
|
||||
{isInternalWorker && order.publishedAt ? (
|
||||
<HallPublishedTag
|
||||
publishedAt={order.publishedAt}
|
||||
className="worker-hall-published-tag"
|
||||
/>
|
||||
) : null}
|
||||
{order.sharing?.enabled ? <Tag color="purple">拼单</Tag> : null}
|
||||
{Number(order.timeoutMinutes || 0) > 0 ? (
|
||||
<Tag color="orange" icon={<ClockCircleOutlined />}>
|
||||
限时 {order.timeoutMinutes}m
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="worker-hall-card-orderno" title={`订单号:${getDisplayOrderNo(order)}`}>
|
||||
{getDisplayOrderNo(order)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="worker-hall-card-body">
|
||||
<strong className="worker-hall-card-title-text" title={productName}>
|
||||
{productName}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
{order.sharing?.enabled ? (
|
||||
<div className="worker-hall-card-sharing">
|
||||
<Typography.Text type="secondary">
|
||||
单价 {formatMoney(order.sharing.unitReward)} · 进度{' '}
|
||||
{order.sharingProgress?.joinedQuantity || 0}/{order.sharing.totalQuantity} · 剩{' '}
|
||||
{sharingRemaining} 份
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="worker-hall-card-amount-row">
|
||||
<div className="worker-hall-card-amount-group">
|
||||
<span className="worker-hall-card-amount-label">接单报酬</span>
|
||||
<div className="worker-hall-card-amount">{formatMoney(rewardAmount)}</div>
|
||||
</div>
|
||||
<div className="worker-hall-card-deposit-group">
|
||||
{depositAmount <= 0 ? (
|
||||
<span className="worker-hall-deposit-free-badge">
|
||||
<CheckCircleOutlined /> 免保证金
|
||||
</span>
|
||||
) : (
|
||||
<span className="worker-hall-deposit-badge">保证金 {formatMoney(depositAmount)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{disabledReason ? (
|
||||
<Typography.Text type="danger" className="worker-hall-card-hint">
|
||||
{disabledReason}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
|
||||
<div className="worker-hall-card-actions">
|
||||
<div className="worker-hall-card-grab-row">
|
||||
{order.sharing?.enabled && (sharingRemaining > 0 || order.myShare) && !disabledReason ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="worker-hall-sharing-button"
|
||||
disabled={Boolean(disabledReason) || Boolean(order.myShare)}
|
||||
onClick={() => onOpenSharing(order)}
|
||||
>
|
||||
{order.myShare
|
||||
? `已拼 ${order.myShare.quantity} 份`
|
||||
: `拼单 ${formatMoney(order.sharing.unitReward)}/份`}
|
||||
</Button>
|
||||
) : null}
|
||||
{sharingRemaining > 0 || !order.sharing?.enabled ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="worker-hall-card-grab-btn"
|
||||
disabled={Boolean(disabledReason)}
|
||||
loading={grabbing}
|
||||
onClick={() => onGrab(order)}
|
||||
>
|
||||
{disabledReason || '立即抢单'}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})
|
||||
|
||||
const HallMobileCard = memo(function HallMobileCard({
|
||||
order,
|
||||
worker,
|
||||
remainingSlots,
|
||||
isInternalWorker,
|
||||
grabbing,
|
||||
onGrab,
|
||||
onOpenSharing,
|
||||
}: HallCardProps) {
|
||||
const disabledReason = resolveGrabDisabledReason(order, worker, remainingSlots)
|
||||
const productName = resolveHallProductName(order) || '未命名商品'
|
||||
const sharingRemaining = resolveHallRemainingQuantity(order)
|
||||
|
||||
return (
|
||||
<article className="worker-hall-mobile-card">
|
||||
<div className="worker-hall-mobile-card-top">
|
||||
<div className="worker-hall-mobile-card-tags">
|
||||
{order.pinnedAt ? (
|
||||
<Tag color="gold" icon={<PushpinOutlined />} className="worker-hall-mobile-tag">
|
||||
置顶
|
||||
</Tag>
|
||||
) : null}
|
||||
{order.categoryName ? (
|
||||
<Tag color="blue" className="worker-hall-mobile-tag">
|
||||
{order.parentCategoryName
|
||||
? `${order.parentCategoryName} · ${order.categoryName}`
|
||||
: order.categoryName}
|
||||
</Tag>
|
||||
) : null}
|
||||
{isInternalWorker && order.publishedAt ? (
|
||||
<HallPublishedTag
|
||||
publishedAt={order.publishedAt}
|
||||
className="worker-hall-published-tag"
|
||||
/>
|
||||
) : null}
|
||||
{order.sharing?.enabled ? (
|
||||
<Tag color="purple" className="worker-hall-mobile-tag">
|
||||
拼单 {order.sharingProgress?.joinedQuantity || 0}/{order.sharing.totalQuantity} · 剩
|
||||
{sharingRemaining}份
|
||||
</Tag>
|
||||
) : null}
|
||||
{Number(order.timeoutMinutes || 0) > 0 ? (
|
||||
<Tag color="orange" icon={<ClockCircleOutlined />} className="worker-hall-mobile-tag">
|
||||
限时{order.timeoutMinutes}分
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="worker-hall-mobile-card-orderno">{getDisplayOrderNo(order)}</span>
|
||||
</div>
|
||||
|
||||
<div className="worker-hall-mobile-card-main">
|
||||
<strong className="worker-hall-mobile-card-title" title={productName}>
|
||||
{productName}
|
||||
</strong>
|
||||
<div className="worker-hall-mobile-card-meta">
|
||||
<span>所需保证金: {formatMoney(resolveHallDepositAmount(order))}</span>
|
||||
{order.sharing?.enabled ? (
|
||||
<span> · 拼单单价 {formatMoney(order.sharing.unitReward)}/份</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="worker-hall-mobile-card-footer">
|
||||
<div className="worker-hall-mobile-card-price-group">
|
||||
<span className="worker-hall-mobile-card-price-label">接单报酬</span>
|
||||
<strong className="worker-hall-mobile-card-price-val">
|
||||
{formatMoney(resolveHallRewardAmount(order))}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div className="worker-hall-mobile-card-btn-group">
|
||||
{order.sharing?.enabled && (sharingRemaining > 0 || order.myShare) && !disabledReason ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
className="worker-hall-sharing-button"
|
||||
disabled={Boolean(disabledReason) || Boolean(order.myShare)}
|
||||
onClick={() => onOpenSharing(order)}
|
||||
>
|
||||
{order.myShare
|
||||
? `已拼 ${order.myShare.quantity} 份`
|
||||
: `拼单 ${formatMoney(order.sharing.unitReward)}/份`}
|
||||
</Button>
|
||||
) : null}
|
||||
{sharingRemaining > 0 || !order.sharing?.enabled ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
className="worker-hall-mobile-grab-btn"
|
||||
disabled={Boolean(disabledReason)}
|
||||
loading={grabbing}
|
||||
onClick={() => onGrab(order)}
|
||||
>
|
||||
{disabledReason || '立即抢单'}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})
|
||||
|
||||
const HALL_CLOCK_INTERVAL_MS = 30_000
|
||||
let hallClockNow = Date.now()
|
||||
let hallClockTimer: ReturnType<typeof setInterval> | null = null
|
||||
const hallClockListeners = new Set<() => void>()
|
||||
|
||||
function subscribeHallClock(listener: () => void) {
|
||||
hallClockListeners.add(listener)
|
||||
if (!hallClockTimer) {
|
||||
hallClockNow = Date.now()
|
||||
hallClockTimer = setInterval(() => {
|
||||
hallClockNow = Date.now()
|
||||
for (const current of hallClockListeners) current()
|
||||
}, HALL_CLOCK_INTERVAL_MS)
|
||||
}
|
||||
return () => {
|
||||
hallClockListeners.delete(listener)
|
||||
if (hallClockListeners.size === 0 && hallClockTimer) {
|
||||
clearInterval(hallClockTimer)
|
||||
hallClockTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getHallClockSnapshot() {
|
||||
return hallClockNow
|
||||
}
|
||||
|
||||
/** 发布时间每 30 秒刷新一次,但只重渲染这个 Tag,不再带着整页 100+ 卡片一起渲染。 */
|
||||
const HallPublishedTag = memo(function HallPublishedTag({
|
||||
publishedAt,
|
||||
className,
|
||||
}: {
|
||||
publishedAt: string
|
||||
className?: string
|
||||
}) {
|
||||
const nowMs = useSyncExternalStore(subscribeHallClock, getHallClockSnapshot, getHallClockSnapshot)
|
||||
return (
|
||||
<Tag
|
||||
icon={<ClockCircleOutlined />}
|
||||
className={className}
|
||||
title={`发布时间: ${formatDateTime(publishedAt)}`}
|
||||
>
|
||||
{formatPublishedElapsedTime(publishedAt, nowMs)}
|
||||
</Tag>
|
||||
)
|
||||
})
|
||||
|
||||
function getCategoryGradient(name: string, id: number): string {
|
||||
const gradients = [
|
||||
'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
|
||||
@@ -1131,6 +1131,9 @@
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* 100+ 单时跳过屏幕外卡片的布局与绘制,滚动到附近再渲染。 */
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 240px;
|
||||
}
|
||||
|
||||
.worker-hall-card:hover {
|
||||
@@ -2599,6 +2602,8 @@ body {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 190px;
|
||||
}
|
||||
|
||||
.worker-hall-mobile-card-top {
|
||||
|
||||
@@ -127,7 +127,15 @@ export function patchWorkerWorkOrderListEnvelope(
|
||||
(entry) => toNumber((entry as { workOrderId?: unknown })?.workOrderId) === id,
|
||||
)
|
||||
if (index >= 0) {
|
||||
nextItems = items.map((entry, itemIndex) => (itemIndex === index ? event.data : entry))
|
||||
// 大厅广播是轻量快照,可能不含 myShare 等个性化字段;就地合并而不是整行替换。
|
||||
nextItems = items.map((entry, itemIndex) =>
|
||||
itemIndex === index
|
||||
? {
|
||||
...(isRecord(entry) ? entry : {}),
|
||||
...(isRecord(event.data) ? event.data : {}),
|
||||
}
|
||||
: entry,
|
||||
)
|
||||
} else if (insert) {
|
||||
nextItems = [event.data, ...items]
|
||||
} else {
|
||||
|
||||
@@ -65,6 +65,28 @@ test('按工单号搜到的行,实时事件来了也不能被删掉,并且
|
||||
assert.equal((itemsOf(updated)[0] as { status?: string }).status, 'in_progress')
|
||||
})
|
||||
|
||||
test('大厅轻量快照就地更新时保留 myShare 等个性化字段', () => {
|
||||
const envelope = buildEnvelope([
|
||||
buildRow({ status: 'open', myShare: { quantity: 2 }, categoryName: '密钥' }),
|
||||
])
|
||||
const updated = patchWorkerWorkOrderListEnvelope(
|
||||
envelope,
|
||||
{
|
||||
operation: 'upsert',
|
||||
data: { workOrderId: 9001, status: 'open', productName: '新商品名' },
|
||||
},
|
||||
false,
|
||||
)
|
||||
const item = itemsOf(updated)[0] as {
|
||||
productName?: string
|
||||
myShare?: { quantity?: number }
|
||||
categoryName?: string
|
||||
}
|
||||
assert.equal(item.productName, '新商品名')
|
||||
assert.equal(item.myShare?.quantity, 2, '广播不带 myShare 时不能把已拼份额覆盖掉')
|
||||
assert.equal(item.categoryName, '密钥')
|
||||
})
|
||||
|
||||
test('不可证真的关键词下,快照只能就地更新:不插入、不删除', () => {
|
||||
const envelope = buildEnvelope([buildRow()])
|
||||
// 关键词没命中前端已知字段(unknown):既不该把它当"不匹配"删掉
|
||||
|
||||
Reference in New Issue
Block a user