perf(worker-hall): 优化百单大厅加载与渲染性能

This commit is contained in:
yml2213
2026-10-24 20:41:58 +08:00
parent 72a758fd7c
commit 7fdef88ba6
11 changed files with 617 additions and 321 deletions
+390 -286
View File
@@ -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%)',
+5
View File
@@ -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):既不该把它当"不匹配"删掉