diff --git a/apps/backend/src/db/migrations/019_acceptance_draft.sql b/apps/backend/src/db/migrations/019_acceptance_draft.sql new file mode 100644 index 00000000..805e3aa0 --- /dev/null +++ b/apps/backend/src/db/migrations/019_acceptance_draft.sql @@ -0,0 +1,18 @@ +-- 019_acceptance_draft.sql —— 验收图片暂存(草稿)。 +-- +-- 说明: +-- 1. 上传图片与提交验收分离:打手可先把验收图片/说明保存为暂存草稿, +-- 不改变工单状态,刷新页面不丢失;提交验收时草稿作为初始内容; +-- 2. 已提交验收(pending_acceptance)的工单仍可补充/修改图片, +-- 直接更新 acceptance_json(保留原 submitted_at); +-- 3. 整单草稿存 work_orders.draft_acceptance_json, +-- 拼单草稿存各自 work_order_shares.draft_acceptance_json。 + +ALTER TABLE work_orders + ADD COLUMN IF NOT EXISTS draft_acceptance_json JSONB NOT NULL DEFAULT '{}'::jsonb; + +ALTER TABLE work_order_shares + ADD COLUMN IF NOT EXISTS draft_acceptance_json JSONB NOT NULL DEFAULT '{}'::jsonb; + +COMMENT ON COLUMN work_orders.draft_acceptance_json IS '打手暂存的验收草稿({note, files, imageUrls, updatedAt}),未提交时保存于此,提交后清空'; +COMMENT ON COLUMN work_order_shares.draft_acceptance_json IS '拼单打手各自暂存的验收草稿({note, files, imageUrls, updatedAt}),提交后清空'; diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 0a7fc1e0..b667d9ba 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -144,6 +144,7 @@ export type WorkOrderShareRow = { share_deposit: number status: string acceptance_json: string | Record + draft_acceptance_json: string | Record submitted_at: string | null accepted_at: string | null created_at: string @@ -170,6 +171,7 @@ export type WorkOrderRow = { material_json: string | Record requirement_json: string | Record acceptance_json: string | Record + draft_acceptance_json: string | Record problem_note: string sharing_enabled: boolean sharing_total_quantity: number diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index bdc6f752..0d57517e 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -250,12 +250,23 @@ export async function submitWorkOrderShareAcceptance(input: { now: string }): Promise<{ share: WorkOrderShareRow | null; failureReason: 'share_not_found' | 'share_status_invalid' | null }> { return withTransaction(async (client) => { - const shareResult = await client.query( - `${WORK_ORDER_SHARE_SELECT} - WHERE wos.work_order_id = $1 AND wos.worker_id = $2 - FOR UPDATE`, + const lockResult = await client.query<{ id: number }>( + ` + SELECT id + FROM work_order_shares + WHERE work_order_id = $1 AND worker_id = $2 + FOR UPDATE + `, [input.workOrderId, input.workerId], ) + const shareId = lockResult.rows[0]?.id || 0 + if (!shareId) { + return { share: null, failureReason: 'share_not_found' } + } + const shareResult = await client.query( + `${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`, + [shareId], + ) const share = shareResult.rows[0] || null if (!share) { return { share: null, failureReason: 'share_not_found' } @@ -266,7 +277,7 @@ export async function submitWorkOrderShareAcceptance(input: { await client.query( ` UPDATE work_order_shares - SET status = 'submitted', acceptance_json = $1::jsonb, submitted_at = $2, updated_at = $2 + SET status = 'submitted', acceptance_json = $1::jsonb, draft_acceptance_json = '{}'::jsonb, submitted_at = $2, updated_at = $2 WHERE id = $3 `, [input.acceptanceJson, input.now, Number(share.id)], @@ -289,6 +300,90 @@ export async function submitWorkOrderShareAcceptance(input: { }) } +/** 保存整单验收草稿(暂存图片/说明),不改变工单状态 */ +export async function updateWorkOrderDraftAcceptance(input: { + workOrderId: number + draftJson: string + now: string +}): Promise { + await query( + ` + UPDATE work_orders + SET draft_acceptance_json = $1::jsonb, updated_at = $2 + WHERE id = $3 + `, + [input.draftJson, input.now, Number(input.workOrderId)], + ) + return getWorkOrderById(input.workOrderId) +} + +/** 更新整单已提交的验收资料(保留原 submitted_at,用于提交验收后补充/修改图片) */ +export async function updateWorkOrderAcceptanceRecord(input: { + workOrderId: number + acceptanceJson: string + now: string +}): Promise { + await query( + ` + UPDATE work_orders + SET acceptance_json = $1::jsonb, updated_at = $2 + WHERE id = $3 + `, + [input.acceptanceJson, input.now, Number(input.workOrderId)], + ) + return getWorkOrderById(input.workOrderId) +} + +/** 保存拼单份额的验收草稿(暂存图片/说明),不改变份额状态 */ +export async function updateWorkOrderShareDraftAcceptance(input: { + workOrderId: number + workerId: number + draftJson: string + now: string +}): Promise { + const result = await query( + `${WORK_ORDER_SHARE_SELECT} + WHERE wos.work_order_id = $1 AND wos.worker_id = $2`, + [Number(input.workOrderId), Number(input.workerId)], + ) + const share = result.rows[0] || null + if (!share) return null + await query( + ` + UPDATE work_order_shares + SET draft_acceptance_json = $1::jsonb, updated_at = $2 + WHERE id = $3 + `, + [input.draftJson, input.now, Number(share.id)], + ) + return getWorkOrderShareById(share.id) +} + +/** 更新拼单份额已提交的验收资料(保留原 submitted_at,用于提交验收后补充/修改图片) */ +export async function updateWorkOrderShareAcceptanceRecord(input: { + workOrderId: number + workerId: number + acceptanceJson: string + now: string +}): Promise { + const result = await query( + `${WORK_ORDER_SHARE_SELECT} + WHERE wos.work_order_id = $1 AND wos.worker_id = $2`, + [Number(input.workOrderId), Number(input.workerId)], + ) + const share = result.rows[0] || null + if (!share) return null + await query( + ` + UPDATE work_order_shares + SET acceptance_json = $1::jsonb, updated_at = $2 + WHERE id = $3 + `, + [input.acceptanceJson, input.now, Number(share.id)], + ) + return getWorkOrderShareById(share.id) +} + export async function getWorkCategoryByKey(categoryKey: string): Promise { const result = await query( 'SELECT * FROM work_categories WHERE category_key = $1 LIMIT 1', @@ -642,6 +737,7 @@ export async function updateWorkOrder( | 'assigned_worker_id' | 'material_json' | 'acceptance_json' + | 'draft_acceptance_json' | 'problem_note' | 'reward_amount' | 'sharing_enabled' @@ -666,23 +762,25 @@ export async function updateWorkOrder( assigned_worker_id = $2, material_json = $3::jsonb, acceptance_json = $4::jsonb, - problem_note = $5, - reward_amount = $6, - sharing_enabled = $7, - sharing_total_quantity = $8, - sharing_unit_reward = $9, - published_at = $10, - assigned_at = $11, - submitted_at = $12, - accepted_at = $13, - updated_at = $14 - WHERE id = $15 + draft_acceptance_json = $5::jsonb, + problem_note = $6, + reward_amount = $7, + sharing_enabled = $8, + sharing_total_quantity = $9, + sharing_unit_reward = $10, + published_at = $11, + assigned_at = $12, + submitted_at = $13, + accepted_at = $14, + updated_at = $15 + WHERE id = $16 `, [ next.status, next.assigned_worker_id || null, toJsonString(next.material_json), toJsonString(next.acceptance_json), + toJsonString(next.draft_acceptance_json), next.problem_note || '', toPositiveInteger(next.reward_amount, 0), next.sharing_enabled === true, diff --git a/apps/backend/src/routes/worker.ts b/apps/backend/src/routes/worker.ts index f4249a34..455ef8cf 100644 --- a/apps/backend/src/routes/worker.ts +++ b/apps/backend/src/routes/worker.ts @@ -20,6 +20,7 @@ import { logoutWorkerSession, registerWorker, sendWorkerSmsCode, + saveWorkerOrderAcceptanceDraft, submitWorkerOrderAcceptance, } from '../services/worker-platform/index.js' import { buildNotFoundPayload, createRouteHandler } from '../utils/http.js' @@ -246,6 +247,24 @@ router.post( ), ) +router.post( + '/orders/:workOrderId/acceptance-draft', + requireActiveWorker, + createRouteHandler( + (req) => + saveWorkerOrderAcceptanceDraft( + String(req.params.workOrderId || ''), + req.body || {}, + getRequiredWorkerSession(req), + ), + { + successMessage: '验收图片已保存', + errorMessage: '保存验收图片失败', + scope: '[worker/orders/:workOrderId/acceptance-draft]', + }, + ), +) + router.post( '/orders/:workOrderId/cancel', requireActiveWorker, diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index 03e7d3d2..c73bc4ff 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -394,6 +394,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow) { fields: resolveRequirementFields(workOrder), }, acceptance: mapAcceptanceForResponse(workOrder.acceptance_json), + draftAcceptance: mapAcceptanceForResponse(workOrder.draft_acceptance_json), problemNote: workOrder.problem_note || '', worker: workOrder.assigned_worker_id ? { @@ -443,6 +444,7 @@ export function mapWorkOrderShare(share: WorkOrderShareRow) { shareDeposit: Number(share.share_deposit || 0), status: share.status, acceptance: safeParseJson(share.acceptance_json), + draftAcceptance: safeParseJson(share.draft_acceptance_json), submittedAt: share.submitted_at, acceptedAt: share.accepted_at, createdAt: share.created_at, diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 5319a859..1cbdfc7a 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -63,6 +63,10 @@ import { settleOverdueWorkOrder, submitWorkOrderShareAcceptance, updateWorkOrder, + updateWorkOrderAcceptanceRecord, + updateWorkOrderDraftAcceptance, + updateWorkOrderShareAcceptanceRecord, + updateWorkOrderShareDraftAcceptance, updateWorkerPassword, updateWorkerUser, upsertWorkCategory, @@ -1111,11 +1115,10 @@ export async function submitWorkerOrderAcceptance( }) } - const files = normalizeUploadedFiles(payload.files) - const imageUrls = [ - ...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean), - ...normalizeStringArray(payload.imageUrls), - ] + const { files, imageUrls, note } = resolveAcceptanceImages( + payload, + workOrder.draft_acceptance_json, + ) if (imageUrls.length === 0) { throw createHttpError('请上传验收图片', { statusCode: 400, @@ -1123,7 +1126,7 @@ export async function submitWorkerOrderAcceptance( }) } const acceptance = { - note: String(payload.note || '').trim(), + note, files, imageUrls: [...new Set(imageUrls)], submittedAt: now, @@ -1131,6 +1134,7 @@ export async function submitWorkerOrderAcceptance( const updated = await updateWorkOrder(workOrder.id, { status: WORK_ORDER_STATUS.PENDING_ACCEPTANCE, acceptance_json: acceptance, + draft_acceptance_json: {}, submitted_at: now, updated_at: now, }) @@ -1198,11 +1202,10 @@ async function submitWorkerSharingAcceptance( }) } const now = nowIso() - const files = normalizeUploadedFiles(payload.files) - const imageUrls = [ - ...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean), - ...normalizeStringArray(payload.imageUrls), - ] + const { files, imageUrls, note } = resolveAcceptanceImages( + payload, + sharingShare.draft_acceptance_json, + ) if (imageUrls.length === 0) { throw createHttpError('请上传验收图片', { statusCode: 400, @@ -1210,7 +1213,7 @@ async function submitWorkerSharingAcceptance( }) } const acceptance = { - note: String(payload.note || '').trim(), + note, files, imageUrls: [...new Set(imageUrls)], submittedAt: now, @@ -1239,6 +1242,221 @@ async function submitWorkerSharingAcceptance( } } +/** + * 从提交/保存验收的载荷中解析图片与说明。 + * 载荷未带图片时回退到暂存草稿(上传图片与提交验收分离:先暂存、后提交)。 + */ +function resolveAcceptanceImages(payload: JsonObject, draftValue: unknown) { + const files = normalizeUploadedFiles(payload.files) + const imageUrls = [ + ...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean), + ...normalizeStringArray(payload.imageUrls), + ] + if (imageUrls.length > 0) { + return { + files, + imageUrls: [...new Set(imageUrls)], + note: String(payload.note ?? '').trim(), + } + } + const draft = safeParseJson(draftValue) + return { + files: normalizeUploadedFiles(draft.files), + imageUrls: [...new Set(normalizeStringArray(draft.imageUrls))], + note: String(payload.note ?? draft.note ?? '').trim(), + } +} + +/** + * 暂存验收图片/说明(草稿)。 + * - 未提交(in_progress / problem / 拼单 joined):写入草稿,不改变工单状态; + * - 已提交(pending_acceptance / 拼单 submitted):直接更新已提交的验收资料(保留提交时间)。 + */ +export async function saveWorkerOrderAcceptanceDraft( + workOrderId: number | string, + payload: JsonObject = {}, + session: WorkerSession, +) { + requireActiveWorkerSession(session) + const workOrder = await getRequiredWorkOrder(workOrderId) + const workerId = Number(session.workerId) + const sharingShare = await getWorkOrderShare(workOrder.id, workerId) + if (sharingShare) { + return saveWorkerSharingAcceptanceDraft(workOrder, sharingShare, payload, session) + } + if (Number(workOrder.assigned_worker_id || 0) !== workerId) { + throw createHttpError('只能操作自己的订单', { + statusCode: 403, + errorCode: 'work_order_owner_required', + }) + } + const now = nowIso() + const files = normalizeUploadedFiles(payload.files) + const imageUrls = [ + ...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean), + ...normalizeStringArray(payload.imageUrls), + ] + const worker = await getRequiredWorker(session.workerId) + + if (workOrder.status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE) { + if (imageUrls.length === 0) { + throw createHttpError('请至少保留一张验收图片', { + statusCode: 400, + errorCode: 'work_order_acceptance_image_required', + }) + } + const previous = safeParseJson(workOrder.acceptance_json) + const acceptance = { + note: String(payload.note ?? previous.note ?? '').trim(), + files, + imageUrls: [...new Set(imageUrls)], + submittedAt: previous.submittedAt || workOrder.submitted_at || now, + updatedAt: now, + } + const updated = await updateWorkOrderAcceptanceRecord({ + workOrderId: workOrder.id, + acceptanceJson: JSON.stringify(acceptance), + now, + }) + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'worker', + actorId: String(session.workerId), + eventType: 'acceptance_updated', + fromStatus: workOrder.status, + toStatus: workOrder.status, + payloadJson: JSON.stringify({ imageCount: imageUrls.length }), + now, + }) + return { + order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)), + } + } + + if ( + ![WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PROBLEM].includes(workOrder.status as never) + ) { + throw createHttpError('当前状态不能保存验收图片', { + statusCode: 409, + errorCode: 'work_order_draft_status_invalid', + }) + } + const draft = { + note: String(payload.note || '').trim(), + files, + imageUrls: [...new Set(imageUrls)], + updatedAt: now, + } + const updated = await updateWorkOrderDraftAcceptance({ + workOrderId: workOrder.id, + draftJson: JSON.stringify(draft), + now, + }) + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'worker', + actorId: String(session.workerId), + eventType: 'acceptance_draft_saved', + fromStatus: workOrder.status, + toStatus: workOrder.status, + payloadJson: JSON.stringify({ imageCount: imageUrls.length }), + now, + }) + return { + order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)), + } +} + +async function saveWorkerSharingAcceptanceDraft( + workOrder: WorkOrderRow, + sharingShare: WorkOrderShareRow, + payload: JsonObject = {}, + session: WorkerSession, +) { + if (workOrder.status === WORK_ORDER_STATUS.ACCEPTED) { + throw createHttpError('该拼单已验收完成,不能再修改验收图片', { + statusCode: 409, + errorCode: 'work_order_sharing_accepted', + }) + } + const now = nowIso() + const files = normalizeUploadedFiles(payload.files) + const imageUrls = [ + ...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean), + ...normalizeStringArray(payload.imageUrls), + ] + + if (sharingShare.status === 'submitted') { + if (imageUrls.length === 0) { + throw createHttpError('请至少保留一张验收图片', { + statusCode: 400, + errorCode: 'work_order_acceptance_image_required', + }) + } + const previous = safeParseJson(sharingShare.acceptance_json) + const acceptance = { + note: String(payload.note ?? previous.note ?? '').trim(), + files, + imageUrls: [...new Set(imageUrls)], + submittedAt: previous.submittedAt || sharingShare.submitted_at || now, + updatedAt: now, + } + const share = await updateWorkOrderShareAcceptanceRecord({ + workOrderId: Number(workOrder.id), + workerId: Number(session.workerId), + acceptanceJson: JSON.stringify(acceptance), + now, + }) + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'worker', + actorId: String(session.workerId), + eventType: 'sharing_acceptance_updated', + fromStatus: workOrder.status, + toStatus: workOrder.status, + payloadJson: JSON.stringify({ + workerId: Number(session.workerId), + imageCount: imageUrls.length, + }), + now, + }) + return { share: share ? mapWorkOrderShare(share) : mapWorkOrderShare(sharingShare) } + } + + if (sharingShare.status !== 'joined') { + throw createHttpError('当前拼单状态不能保存验收图片', { + statusCode: 409, + errorCode: 'work_order_sharing_draft_status_invalid', + }) + } + const draft = { + note: String(payload.note || '').trim(), + files, + imageUrls: [...new Set(imageUrls)], + updatedAt: now, + } + const share = await updateWorkOrderShareDraftAcceptance({ + workOrderId: Number(workOrder.id), + workerId: Number(session.workerId), + draftJson: JSON.stringify(draft), + now, + }) + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'worker', + actorId: String(session.workerId), + eventType: 'sharing_acceptance_draft_saved', + fromStatus: workOrder.status, + toStatus: workOrder.status, + payloadJson: JSON.stringify({ + workerId: Number(session.workerId), + imageCount: imageUrls.length, + }), + now, + }) + return { share: share ? mapWorkOrderShare(share) : mapWorkOrderShare(sharingShare) } +} + export async function collectLookupWorkOrder(payload: JsonObject = {}) { const orderNo = String(payload.orderNo || payload.platformOrderId || '').trim() if (!orderNo) { diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index 949e5573..c34a48d4 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -1,5 +1,4 @@ import { - CheckOutlined, CopyOutlined, EyeOutlined, PictureOutlined, @@ -32,9 +31,15 @@ import ImageUpload from '@/components/files/ImageUpload' import { cancelWorkerOrder, fetchWorkerMyOrders, + saveWorkerAcceptanceDraft, submitWorkerAcceptance, } from '@/services/worker' -import type { CollectField, UploadedFile, WorkOrder } from '@/types/worker-platform' +import type { + CollectField, + UploadedFile, + WorkOrder, + WorkerAcceptance, +} from '@/types/worker-platform' import { formatDateTime } from '@/utils/date-time' const STATUS_OPTIONS = [ @@ -67,7 +72,7 @@ export default function WorkerOrdersPage() { const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [detailOrder, setDetailOrder] = useState(null) - const [submittingOrder, setSubmittingOrder] = useState(null) + const [editingOrder, setEditingOrder] = useState(null) const [acceptanceFiles, setAcceptanceFiles] = useState([]) const [form] = Form.useForm() @@ -148,25 +153,42 @@ export default function WorkerOrdersPage() { } function openAcceptanceModal(order: WorkOrder) { - setSubmittingOrder(order) - setAcceptanceFiles([]) - form.resetFields() + setEditingOrder(order) + // 已提交:以已提交的验收资料为初始内容;未提交:以暂存草稿为初始内容 + const source = hasSubmittedAcceptance(order) + ? getWorkerAcceptance(order) + : getWorkerDraft(order) + setAcceptanceFiles([ + ...getAcceptanceFilesFromSource(source), + ...(Array.isArray(source.imageUrls) ? source.imageUrls : []).map( + (url): UploadedFile => ({ + objectKey: '', + url: String(url || ''), + thumbnailUrl: String(url || ''), + mediumUrl: String(url || ''), + filename: '图片', + contentType: '', + size: 0, + }), + ), + ]) + form.setFieldsValue({ note: String(source.note || '') }) setDetailOrder(null) } async function submitAcceptance(values: AcceptanceFormValues) { - if (!submittingOrder) return + if (!editingOrder) return if (acceptanceFiles.length === 0) { message.error('请上传验收图片') return } try { - await submitWorkerAcceptance(submittingOrder.workOrderId, { + await submitWorkerAcceptance(editingOrder.workOrderId, { note: values.note, files: acceptanceFiles, }) message.success('已提交验收') - setSubmittingOrder(null) + setEditingOrder(null) setAcceptanceFiles([]) form.resetFields() await refreshAll() @@ -175,6 +197,29 @@ export default function WorkerOrdersPage() { } } + async function saveAcceptanceImages(values: AcceptanceFormValues) { + if (!editingOrder) return + if (hasSubmittedAcceptance(editingOrder) && acceptanceFiles.length === 0) { + message.error('请至少保留一张验收图片') + return + } + try { + await saveWorkerAcceptanceDraft(editingOrder.workOrderId, { + note: values.note, + files: acceptanceFiles, + }) + message.success( + hasSubmittedAcceptance(editingOrder) ? '验收图片已更新' : '验收图片已暂存,可继续补充', + ) + setEditingOrder(null) + setAcceptanceFiles([]) + form.resetFields() + await refreshAll() + } catch (error) { + message.error(error instanceof Error ? error.message : '保存失败') + } + } + function applyKeywordSearch() { setKeyword(keywordInput.trim()) setPage(1) @@ -265,22 +310,35 @@ export default function WorkerOrdersPage() { const submittedAt = getAcceptanceSubmittedAt(row) const hasAcceptance = getAcceptanceFiles(row).length > 0 || getAcceptanceImageUrls(row).length > 0 - if (!hasAcceptance && !submittedAt) { + const draftFiles = getDraftFiles(row) + const draftUrls = getDraftImageUrls(row) + const draftCount = draftFiles.length + draftUrls.length + if (!hasAcceptance && !submittedAt && draftCount === 0) { return 未提交 } return (
- - - 提交时间:{formatDateTime(submittedAt)} - - {row.acceptance?.note ? ( + {hasAcceptance ? ( + + ) : null} + {submittedAt ? ( + + 提交时间:{formatDateTime(submittedAt)} + + ) : null} + {draftCount > 0 && !hasSubmittedAcceptance(row) ? ( + <> + 已暂存 {draftCount} 张图片 + + + ) : null} + {getWorkerAcceptance(row).note ? ( - {row.acceptance.note} + {getWorkerAcceptance(row).note} ) : null}
@@ -342,13 +400,13 @@ export default function WorkerOrdersPage() { - {canSubmitAcceptance(row) ? ( + {canEditAcceptanceImages(row) ? ( ) : null} {canCancelOrder(row) ? ( @@ -488,13 +546,13 @@ export default function WorkerOrdersPage() { 取消接单 ) : null} - {canSubmitAcceptance(detailOrder) ? ( + {canEditAcceptanceImages(detailOrder) ? ( ) : null} @@ -573,7 +631,7 @@ export default function WorkerOrdersPage() { - {String(detailOrder.acceptance?.note || '').trim() || '-'} + {String(getWorkerAcceptance(detailOrder).note || '').trim() || '-'} @@ -587,6 +645,19 @@ export default function WorkerOrdersPage() { ) : ( 暂无验收图片 )} + {!hasSubmittedAcceptance(detailOrder) && + (getDraftFiles(detailOrder).length || getDraftImageUrls(detailOrder).length) ? ( + <> + + 已暂存 {getDraftFiles(detailOrder).length + getDraftImageUrls(detailOrder).length} 张图片(未提交,刷新不丢失) + + + + ) : null} @@ -602,18 +673,57 @@ export default function WorkerOrdersPage() { { - setSubmittingOrder(null) + setEditingOrder(null) setAcceptanceFiles([]) form.resetFields() }} - onOk={() => form.submit()} destroyOnHidden + footer={ + editingOrder ? ( + + + + {canSubmitAcceptance(editingOrder) ? ( + + ) : null} + + ) : null + } >
- + String(item || '').trim()) - .filter(Boolean) + const imageUrls = getWorkerAcceptance(row).imageUrls + return Array.isArray(imageUrls) + ? imageUrls.map((item) => String(item || '').trim()).filter(Boolean) + : [] +} + +function getDraftFiles(row: WorkOrder): UploadedFile[] { + return getAcceptanceFilesFromSource(getWorkerDraft(row)) +} + +function getDraftImageUrls(row: WorkOrder): string[] { + const imageUrls = getWorkerDraft(row).imageUrls + return Array.isArray(imageUrls) + ? imageUrls.map((item) => String(item || '').trim()).filter(Boolean) : [] } function getAcceptanceSubmittedAt(order: WorkOrder): string | null { + if (order.myShare) return order.myShare.submittedAt const acceptanceSubmittedAt = typeof order.acceptance?.submittedAt === 'string' ? order.acceptance.submittedAt diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index e9f69812..69752053 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -136,6 +136,16 @@ export function submitWorkerAcceptance( ) } +export function saveWorkerAcceptanceDraft( + workOrderId: number, + payload: { note?: string; imageUrls?: string[]; files?: UploadedFile[] }, +) { + return apiPost<{ order: WorkOrder }>( + `/api/v1/worker/orders/${workOrderId}/acceptance-draft`, + payload, + ) +} + export function cancelWorkerOrder(workOrderId: number) { return apiPost<{ order: WorkOrder }>( `/api/v1/worker/orders/${workOrderId}/cancel`, diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index ed6bb7c9..2782fb08 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -188,6 +188,15 @@ export type WorkOrderSharingConfig = { unitReward: number } +export type WorkerAcceptance = { + note?: string + files?: UploadedFile[] + imageUrls?: string[] + submittedAt?: string + updatedAt?: string + [key: string]: unknown +} + export type WorkOrderShare = { shareId: number workOrderId: number @@ -197,7 +206,8 @@ export type WorkOrderShare = { shareReward: number shareDeposit: number status: string - acceptance: Record + acceptance: WorkerAcceptance + draftAcceptance?: WorkerAcceptance submittedAt: string | null acceptedAt: string | null createdAt: string @@ -235,13 +245,9 @@ export type WorkOrder = { deadlineAt?: string | null material: Record requirement: Record - acceptance: { - note?: string - files?: UploadedFile[] - imageUrls?: string[] - submittedAt?: string - [key: string]: unknown - } + acceptance: WorkerAcceptance + /** 暂存的验收草稿(未提交时保存于此,刷新不丢失;提交后清空) */ + draftAcceptance: WorkerAcceptance problemNote: string worker: null | { workerId: number