fix:优化再拼一次bug,优化打手端移动端拼单列表显示
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
/**
|
||||
* 线上坑:进大厅后过一会儿「排名靠后的订单跑到前面去」。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user