fix:优化再拼一次bug,优化打手端移动端拼单列表显示

This commit is contained in:
2026-10-26 16:36:26 +08:00
parent 2b967ddd4c
commit 7743d2019c
12 changed files with 465 additions and 66 deletions
@@ -36,6 +36,7 @@ import {
pickWorkerRepresentativeShare,
resolveWorkerOrderStatusTabs,
resolveWorkerPermissions,
stripWorkerPersonalShareFields,
} from '../worker-platform/mappers.js'
import { invalidateWorkerMyOrderCaches } from '../worker-platform/worker-my-order-cache.js'
@@ -347,21 +348,27 @@ async function publishWorkOrderSnapshots(input: {
Number(workOrder.sharing_total_quantity || 1),
joinedQuantity,
)
publishRealtimeEvent({
type: 'work_order.changed',
entityId: workOrderId,
scopes: ['worker_hall'],
operation: hallVisible ? 'upsert' : 'remove',
data: hallVisible
? mapWorkOrderForHall(
// 大厅广播是「所有打手通用」的快照:myShare/myShares 是每个打手自己的份额,
// 必须整键剔除,不能带 null/[] 过去。前端补丁对广播行做浅合并,带空值的
// 个人字段会把刚拼单成功写进缓存的份额冲掉,按钮从「再拼一次」变回「拼单」。
const hallSnapshot = hallVisible
? stripWorkerPersonalShareFields(
mapWorkOrderForHall(
workOrder,
{ depositFreeAmount: 0 },
{
// 广播无法按打手等级过滤可见延迟,携带入厅时间由客户端按各自延迟推迟展示。
sharingProgress: { joinedQuantity, pendingSubmissionCount },
},
)
: { workOrderId },
),
)
: null
publishRealtimeEvent({
type: 'work_order.changed',
entityId: workOrderId,
scopes: ['worker_hall'],
operation: hallVisible ? 'upsert' : 'remove',
data: hallSnapshot || { workOrderId },
broadcastWorkers: true,
})
}
@@ -98,6 +98,7 @@ export {
resolveWorkOrderOperationalStatus,
resolveWorkerShareProductName,
resolveWorkerShareStatus,
stripWorkerPersonalShareFields,
} from './work-order-display-mappers.js'
export {
@@ -6,6 +6,7 @@ import {
isWorkerOrderVipEvidencePending,
pickWorkerRepresentativeShare,
resolveWorkerOrderStatusTabs,
stripWorkerPersonalShareFields,
summarizeWorkerShares,
} from './work-order-display-mappers.js'
@@ -248,3 +249,18 @@ test('份额级问题单:已验收份额被标记后只归问题单,母单
})
assert.deepEqual(tabs, ['problem'])
})
test('大厅广播快照必须剔除打手私有份额字段', () => {
const snapshot = stripWorkerPersonalShareFields({
workOrderId: 1,
myShare: null,
myShares: [],
sharingProgress: { joinedQuantity: 2, pendingSubmissionCount: 1 },
})
// 前端对广播行做浅合并:带 null/[] 过去会把缓存里的个人份额冲掉,
// 表现为拼单成功后按钮变回「拼单 ¥x/份」。
assert.equal('myShare' in snapshot, false)
assert.equal('myShares' in snapshot, false)
assert.deepEqual(snapshot.sharingProgress, { joinedQuantity: 2, pendingSubmissionCount: 1 })
assert.equal(snapshot.workOrderId, 1)
})
@@ -605,6 +605,22 @@ export function mapWorkOrderForWorker(
}
}
/**
* 大厅广播快照必须剔除打手私有字段。
*
* 大厅广播是发给所有打手的通用快照,`myShare`/`myShares` 只有本人可见。
* 若原样带 `null`/`[]` 过去,前端 `patchWorkerWorkOrderListEnvelope` 的浅合并
* 会用空值覆盖行上已有的个人份额,按钮从「再拼一次」变回「拼单 ¥x/份」。
*/
export function stripWorkerPersonalShareFields<T extends { myShare?: unknown; myShares?: unknown }>(
snapshot: T,
): Record<string, unknown> {
const result: Record<string, unknown> = { ...snapshot }
delete result.myShare
delete result.myShares
return result
}
/**
* 抢单大厅专用轻量映射。
*
@@ -18,6 +18,7 @@ import { isExplicitOrderNoSearch } from '@/utils/admin-realtime-work-orders'
import {
compareWorkerHallOrderRows,
evaluateWorkerKeywordVerdict,
patchWorkerHallPersonalShares,
patchWorkerWorkOrderInfiniteEnvelope,
patchWorkerWorkOrderListEnvelope,
resolveWorkerMyOrderRealtimeVerdict,
@@ -324,6 +325,24 @@ function applyWorkerRealtimeEvent(
const data = event.data
if (event.scopes.includes('worker_my_orders')) {
patchWorkerMyOrderCaches(queryClient, { ...event, data })
// 大厅行上的个人份额只信任「我的订单」快照:大厅广播是通用快照,已剔除这些字段。
// 否则后台撤掉我的份额后,大厅按钮会一直停在「再拼一次」。
const hallWorkOrderId = Number(data.workOrderId) || Number(event.entityId) || 0
if (hallWorkOrderId > 0 && 'myShare' in data && 'myShares' in data) {
for (const [queryKey, current] of queryClient.getQueriesData({
queryKey: ['worker-hall-orders'],
})) {
queryClient.setQueryData(
queryKey,
patchWorkerHallPersonalShares(
current,
hallWorkOrderId,
data.myShare ?? null,
data.myShares ?? [],
),
)
}
}
void queryClient.invalidateQueries({ queryKey: ['worker-after-sales-cases'] })
}
if (event.scopes.includes('worker_hall')) {
@@ -51,6 +51,7 @@ import {
import type {
WorkCategory,
WorkOrder,
WorkOrderShare,
WorkerLeaderboardPeriod,
WorkerUser,
} from '@/types/worker-platform'
@@ -361,17 +362,27 @@ export default function WorkerHallPage() {
requestId: createRequestId('share'),
})
message.success('拼单成功,已进入我的订单')
const joinedShare = result.data?.share
const currentProgress = sharingOrder.sharingProgress
// 本地即时反馈:把刚拿到的份额合并进行上的 myShares,按钮据此切成「再拼一次」。
// 只写 myShare 虽然也能让按钮变一次,但连续拼单时「已参与 N 次」会一直停在 1,
// 且后端广播不带个人份额,必须在本地维护完整列表。
const previousShares = resolveHallMyShares(sharingOrder)
const joinedQuantity = Number(joinedShare?.quantity || sharingQuantity)
const nextShares = joinedShare
? [...previousShares.filter((share) => share.shareId !== joinedShare.shareId), joinedShare]
: previousShares
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) },
myShare: joinedShare || sharingOrder.myShare || null,
myShares: nextShares,
sharingProgress: {
joinedQuantity:
Number(currentProgress?.joinedQuantity || 0) + Number(sharingQuantity || 0),
...currentProgress,
joinedQuantity: Number(currentProgress?.joinedQuantity || 0) + joinedQuantity,
pendingSubmissionCount: Number(currentProgress?.pendingSubmissionCount || 0) + 1,
},
},
@@ -1163,13 +1174,18 @@ type HallCardProps = {
}
/**
* 我在该工单的份额摘要:参与次数 + 累计份数。
* 我在该工单的全部有效份额。
*
* 同一打手在同一工单可以多次拼单,`myShares` 是全部有效份额;旧数据只有 `myShare`。
* 大厅按钮据此固定显示「再拼一次」,不再随参与次数改文案。
*/
function resolveHallMyShares(order: WorkOrder): WorkOrderShare[] {
if (order.myShares?.length) return order.myShares
return order.myShare ? [order.myShare] : []
}
function resolveHallMyShareSummary(order: WorkOrder): { count: number; quantity: number } {
const shares = order.myShares?.length ? order.myShares : order.myShare ? [order.myShare] : []
const shares = resolveHallMyShares(order)
return {
count: shares.length,
quantity: shares.reduce((total, share) => total + Number(share.quantity || 0), 0),
@@ -1,5 +1,6 @@
import {
CopyOutlined,
DownOutlined,
EditOutlined,
EyeOutlined,
MessageOutlined,
@@ -8,6 +9,7 @@ import {
SearchOutlined,
} from '@ant-design/icons'
import { Alert, Button, Empty, Input, Pagination, Spin, Tag, Typography } from 'antd'
import { useState } from 'react'
import type { ReactNode } from 'react'
import { DeadlineCountdown } from '@/components/DeadlineCountdown'
@@ -238,6 +240,9 @@ function WorkerOrderMobileCard({
| 'onRefresh'
| 'onPageChange'
> & { order: WorkOrder }) {
/** 拼单明细默认折叠,点标题行展开;单份订单不显示该区块。 */
const [sharesExpanded, setSharesExpanded] = useState(false)
const shareCount = order.myShares?.length || 0
const items = getMaterialDetailItems(order).filter((item) => String(item.value || '').trim())
const screenshots = getMaterialScreenshots(order)
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order))
@@ -343,12 +348,23 @@ function WorkerOrderMobileCard({
<span className="worker-order-mobile-sub-time">{formatDateTime(order.assignedAt)}</span>
</div>
{(order.myShares?.length || 0) > 1 && renderShareTable ? (
{shareCount > 1 && renderShareTable ? (
<div className="worker-order-mobile-shares">
<Typography.Text strong style={{ fontSize: 12 }}>
拼单明细({order.myShares?.length} 份)
</Typography.Text>
{renderShareTable(order)}
<button
type="button"
className="worker-order-mobile-shares-toggle"
aria-expanded={sharesExpanded}
onClick={() => setSharesExpanded((previous) => !previous)}
>
<span>拼单明细({shareCount} 份)</span>
<span className="worker-order-mobile-shares-hint">
{sharesExpanded ? '收起' : '展开'}
<DownOutlined rotate={sharesExpanded ? 180 : 0} />
</span>
</button>
{sharesExpanded ? (
<div className="worker-order-mobile-shares-body">{renderShareTable(order)}</div>
) : null}
</div>
) : null}
@@ -22,11 +22,14 @@ const SHARE_STATUS_META: Record<string, { label: string; color: string }> = {
*/
export function WorkerOrderShareTable({
order,
mobile = false,
onOpenAcceptance,
onOpenCancelRequest,
onRemindAcceptance,
}: {
order: WorkOrder
/** 移动端改成卡片列表,避免 9 列表格在窄屏被压成竖排文字。 */
mobile?: boolean
onOpenAcceptance: (order: WorkOrder, shareId: number) => void
onOpenCancelRequest: (order: WorkOrder, shareId: number) => void
onRemindAcceptance: (order: WorkOrder) => void
@@ -41,6 +44,48 @@ export function WorkerOrderShareTable({
if (shares.length === 0) return null
const renderShareActions = (share: WorkOrderShare) => {
const canAccept = !isTimedOut && share.status === 'joined'
const canEdit = !isTimedOut && ['joined', 'submitted'].includes(String(share.status))
const canRemind = !isTimedOut && share.status === 'submitted'
const canCancel = !isTimedOut && ['joined', 'submitted'].includes(String(share.status))
if (!canAccept && !canEdit && !canRemind && !canCancel) {
return (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
无
</Typography.Text>
)
}
return (
<Space size={6} wrap>
{canAccept ? (
<Button
type="primary"
size="small"
onClick={() => onOpenAcceptance(order, share.shareId)}
>
验收
</Button>
) : null}
{canEdit && share.status === 'submitted' ? (
<Button size="small" onClick={() => onOpenAcceptance(order, share.shareId)}>
补充图片
</Button>
) : null}
{canRemind ? (
<Button size="small" onClick={() => onRemindAcceptance(order)}>
催验收
</Button>
) : null}
{canCancel ? (
<Button danger size="small" onClick={() => onOpenCancelRequest(order, share.shareId)}>
申请取消
</Button>
) : null}
</Space>
)
}
const columns: TableColumnsType<WorkOrderShare> = [
{
title: '份额',
@@ -154,50 +199,79 @@ export function WorkerOrderShareTable({
{
title: '操作',
width: 190,
render: (_, share) => {
const canAccept = !isTimedOut && share.status === 'joined'
const canEdit = !isTimedOut && ['joined', 'submitted'].includes(String(share.status))
const canRemind = !isTimedOut && share.status === 'submitted'
const canCancel = !isTimedOut && ['joined', 'submitted'].includes(String(share.status))
if (!canAccept && !canEdit && !canRemind && !canCancel) {
return (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
无
</Typography.Text>
)
}
return (
<Space size={6} wrap>
{canAccept ? (
<Button
type="primary"
size="small"
onClick={() => onOpenAcceptance(order, share.shareId)}
>
验收
</Button>
) : null}
{canEdit && share.status === 'submitted' ? (
<Button size="small" onClick={() => onOpenAcceptance(order, share.shareId)}>
补充图片
</Button>
) : null}
{canRemind ? (
<Button size="small" onClick={() => onRemindAcceptance(order)}>
催验收
</Button>
) : null}
{canCancel ? (
<Button danger size="small" onClick={() => onOpenCancelRequest(order, share.shareId)}>
申请取消
</Button>
) : null}
</Space>
)
},
render: (_, share) => renderShareActions(share),
},
]
if (mobile) {
return (
<div className="worker-order-share-cards">
{shares.map((share, index) => {
const meta = SHARE_STATUS_META[String(share.status)] || {
label: String(share.status || '-'),
color: 'default',
}
const problemNote = String(share.problemNote || '').trim()
const canAct = !isTimedOut && ['joined', 'submitted'].includes(String(share.status))
return (
<div className="worker-order-share-card" key={share.shareId}>
<div className="worker-order-share-card-head">
<Typography.Text strong className="worker-order-share-card-title">
第 {index + 1} 份 · #{share.shareId}
</Typography.Text>
<Tag color={meta.color} style={{ marginInlineEnd: 0 }}>
{meta.label}
</Tag>
</div>
<div className="worker-order-share-card-meta">
<div className="worker-order-share-card-meta-item">
<span className="worker-order-share-card-meta-label">数量</span>
<span className="worker-order-share-card-meta-value">{share.quantity} 份</span>
</div>
<div className="worker-order-share-card-meta-item">
<span className="worker-order-share-card-meta-label">奖励</span>
<span className="worker-order-share-card-meta-value" style={{ color: '#cf1322' }}>
{formatMoney(share.shareReward)}
</span>
</div>
<div className="worker-order-share-card-meta-item">
<span className="worker-order-share-card-meta-label">押金</span>
<span className="worker-order-share-card-meta-value">
{formatMoney(share.shareDeposit)}
</span>
</div>
</div>
<div className="worker-order-share-card-line">
<span className="worker-order-share-card-line-label">截止时间</span>
<span className="worker-order-share-card-line-value">
{share.deadlineAt ? formatDateTime(share.deadlineAt) : '不限时'}
</span>
{share.deadlineAt && share.status === 'joined' && !isTimedOut ? (
<DeadlineCountdown deadlineAt={share.deadlineAt} />
) : null}
</div>
{share.problemMarked ? (
<div className="worker-order-share-card-problem">
<Tag color="red" style={{ marginInlineEnd: 6 }}>
问题单
</Tag>
{problemNote || '后台已标记该份额有问题'}
</div>
) : null}
<div className="worker-order-share-card-acceptance">
<span className="worker-order-share-card-line-label">验收信息</span>
<ShareAcceptanceCell share={share} />
</div>
{canAct ? (
<div className="worker-order-share-card-actions">{renderShareActions(share)}</div>
) : null}
</div>
)
})}
</div>
)
}
return (
<PercentTable<WorkOrderShare>
rowKey="shareId"
@@ -592,6 +592,7 @@ export default function WorkerOrdersPage() {
return (
<WorkerOrderShareTable
order={order}
mobile={isMobile}
onOpenAcceptance={openAcceptanceForShare}
onOpenCancelRequest={openCancelRequestForShare}
onRemindAcceptance={(target) => {
+150 -3
View File
@@ -4894,8 +4894,155 @@ body {
.worker-order-mobile-shares {
display: flex;
flex-direction: column;
gap: 8px;
margin: 8px 0 4px;
}
/* 拼单明细默认折叠:整行可点,右侧显示「展开/收起」与箭头 */
.worker-order-mobile-shares-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 8px 0 0;
background: transparent;
border: 0;
border-top: 1px dashed #f0f0f0;
color: #0f172a;
font-size: 12px;
font-weight: 600;
line-height: 1.4;
text-align: left;
cursor: pointer;
}
.worker-order-mobile-shares-toggle:active {
opacity: 0.7;
}
.worker-order-mobile-shares-hint {
display: inline-flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
color: #94a3b8;
font-size: 12px;
font-weight: 400;
}
.worker-order-mobile-shares-body {
display: flex;
flex-direction: column;
gap: 6px;
}
/* 移动端拼单明细:窄屏改用卡片,避免多列表格被压成竖排文字 */
.worker-order-share-cards {
display: flex;
flex-direction: column;
gap: 8px;
}
.worker-order-share-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 10px;
}
.worker-order-share-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.worker-order-share-card-title {
font-size: 13px;
color: #0f172a;
}
.worker-order-share-card-meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.worker-order-share-card-meta-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 5px 8px;
background: #f8fafc;
border-radius: 8px;
min-width: 0;
}
.worker-order-share-card-meta-label {
font-size: 11px;
color: #94a3b8;
line-height: 1.2;
}
.worker-order-share-card-meta-value {
font-size: 13px;
font-weight: 700;
color: #0f172a;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.worker-order-share-card-line {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin: 8px 12px 4px;
padding-top: 8px;
border-top: 1px dashed #f0f0f0;
font-size: 12px;
color: #475569;
}
.worker-order-share-card-line-label {
color: #94a3b8;
font-size: 11px;
flex-shrink: 0;
}
.worker-order-share-card-line-value {
color: #334155;
}
.worker-order-share-card-problem {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
padding: 6px 8px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #b91c1c;
font-size: 12px;
line-height: 1.4;
}
.worker-order-share-card-acceptance {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #475569;
}
.worker-order-share-card-actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 6px;
padding-top: 6px;
border-top: 1px dashed #eef2f7;
}
@@ -155,10 +155,11 @@ export function patchWorkerWorkOrderListEnvelope(
event: WorkerListEvent,
insert = false,
/**
* 就地更新时,快照里没有的「本打手私有字段」保留旧值。
* 就地更新时,快照里没有(或为空)的「本打手私有字段」保留旧值。
*
* 大厅广播是发给所有打手的通用快照,天然没有 `myShare`/`myShares`(那是每个打手自己的份额)。
* 整行替换会把「再拼一次」冲回「拼单 ¥x/份」,所以覆盖行时必须保留。
* 旧版本后端还会显式带 `null`/`[]` 过来;整行替换或直接合并空值都会把
* 「再拼一次」冲回「拼单 ¥x/份」,所以覆盖行时两种情况都要保留。
*/
preserveFields: readonly string[] = [],
/**
@@ -198,7 +199,11 @@ export function patchWorkerWorkOrderListEnvelope(
...(isRecord(event.data) ? event.data : {}),
}
for (const field of preserveFields) {
if (existing && !(field in merged) && field in existing) merged[field] = existing[field]
if (!existing || !(field in existing)) continue
// 快照没有该键,或显式带了 null/[](旧版大厅广播),都视为「没有个人份额信息」。
const incoming = merged[field]
const incomingEmpty = incoming == null || (Array.isArray(incoming) && incoming.length === 0)
if (incomingEmpty) merged[field] = existing[field]
}
nextItems = items.map((entry, itemIndex) => (itemIndex === index ? merged : entry))
} else if (insert) {
@@ -259,6 +264,36 @@ export function sortWorkerHallItems(items: unknown[]): unknown[] {
})
}
/**
* 只把「我的订单」快照里的个人份额字段覆盖到大厅缓存行上。
*
* 大厅广播是发给所有打手的通用快照,已经剔除 myShare/myShares(否则浅合并会
* 冲掉「再拼一次」);但「后台撤销我的份额」「我的份额被验收」这类变化只出现在
* 「我的订单」快照里,需要用后者回填大厅行。这里不增删、不重排,找不到工单就原样返回。
*/
export function patchWorkerHallPersonalShares(
current: unknown,
workOrderId: number,
myShare: unknown,
myShares: unknown,
): unknown {
if (!isRecord(current) || !Array.isArray(current.pages) || workOrderId <= 0) return current
let changed = false
const pages = current.pages.map((page) => {
if (!isRecord(page) || !isRecord(page.data) || !Array.isArray(page.data.items)) return page
let pageChanged = false
const items = page.data.items.map((item) => {
if (!isRecord(item) || toNumber(item.workOrderId) !== workOrderId) return item
pageChanged = true
return { ...item, myShare, myShares }
})
if (!pageChanged) return page
changed = true
return { ...page, data: { ...page.data, items } }
})
return changed ? { ...current, pages } : current
}
/** 无限下拉(大厅):第 1 页允许插入,其余页只做就地更新;最终按置顶优先重排。 */
export function patchWorkerWorkOrderInfiniteEnvelope(
current: unknown,
@@ -5,6 +5,7 @@ import {
compareWorkerHallOrderRows,
evaluateWorkerKeywordVerdict,
matchesWorkerWorkOrderKeyword,
patchWorkerHallPersonalShares,
patchWorkerWorkOrderInfiniteEnvelope,
patchWorkerWorkOrderListEnvelope,
resolveWorkerMyOrderRealtimeVerdict,
@@ -428,6 +429,56 @@ test('大厅广播覆盖行时必须保留本打手的 myShare/myShares,并更
assert.equal(insertedRow?.myShares ?? null, null)
})
test('旧版大厅广播显式带 null/[] 时也不能冲掉本打手份额', () => {
const cachedRow = buildRow({
status: 'open',
myShare: { shareId: 88, quantity: 2, status: 'joined' },
myShares: [{ shareId: 88, quantity: 2, status: 'joined' }],
sharingProgress: { joinedQuantity: 2, pendingSubmissionCount: 1 },
})
const envelope = { pages: [buildEnvelope([cachedRow])], pageParams: [1] }
const patched = patchWorkerWorkOrderInfiniteEnvelope(
envelope,
{
operation: 'upsert',
data: buildRow({
status: 'open',
myShare: null,
myShares: [],
sharingProgress: { joinedQuantity: 3 },
}),
},
true,
['myShare', 'myShares'],
) as { pages: unknown[] }
const row = itemsOf(patched.pages[0])[0] as {
myShare?: { shareId: number }
myShares?: unknown[]
sharingProgress?: { joinedQuantity: number }
}
assert.equal(row.myShare?.shareId, 88, '旧事件不能把「再拼一次」冲掉')
assert.equal(row.myShares?.length, 1)
assert.equal(row.sharingProgress?.joinedQuantity, 3, '公开进度仍取广播最新值')
})
test('「我的订单」快照回填大厅行个人份额:撤销份额后清空,找不到工单不改缓存', () => {
const joined = buildRow({
workOrderId: 5,
status: 'open',
myShare: { shareId: 66, quantity: 1, status: 'joined' },
myShares: [{ shareId: 66, quantity: 1, status: 'joined' }],
})
const envelope = { pages: [buildEnvelope([joined])], pageParams: [1] }
const cleared = patchWorkerHallPersonalShares(envelope, 5, null, []) as { pages: unknown[] }
const row = itemsOf(cleared.pages[0])[0] as { myShare?: unknown; myShares?: unknown[] }
assert.equal(row.myShare ?? null, null, '后台撤掉份额后大厅按钮要能恢复成「拼单」')
assert.deepEqual(row.myShares, [])
// 工单不在大厅缓存里时原样返回,不能凭空造行。
assert.equal(patchWorkerHallPersonalShares(envelope, 999, null, []), envelope)
})
/**
* 线上坑:进大厅后过一会儿「排名靠后的订单跑到前面去」。
*