From 85fa8f11fdb06c0a5c841b50fad4cc5c1011a9a9 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 14 Aug 2026 17:34:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=8D=95=E5=B7=A5=E5=8D=95?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E5=8F=8C=E5=87=BB=E7=BC=96=E8=BE=91=E4=B8=8E?= =?UTF-8?q?=E8=B5=84=E6=96=99=E7=B2=98=E8=B4=B4=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 表格双击发单金额/押金原地编辑,自动聚焦全选,Esc 取消 - 发单金额高于用户付款金额时保存前弹确认提示(编辑弹窗同步) - 亏损单(发单金额>付款金额)在列表中红色加粗显示,便于定位 - Mock 工单支持填写用户付款金额(留空默认同接单金额),便于本地测试 - 人工补充资料支持客服消息剪贴板粘贴自动识别填充(系统/区服/角色名称/游戏编号,区服选填有则识别) --- .../services/worker-platform/admin-service.ts | 40 ++- .../pages/admin/panels/WorkOrdersPanel.tsx | 286 ++++++++++++++++-- .../src/services/admin/worker-platform.ts | 1 + 3 files changed, 295 insertions(+), 32 deletions(-) diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index b89ca1a9..2af2e1fc 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -797,24 +797,40 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) { ? rewardAmount : 0 : normalizeAmountFen(payload.requiredDepositAmount, 0) + const paymentFen = + payload.paymentAmount === undefined && + payload.paymentAmountYuan === undefined && + payload.userPaymentAmount === undefined + ? rewardAmount + : normalizeAmountFen( + payload.paymentAmount ?? payload.paymentAmountYuan ?? payload.userPaymentAmount, + rewardAmount, + ) const materialComplete = payload.materialComplete === true || payload.materialComplete === 'true' const fields = normalizeRequirementFields(payload.fields) const platformOrderId = String( payload.platformOrderId || payload.orderNo || `MOCKWORK${Date.now()}`, ).trim() const productName = String(payload.productName || '指挥官秘钥1个').trim() - const material = materialComplete - ? { - collect: { - complete: true, - submittedAt: now, - fields: fields.reduce>((result, field) => { - result[field.key] = String(field.mockValue || `${field.label}测试值`) - return result - }, {}), - }, - } - : {} + const material = { + ...(materialComplete + ? { + collect: { + complete: true, + submittedAt: now, + fields: fields.reduce>((result, field) => { + result[field.key] = String(field.mockValue || `${field.label}测试值`) + return result + }, {}), + }, + } + : {}), + source: { + orderAmountFen: paymentFen, + syncSource: 'mock', + mock: true, + }, + } const workOrder = await createWorkOrder({ workOrderNo: randomId('WO'), platformOrderId, diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 741fd58a..aca9391b 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -34,7 +34,7 @@ import { Typography, } from 'antd' import type { TableColumnsType } from 'antd' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router' import JsonPreview from '@/components/admin/JsonPreview' @@ -126,6 +126,7 @@ export default function WorkOrdersPanel() { const [resolutionOrder, setResolutionOrder] = useState(null) const [materialOrder, setMaterialOrder] = useState(null) const [materialScreenshots, setMaterialScreenshots] = useState([]) + const [materialPasteText, setMaterialPasteText] = useState('') const [sharingOrder, setSharingOrder] = useState(null) const [assignOrder, setAssignOrder] = useState(null) const [assignWorkerId, setAssignWorkerId] = useState(undefined) @@ -177,6 +178,32 @@ export default function WorkOrdersPanel() { setAssignWorkerId(undefined) } + function applyMaterialPaste(text: string) { + if (!materialOrder) return + const recognized = parseWorkerMaterialText(text) + const keys = Object.keys(recognized) as Array + if (keys.length === 0) { + message.warning('未识别到可填充的字段,请检查粘贴内容格式') + return + } + const requirementFields = getRequirementFields(materialOrder) + const availableKeys = new Set(requirementFields.map((field) => field.key)) + const next: Record = {} + const filled: string[] = [] + for (const key of keys) { + if (!availableKeys.has(key)) continue + next[key] = recognized[key] + const label = requirementFields.find((field) => field.key === key)?.label || key + filled.push(`${label}=${recognized[key]}`) + } + if (filled.length === 0) { + message.warning('识别到的字段与当前资料要求不匹配') + return + } + materialForm.setFieldsValue({ fields: next }) + message.success(`已自动填写:${filled.join(',')}`) + } + async function submitAssign() { if (!assignOrder || !assignWorkerId) return const succeeded = await runAction( @@ -396,20 +423,45 @@ export default function WorkOrdersPanel() { timeoutPolicy?: string }) { if (!editOrder) return - const succeeded = await runAction( - () => - updateAdminWorkOrder(editOrder.workOrderId, { - productName: String(values.productName || '').trim(), - platformOrderId: String(values.platformOrderId || '').trim(), - categoryId: values.categoryId || null, - rewardAmount: Number(values.rewardAmount || 0), - requiredDepositAmount: Number(values.requiredDepositAmount || 0), - fieldsText: String(values.fieldsText || ''), - timeoutMinutes: Math.max(0, Number(values.timeoutMinutes || 0)), - timeoutPolicy: values.timeoutPolicy || 'reopen', - }), - '工单信息已保存', - ) + const doSave = () => + runAction( + () => + updateAdminWorkOrder(editOrder.workOrderId, { + productName: String(values.productName || '').trim(), + platformOrderId: String(values.platformOrderId || '').trim(), + categoryId: values.categoryId || null, + rewardAmount: Number(values.rewardAmount || 0), + requiredDepositAmount: Number(values.requiredDepositAmount || 0), + fieldsText: String(values.fieldsText || ''), + timeoutMinutes: Math.max(0, Number(values.timeoutMinutes || 0)), + timeoutPolicy: values.timeoutPolicy || 'reopen', + }), + '工单信息已保存', + ) + const paymentFen = getUserPaymentFen(editOrder) + const rewardFen = Math.round(Number(values.rewardAmount || 0) * 100) + if (paymentFen !== null && rewardFen > paymentFen) { + modal.confirm({ + title: '发单金额高于用户付款金额', + content: ( + + 该订单用户付款 {formatMoney(paymentFen)},发单金额{' '} + {formatMoney(rewardFen)} + ,本单平台将亏损。确认保存? + + ), + okText: '确认保存', + cancelText: '取消', + onOk: async () => { + const succeeded = await doSave() + if (!succeeded) return + setEditOrder(null) + editForm.resetFields() + }, + }) + return + } + const succeeded = await doSave() if (!succeeded) return setEditOrder(null) editForm.resetFields() @@ -461,13 +513,17 @@ export default function WorkOrdersPanel() { }, { title: '发单金额', - width: 110, - render: (_, row) => formatMoney(row.rewardAmount), + width: 130, + render: (_, row) => ( + + ), }, { title: '押金', - width: 110, - render: (_, row) => formatMoney(row.requiredDepositAmount), + width: 130, + render: (_, row) => ( + + ), }, { title: '时限', @@ -1132,12 +1188,42 @@ export default function WorkOrdersPanel() { open={Boolean(materialOrder)} onCancel={() => { setMaterialOrder(null) + setMaterialPasteText('') materialForm.resetFields() }} onOk={() => materialForm.submit()} destroyOnHidden >
+ + + setMaterialPasteText(event.target.value)} + onPaste={(event) => { + const text = event.clipboardData?.getData('text/plain') || '' + if (!text.trim()) return + event.preventDefault() + applyMaterialPaste(text) + setMaterialPasteText('') + }} + placeholder={ + '粘贴客服消息后自动识别填充,格式如:\n安卓还是苹果(必填):安卓\nQ区还是V区(必填):Q区\n角色名称(必填):张三\n游戏编号(必填):123456' + } + /> + + + {getRequirementFields(materialOrder).map((field) => ( Promise | void +}) { + const { message, modal } = App.useApp() + const [editing, setEditing] = useState(false) + const [draft, setDraft] = useState(null) + const savingRef = useRef(false) + + const editable = + ['pending_material', 'unassigned', 'open'].includes(order.status) && !order.worker + const currentFen = Number(order[field] || 0) + const currentYuan = currentFen / 100 + const paymentFen = field === 'rewardAmount' ? getUserPaymentFen(order) : null + const isLossOrder = paymentFen !== null && currentFen > paymentFen + + async function doSave(nextYuan: number) { + if (savingRef.current) return + savingRef.current = true + try { + await updateAdminWorkOrder(order.workOrderId, { [field]: nextYuan }) + message.success('金额已更新') + setEditing(false) + await onSaved() + } catch (error) { + message.error(error instanceof Error ? error.message : '保存失败') + } finally { + savingRef.current = false + } + } + + async function save() { + if (savingRef.current) return + if (draft === null || !Number.isFinite(Number(draft))) { + setEditing(false) + return + } + const nextYuan = Number(draft) + if (nextYuan < 0) { + message.error('金额不能为负数') + return + } + if (Math.abs(nextYuan - currentYuan) < 0.005) { + setEditing(false) + return + } + const nextFen = Math.round(nextYuan * 100) + if (paymentFen !== null && nextFen > paymentFen) { + modal.confirm({ + title: '发单金额高于用户付款金额', + content: ( + + 该订单用户付款 {formatMoney(paymentFen)},发单金额{' '} + {formatMoney(nextFen)} + ,本单平台将亏损。确认保存? + + ), + okText: '确认保存', + cancelText: '取消', + onOk: () => doSave(nextYuan), + }) + return + } + await doSave(nextYuan) + } + + if (!editing) { + return ( + { + if (!editable) return + setDraft(currentYuan) + setEditing(true) + }} + > + {formatMoney(currentFen)} + + ) + } + + return ( + event.target.select()} + onPressEnter={() => void save()} + onBlur={() => void save()} + onKeyDown={(event) => { + if (event.key === 'Escape') setEditing(false) + }} + /> + ) +} + +/** 解析客服粘贴文本(如「安卓还是苹果(必填):安卓」),返回字段 key → 值 */ +function parseWorkerMaterialText(text: string): Record { + const result: Record = {} + const lines = String(text || '') + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + for (const line of lines) { + const match = line.match(/^(.+?)[::]\s*(.+)$/) + if (!match) continue + const label = match[1] + .replace(/[((](必填|选填)[))]/g, '') + .trim() + .toLowerCase() + const value = match[2].trim() + if (!label || !value) continue + + if (/系统|安卓|苹果/.test(label)) { + if (value.includes('苹果')) result.system = '苹果' + else if (value.includes('安卓')) result.system = '安卓' + } else if (/区/.test(label) || /服/.test(label)) { + const zoneValue = value.toLowerCase() + if (zoneValue.includes('微信') || /^v/.test(zoneValue)) result.serverZone = '微信区' + else if (zoneValue.includes('q')) result.serverZone = 'QQ区' + } else if (/角色|昵称|名称/.test(label)) { + result.gameNickname = value + } else if (/编号|游戏id|账号id|^id|uid/.test(label)) { + result.gameId = value + } + } + return result } function formatRequirementFieldsText(fields: CollectField[]): string { @@ -1373,6 +1610,7 @@ function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise }) { platformOrderId?: string productName?: string rewardAmount?: number + paymentAmount?: number materialComplete?: boolean }) { setLoading(true) @@ -1407,6 +1645,14 @@ function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise }) { + + + (