大厅与后台工单列表改为精确补丁并支持抢单确认
This commit is contained in:
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user