大厅与后台工单列表改为精确补丁并支持抢单确认

This commit is contained in:
yml2213
2026-08-22 09:30:38 +08:00
parent 1f7ff016df
commit ed81122047
7 changed files with 103 additions and 10 deletions
+1
View File
@@ -30,3 +30,4 @@ apps/backend/data/logs
.snow .snow
send_code/ send_code/
.reasonix/ .reasonix/
*.tsbuildinfo
@@ -1,5 +1,7 @@
import type { Response } from 'express' import type { Response } from 'express'
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
import { import {
getWorkerFinanceRequestById, getWorkerFinanceRequestById,
getWorkerUserById, getWorkerUserById,
@@ -292,13 +294,26 @@ async function publishWorkOrderSnapshots(input: {
}) })
if (input.hallChanged) { if (input.hallChanged) {
// 携带快照的精确事件让客户端原地增删缓存行,避免整页刷新导致抢单误点。
const hallVisible =
workOrder.status === WORK_ORDER_STATUS.OPEN &&
!isWorkOrderSharingFilled(
workOrder.sharing_enabled === true,
Number(workOrder.sharing_total_quantity || 1),
joinedQuantity,
)
publishRealtimeEvent({ publishRealtimeEvent({
type: 'work_order.changed', type: 'work_order.changed',
entityId: workOrderId, entityId: workOrderId,
scopes: ['worker_hall'], scopes: ['worker_hall'],
// 队首补位可能影响多张订单,客户端需要重新读取大厅快照。 operation: hallVisible ? 'upsert' : 'remove',
operation: 'refresh', data: hallVisible
data: { workOrderId }, ? {
...mapWorkOrderForWorker(workOrder, { depositFreeAmount: 0 }),
requiredDepositAmount: Number(workOrder.required_deposit_amount || 0),
sharingProgress: { joinedQuantity, pendingSubmissionCount },
}
: { workOrderId },
broadcastWorkers: true, broadcastWorkers: true,
}) })
} }
+11 -3
View File
@@ -415,14 +415,22 @@ function patchAdminWorkOrderCaches(
.filter(Boolean) .filter(Boolean)
const keyword = String(queryKey[2] || '') const keyword = String(queryKey[2] || '')
const workerId = Number(queryKey[3] || 0) const workerId = Number(queryKey[3] || 0)
const targetWorkOrderId = Number(queryKey[4] || 0) const workerLevelIds = String(queryKey[4] || '')
const page = Number(queryKey[5] || 1) .split(',')
.map((value) => Number(value))
.filter(Boolean)
const targetWorkOrderId = Number(queryKey[5] || 0)
const page = Number(queryKey[6] || 1)
// 等级筛选的匹配逻辑只在服务端(接单打手或拼单参与者等级),快照不含等级,
// 无法在前端复现:已有行视为匹配(拉取时已通过服务端过滤),但不再插入新行。
const levelFilterActive = workerLevelIds.length > 0
const matches = const matches =
(!statuses.length || statuses.includes(String(item.status || ''))) && (!statuses.length || statuses.includes(String(item.status || ''))) &&
matchesWorkOrderKeyword(item, keyword) && matchesWorkOrderKeyword(item, keyword) &&
(!workerId || workerId === Number(item.worker?.workerId || 0)) && (!workerId || workerId === Number(item.worker?.workerId || 0)) &&
(!targetWorkOrderId || targetWorkOrderId === Number(item.workOrderId || 0)) (!targetWorkOrderId || targetWorkOrderId === Number(item.workOrderId || 0))
const shouldInsert = event.operation === 'upsert' && page === 1 && matches const shouldInsert =
event.operation === 'upsert' && page === 1 && matches && !levelFilterActive
const operation = event.operation === 'remove' || !matches ? 'remove' : 'upsert' const operation = event.operation === 'remove' || !matches ? 'remove' : 'upsert'
queryClient.setQueryData( queryClient.setQueryData(
queryKey, queryKey,
+2 -1
View File
@@ -233,7 +233,8 @@ function patchWorkerHallCaches(
event: RealtimeEvent, event: RealtimeEvent,
) { ) {
if (event.operation === 'refresh') { if (event.operation === 'refresh') {
void queryClient.resetQueries({ queryKey: ['worker-hall-orders'] }) // 仅大厅全局配置变化走全量刷新;不清缓存,旧列表保持展示避免误点。
void queryClient.invalidateQueries({ queryKey: ['worker-hall-orders'] })
return return
} }
const workerSnapshot = applyWorkerDepositRule(queryClient, event.data) const workerSnapshot = applyWorkerDepositRule(queryClient, event.data)
@@ -1,6 +1,7 @@
import { import {
ClockCircleOutlined, ClockCircleOutlined,
PushpinOutlined, PushpinOutlined,
QuestionCircleOutlined,
ReloadOutlined, ReloadOutlined,
SearchOutlined, SearchOutlined,
TrophyOutlined, TrophyOutlined,
@@ -19,6 +20,7 @@ import {
Modal, Modal,
Space, Space,
Spin, Spin,
Switch,
Tabs, Tabs,
Tag, Tag,
Tooltip, Tooltip,
@@ -37,6 +39,10 @@ import {
} from '@/services/worker' } from '@/services/worker'
import type { WorkOrder, WorkerLeaderboardPeriod, WorkerUser } from '@/types/worker-platform' import type { WorkOrder, WorkerLeaderboardPeriod, WorkerUser } from '@/types/worker-platform'
import { useIsMobile } from '@/utils/use-is-mobile' import { useIsMobile } from '@/utils/use-is-mobile'
import {
isGrabConfirmEnabled,
setGrabConfirmEnabled as setGrabConfirmEnabledStorage,
} from '@/utils/worker-grab-confirm'
import { import {
resolveHallDepositAmount, resolveHallDepositAmount,
resolveHallProductName, resolveHallProductName,
@@ -53,7 +59,7 @@ const HALL_PAGE_SIZE = 20
export default function WorkerHallPage() { export default function WorkerHallPage() {
const isMobile = useIsMobile() const isMobile = useIsMobile()
const { message } = App.useApp() const { message, modal } = App.useApp()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [keywordInput, setKeywordInput] = useState('') const [keywordInput, setKeywordInput] = useState('')
const [keyword, setKeyword] = useState('') const [keyword, setKeyword] = useState('')
@@ -64,6 +70,7 @@ export default function WorkerHallPage() {
const [leaderboardPeriod, setLeaderboardPeriod] = useState<WorkerLeaderboardPeriod>('all') const [leaderboardPeriod, setLeaderboardPeriod] = useState<WorkerLeaderboardPeriod>('all')
const [sharingQuantity, setSharingQuantity] = useState(1) const [sharingQuantity, setSharingQuantity] = useState(1)
const [submittingSharing, setSubmittingSharing] = useState(false) const [submittingSharing, setSubmittingSharing] = useState(false)
const [grabConfirmEnabled, setGrabConfirmEnabled] = useState(isGrabConfirmEnabled)
const loadMoreRef = useRef<HTMLDivElement | null>(null) const loadMoreRef = useRef<HTMLDivElement | null>(null)
const ordersQuery = useInfiniteQuery({ const ordersQuery = useInfiniteQuery({
@@ -163,6 +170,28 @@ export default function WorkerHallPage() {
]) ])
} }
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),
})
}
async function submitGrab(order: WorkOrder) { async function submitGrab(order: WorkOrder) {
setGrabbingId(order.workOrderId) setGrabbingId(order.workOrderId)
try { try {
@@ -267,6 +296,16 @@ export default function WorkerHallPage() {
</div> </div>
<div className="worker-hall-mobile-top-actions"> <div className="worker-hall-mobile-top-actions">
<Tooltip title={`抢单确认已${grabConfirmEnabled ? '开启' : '关闭'}`}>
<Button
aria-label="抢单确认开关"
size="small"
shape="circle"
icon={<QuestionCircleOutlined />}
type={grabConfirmEnabled ? 'primary' : 'default'}
onClick={() => toggleGrabConfirm(!grabConfirmEnabled)}
/>
</Tooltip>
<Tooltip title="接单排行榜"> <Tooltip title="接单排行榜">
<Button <Button
aria-label="接单排行榜" aria-label="接单排行榜"
@@ -468,7 +507,7 @@ export default function WorkerHallPage() {
className="worker-hall-mobile-grab-btn" className="worker-hall-mobile-grab-btn"
disabled={Boolean(disabledReason)} disabled={Boolean(disabledReason)}
loading={grabbingId === order.workOrderId} loading={grabbingId === order.workOrderId}
onClick={() => submitGrab(order)} onClick={() => requestGrab(order)}
> >
{disabledReason || '立即抢单'} {disabledReason || '立即抢单'}
</Button> </Button>
@@ -490,6 +529,16 @@ export default function WorkerHallPage() {
<Typography.Title level={3}></Typography.Title> <Typography.Title level={3}></Typography.Title>
<div className="worker-hall-page-actions"> <div className="worker-hall-page-actions">
<div className="worker-hall-head-controls"> <div className="worker-hall-head-controls">
<Tooltip title="开启后点击抢单会先弹窗确认,防止误点">
<span className="worker-hall-grab-confirm-toggle">
<Switch
size="small"
checked={grabConfirmEnabled}
onChange={toggleGrabConfirm}
/>
</span>
</Tooltip>
<Button <Button
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
loading={ loading={
@@ -693,7 +742,7 @@ export default function WorkerHallPage() {
size="large" size="large"
disabled={Boolean(disabledReason)} disabled={Boolean(disabledReason)}
loading={grabbingId === order.workOrderId} loading={grabbingId === order.workOrderId}
onClick={() => submitGrab(order)} onClick={() => requestGrab(order)}
> >
{disabledReason || '立即抢单'} {disabledReason || '立即抢单'}
</Button> </Button>
+9
View File
@@ -2181,3 +2181,12 @@ body {
padding: 0; padding: 0;
font-size: 12px; font-size: 12px;
} }
.worker-hall-grab-confirm-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
color: #6b7280;
font-size: 13px;
white-space: nowrap;
}
@@ -0,0 +1,10 @@
const GRAB_CONFIRM_KEY = 'order-site-worker-grab-confirm'
/** 抢单二次确认开关,默认关闭,由打手自行开启。 */
export function isGrabConfirmEnabled() {
return window.localStorage.getItem(GRAB_CONFIRM_KEY) === '1'
}
export function setGrabConfirmEnabled(enabled: boolean) {
window.localStorage.setItem(GRAB_CONFIRM_KEY, enabled ? '1' : '0')
}