From ed81122047ccc11d8118d65a97c11f89ca3205d6 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 22 Aug 2026 09:30:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E5=8E=85=E4=B8=8E=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=B7=A5=E5=8D=95=E5=88=97=E8=A1=A8=E6=94=B9=E4=B8=BA=E7=B2=BE?= =?UTF-8?q?=E7=A1=AE=E8=A1=A5=E4=B8=81=E5=B9=B6=E6=94=AF=E6=8C=81=E6=8A=A2?= =?UTF-8?q?=E5=8D=95=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../realtime/realtime-event-service.ts | 21 ++++++- apps/frontend/src/layouts/AdminLayout.tsx | 14 ++++- apps/frontend/src/layouts/WorkerLayout.tsx | 3 +- .../src/pages/worker/WorkerHallPage.tsx | 55 ++++++++++++++++++- apps/frontend/src/styles/worker.css | 9 +++ .../frontend/src/utils/worker-grab-confirm.ts | 10 ++++ 7 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 apps/frontend/src/utils/worker-grab-confirm.ts diff --git a/.gitignore b/.gitignore index e59e220d..d2c7987f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ apps/backend/data/logs .snow send_code/ .reasonix/ +*.tsbuildinfo diff --git a/apps/backend/src/services/realtime/realtime-event-service.ts b/apps/backend/src/services/realtime/realtime-event-service.ts index 64a59619..3d4778f3 100644 --- a/apps/backend/src/services/realtime/realtime-event-service.ts +++ b/apps/backend/src/services/realtime/realtime-event-service.ts @@ -1,5 +1,7 @@ import type { Response } from 'express' +import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js' + import { getWorkerFinanceRequestById, getWorkerUserById, @@ -292,13 +294,26 @@ async function publishWorkOrderSnapshots(input: { }) if (input.hallChanged) { + // 携带快照的精确事件让客户端原地增删缓存行,避免整页刷新导致抢单误点。 + const hallVisible = + workOrder.status === WORK_ORDER_STATUS.OPEN && + !isWorkOrderSharingFilled( + workOrder.sharing_enabled === true, + Number(workOrder.sharing_total_quantity || 1), + joinedQuantity, + ) publishRealtimeEvent({ type: 'work_order.changed', entityId: workOrderId, scopes: ['worker_hall'], - // 队首补位可能影响多张订单,客户端需要重新读取大厅快照。 - operation: 'refresh', - data: { workOrderId }, + operation: hallVisible ? 'upsert' : 'remove', + data: hallVisible + ? { + ...mapWorkOrderForWorker(workOrder, { depositFreeAmount: 0 }), + requiredDepositAmount: Number(workOrder.required_deposit_amount || 0), + sharingProgress: { joinedQuantity, pendingSubmissionCount }, + } + : { workOrderId }, broadcastWorkers: true, }) } diff --git a/apps/frontend/src/layouts/AdminLayout.tsx b/apps/frontend/src/layouts/AdminLayout.tsx index 37ce7f05..ffd2fe29 100644 --- a/apps/frontend/src/layouts/AdminLayout.tsx +++ b/apps/frontend/src/layouts/AdminLayout.tsx @@ -415,14 +415,22 @@ function patchAdminWorkOrderCaches( .filter(Boolean) const keyword = String(queryKey[2] || '') const workerId = Number(queryKey[3] || 0) - const targetWorkOrderId = Number(queryKey[4] || 0) - const page = Number(queryKey[5] || 1) + const workerLevelIds = String(queryKey[4] || '') + .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 = (!statuses.length || statuses.includes(String(item.status || ''))) && matchesWorkOrderKeyword(item, keyword) && (!workerId || workerId === Number(item.worker?.workerId || 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' queryClient.setQueryData( queryKey, diff --git a/apps/frontend/src/layouts/WorkerLayout.tsx b/apps/frontend/src/layouts/WorkerLayout.tsx index 387610fe..931e39f2 100644 --- a/apps/frontend/src/layouts/WorkerLayout.tsx +++ b/apps/frontend/src/layouts/WorkerLayout.tsx @@ -233,7 +233,8 @@ function patchWorkerHallCaches( event: RealtimeEvent, ) { if (event.operation === 'refresh') { - void queryClient.resetQueries({ queryKey: ['worker-hall-orders'] }) + // 仅大厅全局配置变化走全量刷新;不清缓存,旧列表保持展示避免误点。 + void queryClient.invalidateQueries({ queryKey: ['worker-hall-orders'] }) return } const workerSnapshot = applyWorkerDepositRule(queryClient, event.data) diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index bd4dc726..7a01b892 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -1,6 +1,7 @@ import { ClockCircleOutlined, PushpinOutlined, + QuestionCircleOutlined, ReloadOutlined, SearchOutlined, TrophyOutlined, @@ -19,6 +20,7 @@ import { Modal, Space, Spin, + Switch, Tabs, Tag, Tooltip, @@ -37,6 +39,10 @@ import { } from '@/services/worker' import type { WorkOrder, WorkerLeaderboardPeriod, WorkerUser } from '@/types/worker-platform' import { useIsMobile } from '@/utils/use-is-mobile' +import { + isGrabConfirmEnabled, + setGrabConfirmEnabled as setGrabConfirmEnabledStorage, +} from '@/utils/worker-grab-confirm' import { resolveHallDepositAmount, resolveHallProductName, @@ -53,7 +59,7 @@ const HALL_PAGE_SIZE = 20 export default function WorkerHallPage() { const isMobile = useIsMobile() - const { message } = App.useApp() + const { message, modal } = App.useApp() const queryClient = useQueryClient() const [keywordInput, setKeywordInput] = useState('') const [keyword, setKeyword] = useState('') @@ -64,6 +70,7 @@ export default function WorkerHallPage() { const [leaderboardPeriod, setLeaderboardPeriod] = useState('all') const [sharingQuantity, setSharingQuantity] = useState(1) const [submittingSharing, setSubmittingSharing] = useState(false) + const [grabConfirmEnabled, setGrabConfirmEnabled] = useState(isGrabConfirmEnabled) const loadMoreRef = useRef(null) 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: , + content: `确认抢下「${resolveHallProductName(order)}」?奖励 ${formatMoney( + resolveHallRewardAmount(order), + )}`, + okText: '确认抢单', + cancelText: '取消', + onOk: () => submitGrab(order), + }) + } + async function submitGrab(order: WorkOrder) { setGrabbingId(order.workOrderId) try { @@ -267,6 +296,16 @@ export default function WorkerHallPage() {
+ + @@ -490,6 +529,16 @@ export default function WorkerHallPage() { 抢单大厅
+ + + 抢单确认 + + + diff --git a/apps/frontend/src/styles/worker.css b/apps/frontend/src/styles/worker.css index 6b9822d1..43cd6a55 100644 --- a/apps/frontend/src/styles/worker.css +++ b/apps/frontend/src/styles/worker.css @@ -2181,3 +2181,12 @@ body { padding: 0; font-size: 12px; } + +.worker-hall-grab-confirm-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + color: #6b7280; + font-size: 13px; + white-space: nowrap; +} diff --git a/apps/frontend/src/utils/worker-grab-confirm.ts b/apps/frontend/src/utils/worker-grab-confirm.ts new file mode 100644 index 00000000..28dd1c84 --- /dev/null +++ b/apps/frontend/src/utils/worker-grab-confirm.ts @@ -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') +}