feat: 支持验收图片暂存
This commit is contained in:
@@ -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}),提交后清空';
|
||||||
@@ -144,6 +144,7 @@ export type WorkOrderShareRow = {
|
|||||||
share_deposit: number
|
share_deposit: number
|
||||||
status: string
|
status: string
|
||||||
acceptance_json: string | Record<string, unknown>
|
acceptance_json: string | Record<string, unknown>
|
||||||
|
draft_acceptance_json: string | Record<string, unknown>
|
||||||
submitted_at: string | null
|
submitted_at: string | null
|
||||||
accepted_at: string | null
|
accepted_at: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
@@ -170,6 +171,7 @@ export type WorkOrderRow = {
|
|||||||
material_json: string | Record<string, unknown>
|
material_json: string | Record<string, unknown>
|
||||||
requirement_json: string | Record<string, unknown>
|
requirement_json: string | Record<string, unknown>
|
||||||
acceptance_json: string | Record<string, unknown>
|
acceptance_json: string | Record<string, unknown>
|
||||||
|
draft_acceptance_json: string | Record<string, unknown>
|
||||||
problem_note: string
|
problem_note: string
|
||||||
sharing_enabled: boolean
|
sharing_enabled: boolean
|
||||||
sharing_total_quantity: number
|
sharing_total_quantity: number
|
||||||
|
|||||||
@@ -250,12 +250,23 @@ export async function submitWorkOrderShareAcceptance(input: {
|
|||||||
now: string
|
now: string
|
||||||
}): Promise<{ share: WorkOrderShareRow | null; failureReason: 'share_not_found' | 'share_status_invalid' | null }> {
|
}): Promise<{ share: WorkOrderShareRow | null; failureReason: 'share_not_found' | 'share_status_invalid' | null }> {
|
||||||
return withTransaction(async (client) => {
|
return withTransaction(async (client) => {
|
||||||
const shareResult = await client.query<WorkOrderShareRow>(
|
const lockResult = await client.query<{ id: number }>(
|
||||||
`${WORK_ORDER_SHARE_SELECT}
|
`
|
||||||
WHERE wos.work_order_id = $1 AND wos.worker_id = $2
|
SELECT id
|
||||||
FOR UPDATE`,
|
FROM work_order_shares
|
||||||
|
WHERE work_order_id = $1 AND worker_id = $2
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
[input.workOrderId, input.workerId],
|
[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<WorkOrderShareRow>(
|
||||||
|
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
||||||
|
[shareId],
|
||||||
|
)
|
||||||
const share = shareResult.rows[0] || null
|
const share = shareResult.rows[0] || null
|
||||||
if (!share) {
|
if (!share) {
|
||||||
return { share: null, failureReason: 'share_not_found' }
|
return { share: null, failureReason: 'share_not_found' }
|
||||||
@@ -266,7 +277,7 @@ export async function submitWorkOrderShareAcceptance(input: {
|
|||||||
await client.query(
|
await client.query(
|
||||||
`
|
`
|
||||||
UPDATE work_order_shares
|
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
|
WHERE id = $3
|
||||||
`,
|
`,
|
||||||
[input.acceptanceJson, input.now, Number(share.id)],
|
[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<WorkOrderRow | null> {
|
||||||
|
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<WorkOrderRow | null> {
|
||||||
|
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<WorkOrderShareRow | null> {
|
||||||
|
const result = await query<WorkOrderShareRow>(
|
||||||
|
`${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<WorkOrderShareRow | null> {
|
||||||
|
const result = await query<WorkOrderShareRow>(
|
||||||
|
`${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<WorkCategoryRow | null> {
|
export async function getWorkCategoryByKey(categoryKey: string): Promise<WorkCategoryRow | null> {
|
||||||
const result = await query<WorkCategoryRow>(
|
const result = await query<WorkCategoryRow>(
|
||||||
'SELECT * FROM work_categories WHERE category_key = $1 LIMIT 1',
|
'SELECT * FROM work_categories WHERE category_key = $1 LIMIT 1',
|
||||||
@@ -642,6 +737,7 @@ export async function updateWorkOrder(
|
|||||||
| 'assigned_worker_id'
|
| 'assigned_worker_id'
|
||||||
| 'material_json'
|
| 'material_json'
|
||||||
| 'acceptance_json'
|
| 'acceptance_json'
|
||||||
|
| 'draft_acceptance_json'
|
||||||
| 'problem_note'
|
| 'problem_note'
|
||||||
| 'reward_amount'
|
| 'reward_amount'
|
||||||
| 'sharing_enabled'
|
| 'sharing_enabled'
|
||||||
@@ -666,23 +762,25 @@ export async function updateWorkOrder(
|
|||||||
assigned_worker_id = $2,
|
assigned_worker_id = $2,
|
||||||
material_json = $3::jsonb,
|
material_json = $3::jsonb,
|
||||||
acceptance_json = $4::jsonb,
|
acceptance_json = $4::jsonb,
|
||||||
problem_note = $5,
|
draft_acceptance_json = $5::jsonb,
|
||||||
reward_amount = $6,
|
problem_note = $6,
|
||||||
sharing_enabled = $7,
|
reward_amount = $7,
|
||||||
sharing_total_quantity = $8,
|
sharing_enabled = $8,
|
||||||
sharing_unit_reward = $9,
|
sharing_total_quantity = $9,
|
||||||
published_at = $10,
|
sharing_unit_reward = $10,
|
||||||
assigned_at = $11,
|
published_at = $11,
|
||||||
submitted_at = $12,
|
assigned_at = $12,
|
||||||
accepted_at = $13,
|
submitted_at = $13,
|
||||||
updated_at = $14
|
accepted_at = $14,
|
||||||
WHERE id = $15
|
updated_at = $15
|
||||||
|
WHERE id = $16
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
next.status,
|
next.status,
|
||||||
next.assigned_worker_id || null,
|
next.assigned_worker_id || null,
|
||||||
toJsonString(next.material_json),
|
toJsonString(next.material_json),
|
||||||
toJsonString(next.acceptance_json),
|
toJsonString(next.acceptance_json),
|
||||||
|
toJsonString(next.draft_acceptance_json),
|
||||||
next.problem_note || '',
|
next.problem_note || '',
|
||||||
toPositiveInteger(next.reward_amount, 0),
|
toPositiveInteger(next.reward_amount, 0),
|
||||||
next.sharing_enabled === true,
|
next.sharing_enabled === true,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
logoutWorkerSession,
|
logoutWorkerSession,
|
||||||
registerWorker,
|
registerWorker,
|
||||||
sendWorkerSmsCode,
|
sendWorkerSmsCode,
|
||||||
|
saveWorkerOrderAcceptanceDraft,
|
||||||
submitWorkerOrderAcceptance,
|
submitWorkerOrderAcceptance,
|
||||||
} from '../services/worker-platform/index.js'
|
} from '../services/worker-platform/index.js'
|
||||||
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.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(
|
router.post(
|
||||||
'/orders/:workOrderId/cancel',
|
'/orders/:workOrderId/cancel',
|
||||||
requireActiveWorker,
|
requireActiveWorker,
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
|
|||||||
fields: resolveRequirementFields(workOrder),
|
fields: resolveRequirementFields(workOrder),
|
||||||
},
|
},
|
||||||
acceptance: mapAcceptanceForResponse(workOrder.acceptance_json),
|
acceptance: mapAcceptanceForResponse(workOrder.acceptance_json),
|
||||||
|
draftAcceptance: mapAcceptanceForResponse(workOrder.draft_acceptance_json),
|
||||||
problemNote: workOrder.problem_note || '',
|
problemNote: workOrder.problem_note || '',
|
||||||
worker: workOrder.assigned_worker_id
|
worker: workOrder.assigned_worker_id
|
||||||
? {
|
? {
|
||||||
@@ -443,6 +444,7 @@ export function mapWorkOrderShare(share: WorkOrderShareRow) {
|
|||||||
shareDeposit: Number(share.share_deposit || 0),
|
shareDeposit: Number(share.share_deposit || 0),
|
||||||
status: share.status,
|
status: share.status,
|
||||||
acceptance: safeParseJson(share.acceptance_json),
|
acceptance: safeParseJson(share.acceptance_json),
|
||||||
|
draftAcceptance: safeParseJson(share.draft_acceptance_json),
|
||||||
submittedAt: share.submitted_at,
|
submittedAt: share.submitted_at,
|
||||||
acceptedAt: share.accepted_at,
|
acceptedAt: share.accepted_at,
|
||||||
createdAt: share.created_at,
|
createdAt: share.created_at,
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ import {
|
|||||||
settleOverdueWorkOrder,
|
settleOverdueWorkOrder,
|
||||||
submitWorkOrderShareAcceptance,
|
submitWorkOrderShareAcceptance,
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
|
updateWorkOrderAcceptanceRecord,
|
||||||
|
updateWorkOrderDraftAcceptance,
|
||||||
|
updateWorkOrderShareAcceptanceRecord,
|
||||||
|
updateWorkOrderShareDraftAcceptance,
|
||||||
updateWorkerPassword,
|
updateWorkerPassword,
|
||||||
updateWorkerUser,
|
updateWorkerUser,
|
||||||
upsertWorkCategory,
|
upsertWorkCategory,
|
||||||
@@ -1111,11 +1115,10 @@ export async function submitWorkerOrderAcceptance(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = normalizeUploadedFiles(payload.files)
|
const { files, imageUrls, note } = resolveAcceptanceImages(
|
||||||
const imageUrls = [
|
payload,
|
||||||
...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
|
workOrder.draft_acceptance_json,
|
||||||
...normalizeStringArray(payload.imageUrls),
|
)
|
||||||
]
|
|
||||||
if (imageUrls.length === 0) {
|
if (imageUrls.length === 0) {
|
||||||
throw createHttpError('请上传验收图片', {
|
throw createHttpError('请上传验收图片', {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
@@ -1123,7 +1126,7 @@ export async function submitWorkerOrderAcceptance(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const acceptance = {
|
const acceptance = {
|
||||||
note: String(payload.note || '').trim(),
|
note,
|
||||||
files,
|
files,
|
||||||
imageUrls: [...new Set(imageUrls)],
|
imageUrls: [...new Set(imageUrls)],
|
||||||
submittedAt: now,
|
submittedAt: now,
|
||||||
@@ -1131,6 +1134,7 @@ export async function submitWorkerOrderAcceptance(
|
|||||||
const updated = await updateWorkOrder(workOrder.id, {
|
const updated = await updateWorkOrder(workOrder.id, {
|
||||||
status: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
status: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||||
acceptance_json: acceptance,
|
acceptance_json: acceptance,
|
||||||
|
draft_acceptance_json: {},
|
||||||
submitted_at: now,
|
submitted_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
@@ -1198,11 +1202,10 @@ async function submitWorkerSharingAcceptance(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const files = normalizeUploadedFiles(payload.files)
|
const { files, imageUrls, note } = resolveAcceptanceImages(
|
||||||
const imageUrls = [
|
payload,
|
||||||
...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
|
sharingShare.draft_acceptance_json,
|
||||||
...normalizeStringArray(payload.imageUrls),
|
)
|
||||||
]
|
|
||||||
if (imageUrls.length === 0) {
|
if (imageUrls.length === 0) {
|
||||||
throw createHttpError('请上传验收图片', {
|
throw createHttpError('请上传验收图片', {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
@@ -1210,7 +1213,7 @@ async function submitWorkerSharingAcceptance(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const acceptance = {
|
const acceptance = {
|
||||||
note: String(payload.note || '').trim(),
|
note,
|
||||||
files,
|
files,
|
||||||
imageUrls: [...new Set(imageUrls)],
|
imageUrls: [...new Set(imageUrls)],
|
||||||
submittedAt: now,
|
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 = {}) {
|
export async function collectLookupWorkOrder(payload: JsonObject = {}) {
|
||||||
const orderNo = String(payload.orderNo || payload.platformOrderId || '').trim()
|
const orderNo = String(payload.orderNo || payload.platformOrderId || '').trim()
|
||||||
if (!orderNo) {
|
if (!orderNo) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
CheckOutlined,
|
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
PictureOutlined,
|
PictureOutlined,
|
||||||
@@ -32,9 +31,15 @@ import ImageUpload from '@/components/files/ImageUpload'
|
|||||||
import {
|
import {
|
||||||
cancelWorkerOrder,
|
cancelWorkerOrder,
|
||||||
fetchWorkerMyOrders,
|
fetchWorkerMyOrders,
|
||||||
|
saveWorkerAcceptanceDraft,
|
||||||
submitWorkerAcceptance,
|
submitWorkerAcceptance,
|
||||||
} from '@/services/worker'
|
} 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'
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
@@ -67,7 +72,7 @@ export default function WorkerOrdersPage() {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
||||||
const [submittingOrder, setSubmittingOrder] = useState<WorkOrder | null>(null)
|
const [editingOrder, setEditingOrder] = useState<WorkOrder | null>(null)
|
||||||
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
||||||
const [form] = Form.useForm<AcceptanceFormValues>()
|
const [form] = Form.useForm<AcceptanceFormValues>()
|
||||||
|
|
||||||
@@ -148,25 +153,42 @@ export default function WorkerOrdersPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openAcceptanceModal(order: WorkOrder) {
|
function openAcceptanceModal(order: WorkOrder) {
|
||||||
setSubmittingOrder(order)
|
setEditingOrder(order)
|
||||||
setAcceptanceFiles([])
|
// 已提交:以已提交的验收资料为初始内容;未提交:以暂存草稿为初始内容
|
||||||
form.resetFields()
|
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)
|
setDetailOrder(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitAcceptance(values: AcceptanceFormValues) {
|
async function submitAcceptance(values: AcceptanceFormValues) {
|
||||||
if (!submittingOrder) return
|
if (!editingOrder) return
|
||||||
if (acceptanceFiles.length === 0) {
|
if (acceptanceFiles.length === 0) {
|
||||||
message.error('请上传验收图片')
|
message.error('请上传验收图片')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await submitWorkerAcceptance(submittingOrder.workOrderId, {
|
await submitWorkerAcceptance(editingOrder.workOrderId, {
|
||||||
note: values.note,
|
note: values.note,
|
||||||
files: acceptanceFiles,
|
files: acceptanceFiles,
|
||||||
})
|
})
|
||||||
message.success('已提交验收')
|
message.success('已提交验收')
|
||||||
setSubmittingOrder(null)
|
setEditingOrder(null)
|
||||||
setAcceptanceFiles([])
|
setAcceptanceFiles([])
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
await refreshAll()
|
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() {
|
function applyKeywordSearch() {
|
||||||
setKeyword(keywordInput.trim())
|
setKeyword(keywordInput.trim())
|
||||||
setPage(1)
|
setPage(1)
|
||||||
@@ -265,22 +310,35 @@ export default function WorkerOrdersPage() {
|
|||||||
const submittedAt = getAcceptanceSubmittedAt(row)
|
const submittedAt = getAcceptanceSubmittedAt(row)
|
||||||
const hasAcceptance =
|
const hasAcceptance =
|
||||||
getAcceptanceFiles(row).length > 0 || getAcceptanceImageUrls(row).length > 0
|
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 <Typography.Text type="secondary">未提交</Typography.Text>
|
return <Typography.Text type="secondary">未提交</Typography.Text>
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<ImagePreviewList
|
{hasAcceptance ? (
|
||||||
files={getAcceptanceFiles(row)}
|
<ImagePreviewList
|
||||||
imageUrls={getAcceptanceImageUrls(row)}
|
files={getAcceptanceFiles(row)}
|
||||||
size={40}
|
imageUrls={getAcceptanceImageUrls(row)}
|
||||||
/>
|
size={40}
|
||||||
<Typography.Text type="secondary">
|
/>
|
||||||
提交时间:{formatDateTime(submittedAt)}
|
) : null}
|
||||||
</Typography.Text>
|
{submittedAt ? (
|
||||||
{row.acceptance?.note ? (
|
<Typography.Text type="secondary">
|
||||||
|
提交时间:{formatDateTime(submittedAt)}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
{draftCount > 0 && !hasSubmittedAcceptance(row) ? (
|
||||||
|
<>
|
||||||
|
<Tag color="blue">已暂存 {draftCount} 张图片</Tag>
|
||||||
|
<ImagePreviewList files={draftFiles} imageUrls={draftUrls} size={40} />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{getWorkerAcceptance(row).note ? (
|
||||||
<Typography.Text type="secondary" ellipsis>
|
<Typography.Text type="secondary" ellipsis>
|
||||||
{row.acceptance.note}
|
{getWorkerAcceptance(row).note}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -342,13 +400,13 @@ export default function WorkerOrdersPage() {
|
|||||||
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(row)}>
|
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(row)}>
|
||||||
详情
|
详情
|
||||||
</Button>
|
</Button>
|
||||||
{canSubmitAcceptance(row) ? (
|
{canEditAcceptanceImages(row) ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<CheckOutlined />}
|
icon={<PictureOutlined />}
|
||||||
type="primary"
|
type={canSubmitAcceptance(row) ? 'primary' : 'default'}
|
||||||
onClick={() => openAcceptanceModal(row)}
|
onClick={() => openAcceptanceModal(row)}
|
||||||
>
|
>
|
||||||
提交验收
|
{hasSubmittedAcceptance(row) ? '补充图片' : '上传图片 / 提交验收'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{canCancelOrder(row) ? (
|
{canCancelOrder(row) ? (
|
||||||
@@ -488,13 +546,13 @@ export default function WorkerOrdersPage() {
|
|||||||
取消接单
|
取消接单
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{canSubmitAcceptance(detailOrder) ? (
|
{canEditAcceptanceImages(detailOrder) ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type={canSubmitAcceptance(detailOrder) ? 'primary' : 'default'}
|
||||||
icon={<CheckOutlined />}
|
icon={<PictureOutlined />}
|
||||||
onClick={() => openAcceptanceModal(detailOrder)}
|
onClick={() => openAcceptanceModal(detailOrder)}
|
||||||
>
|
>
|
||||||
提交验收
|
{hasSubmittedAcceptance(detailOrder) ? '补充图片' : '上传图片 / 提交验收'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -573,7 +631,7 @@ export default function WorkerOrdersPage() {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="验收说明" span={2}>
|
<Descriptions.Item label="验收说明" span={2}>
|
||||||
<span className="worker-order-detail-value">
|
<span className="worker-order-detail-value">
|
||||||
{String(detailOrder.acceptance?.note || '').trim() || '-'}
|
{String(getWorkerAcceptance(detailOrder).note || '').trim() || '-'}
|
||||||
</span>
|
</span>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
@@ -587,6 +645,19 @@ export default function WorkerOrdersPage() {
|
|||||||
) : (
|
) : (
|
||||||
<Typography.Text type="secondary">暂无验收图片</Typography.Text>
|
<Typography.Text type="secondary">暂无验收图片</Typography.Text>
|
||||||
)}
|
)}
|
||||||
|
{!hasSubmittedAcceptance(detailOrder) &&
|
||||||
|
(getDraftFiles(detailOrder).length || getDraftImageUrls(detailOrder).length) ? (
|
||||||
|
<>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
已暂存 {getDraftFiles(detailOrder).length + getDraftImageUrls(detailOrder).length} 张图片(未提交,刷新不丢失)
|
||||||
|
</Typography.Text>
|
||||||
|
<ImagePreviewList
|
||||||
|
files={getDraftFiles(detailOrder)}
|
||||||
|
imageUrls={getDraftImageUrls(detailOrder)}
|
||||||
|
size={76}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -602,18 +673,57 @@ export default function WorkerOrdersPage() {
|
|||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="提交验收"
|
title={
|
||||||
open={Boolean(submittingOrder)}
|
editingOrder
|
||||||
|
? hasSubmittedAcceptance(editingOrder)
|
||||||
|
? '补充验收图片'
|
||||||
|
: '提交验收'
|
||||||
|
: '验收图片'
|
||||||
|
}
|
||||||
|
open={Boolean(editingOrder)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setSubmittingOrder(null)
|
setEditingOrder(null)
|
||||||
setAcceptanceFiles([])
|
setAcceptanceFiles([])
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
}}
|
}}
|
||||||
onOk={() => form.submit()}
|
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
|
footer={
|
||||||
|
editingOrder ? (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingOrder(null)
|
||||||
|
setAcceptanceFiles([])
|
||||||
|
form.resetFields()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => void saveAcceptanceImages(form.getFieldsValue())}
|
||||||
|
>
|
||||||
|
{hasSubmittedAcceptance(editingOrder) ? '保存修改' : '保存暂存'}
|
||||||
|
</Button>
|
||||||
|
{canSubmitAcceptance(editingOrder) ? (
|
||||||
|
<Button type="primary" onClick={() => form.submit()}>
|
||||||
|
提交验收
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={submitAcceptance}>
|
<Form form={form} layout="vertical" onFinish={submitAcceptance}>
|
||||||
<Form.Item label="完成说明" name="note">
|
<Form.Item
|
||||||
|
label="完成说明"
|
||||||
|
name="note"
|
||||||
|
extra={
|
||||||
|
editingOrder && !hasSubmittedAcceptance(editingOrder)
|
||||||
|
? '图片和说明可先「保存暂存」,之后随时继续补充,不会丢失;确认完成后再「提交验收」。'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="可补充代练结果、注意事项或截图说明。"
|
placeholder="可补充代练结果、注意事项或截图说明。"
|
||||||
@@ -642,6 +752,22 @@ function canSubmitAcceptance(order: WorkOrder) {
|
|||||||
return ['in_progress', 'problem'].includes(order.status)
|
return ['in_progress', 'problem'].includes(order.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 是否可编辑验收图片:未提交时可暂存,已提交(待验收)时可补充/修改 */
|
||||||
|
function canEditAcceptanceImages(order: WorkOrder) {
|
||||||
|
if (order.myShare) {
|
||||||
|
return ['joined', 'submitted'].includes(order.myShare.status) && order.status !== 'accepted'
|
||||||
|
}
|
||||||
|
return ['in_progress', 'problem', 'pending_acceptance'].includes(order.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否已提交验收(整单 pending_acceptance / 拼单份额 submitted) */
|
||||||
|
function hasSubmittedAcceptance(order: WorkOrder) {
|
||||||
|
if (order.myShare) {
|
||||||
|
return order.myShare.status === 'submitted'
|
||||||
|
}
|
||||||
|
return order.status === 'pending_acceptance'
|
||||||
|
}
|
||||||
|
|
||||||
function canCancelOrder(order: WorkOrder) {
|
function canCancelOrder(order: WorkOrder) {
|
||||||
return !order.myShare && order.status === 'in_progress'
|
return !order.myShare && order.status === 'in_progress'
|
||||||
}
|
}
|
||||||
@@ -776,19 +902,44 @@ function renderDetailFieldValues(items: DetailFieldItem[], emptyText: string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 打手自己的验收资料(拼单时取份额自己的) */
|
||||||
|
function getWorkerAcceptance(order: WorkOrder): WorkerAcceptance {
|
||||||
|
return order.myShare ? order.myShare.acceptance : order.acceptance
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打手自己的暂存草稿(拼单时取份额自己的) */
|
||||||
|
function getWorkerDraft(order: WorkOrder): WorkerAcceptance {
|
||||||
|
return order.myShare?.draftAcceptance || order.draftAcceptance
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAcceptanceFilesFromSource(source: WorkerAcceptance): UploadedFile[] {
|
||||||
|
return Array.isArray(source.files) ? source.files : []
|
||||||
|
}
|
||||||
|
|
||||||
function getAcceptanceFiles(row: WorkOrder): UploadedFile[] {
|
function getAcceptanceFiles(row: WorkOrder): UploadedFile[] {
|
||||||
return Array.isArray(row.acceptance?.files) ? row.acceptance.files : []
|
return getAcceptanceFilesFromSource(getWorkerAcceptance(row))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAcceptanceImageUrls(row: WorkOrder): string[] {
|
function getAcceptanceImageUrls(row: WorkOrder): string[] {
|
||||||
return Array.isArray(row.acceptance?.imageUrls)
|
const imageUrls = getWorkerAcceptance(row).imageUrls
|
||||||
? row.acceptance.imageUrls
|
return Array.isArray(imageUrls)
|
||||||
.map((item) => String(item || '').trim())
|
? imageUrls.map((item) => String(item || '').trim()).filter(Boolean)
|
||||||
.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 {
|
function getAcceptanceSubmittedAt(order: WorkOrder): string | null {
|
||||||
|
if (order.myShare) return order.myShare.submittedAt
|
||||||
const acceptanceSubmittedAt =
|
const acceptanceSubmittedAt =
|
||||||
typeof order.acceptance?.submittedAt === 'string'
|
typeof order.acceptance?.submittedAt === 'string'
|
||||||
? order.acceptance.submittedAt
|
? order.acceptance.submittedAt
|
||||||
|
|||||||
@@ -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) {
|
export function cancelWorkerOrder(workOrderId: number) {
|
||||||
return apiPost<{ order: WorkOrder }>(
|
return apiPost<{ order: WorkOrder }>(
|
||||||
`/api/v1/worker/orders/${workOrderId}/cancel`,
|
`/api/v1/worker/orders/${workOrderId}/cancel`,
|
||||||
|
|||||||
@@ -188,6 +188,15 @@ export type WorkOrderSharingConfig = {
|
|||||||
unitReward: number
|
unitReward: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerAcceptance = {
|
||||||
|
note?: string
|
||||||
|
files?: UploadedFile[]
|
||||||
|
imageUrls?: string[]
|
||||||
|
submittedAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkOrderShare = {
|
export type WorkOrderShare = {
|
||||||
shareId: number
|
shareId: number
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
@@ -197,7 +206,8 @@ export type WorkOrderShare = {
|
|||||||
shareReward: number
|
shareReward: number
|
||||||
shareDeposit: number
|
shareDeposit: number
|
||||||
status: string
|
status: string
|
||||||
acceptance: Record<string, unknown>
|
acceptance: WorkerAcceptance
|
||||||
|
draftAcceptance?: WorkerAcceptance
|
||||||
submittedAt: string | null
|
submittedAt: string | null
|
||||||
acceptedAt: string | null
|
acceptedAt: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
@@ -235,13 +245,9 @@ export type WorkOrder = {
|
|||||||
deadlineAt?: string | null
|
deadlineAt?: string | null
|
||||||
material: Record<string, unknown>
|
material: Record<string, unknown>
|
||||||
requirement: Record<string, unknown>
|
requirement: Record<string, unknown>
|
||||||
acceptance: {
|
acceptance: WorkerAcceptance
|
||||||
note?: string
|
/** 暂存的验收草稿(未提交时保存于此,刷新不丢失;提交后清空) */
|
||||||
files?: UploadedFile[]
|
draftAcceptance: WorkerAcceptance
|
||||||
imageUrls?: string[]
|
|
||||||
submittedAt?: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
problemNote: string
|
problemNote: string
|
||||||
worker: null | {
|
worker: null | {
|
||||||
workerId: number
|
workerId: number
|
||||||
|
|||||||
Reference in New Issue
Block a user