feat: 支持验收图片暂存
This commit is contained in:
@@ -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<WorkOrder | null>(null)
|
||||
const [submittingOrder, setSubmittingOrder] = useState<WorkOrder | null>(null)
|
||||
const [editingOrder, setEditingOrder] = useState<WorkOrder | null>(null)
|
||||
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
||||
const [form] = Form.useForm<AcceptanceFormValues>()
|
||||
|
||||
@@ -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 <Typography.Text type="secondary">未提交</Typography.Text>
|
||||
}
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
<ImagePreviewList
|
||||
files={getAcceptanceFiles(row)}
|
||||
imageUrls={getAcceptanceImageUrls(row)}
|
||||
size={40}
|
||||
/>
|
||||
<Typography.Text type="secondary">
|
||||
提交时间:{formatDateTime(submittedAt)}
|
||||
</Typography.Text>
|
||||
{row.acceptance?.note ? (
|
||||
{hasAcceptance ? (
|
||||
<ImagePreviewList
|
||||
files={getAcceptanceFiles(row)}
|
||||
imageUrls={getAcceptanceImageUrls(row)}
|
||||
size={40}
|
||||
/>
|
||||
) : null}
|
||||
{submittedAt ? (
|
||||
<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>
|
||||
{row.acceptance.note}
|
||||
{getWorkerAcceptance(row).note}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -342,13 +400,13 @@ export default function WorkerOrdersPage() {
|
||||
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(row)}>
|
||||
详情
|
||||
</Button>
|
||||
{canSubmitAcceptance(row) ? (
|
||||
{canEditAcceptanceImages(row) ? (
|
||||
<Button
|
||||
icon={<CheckOutlined />}
|
||||
type="primary"
|
||||
icon={<PictureOutlined />}
|
||||
type={canSubmitAcceptance(row) ? 'primary' : 'default'}
|
||||
onClick={() => openAcceptanceModal(row)}
|
||||
>
|
||||
提交验收
|
||||
{hasSubmittedAcceptance(row) ? '补充图片' : '上传图片 / 提交验收'}
|
||||
</Button>
|
||||
) : null}
|
||||
{canCancelOrder(row) ? (
|
||||
@@ -488,13 +546,13 @@ export default function WorkerOrdersPage() {
|
||||
取消接单
|
||||
</Button>
|
||||
) : null}
|
||||
{canSubmitAcceptance(detailOrder) ? (
|
||||
{canEditAcceptanceImages(detailOrder) ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
type={canSubmitAcceptance(detailOrder) ? 'primary' : 'default'}
|
||||
icon={<PictureOutlined />}
|
||||
onClick={() => openAcceptanceModal(detailOrder)}
|
||||
>
|
||||
提交验收
|
||||
{hasSubmittedAcceptance(detailOrder) ? '补充图片' : '上传图片 / 提交验收'}
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
@@ -573,7 +631,7 @@ export default function WorkerOrdersPage() {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="验收说明" span={2}>
|
||||
<span className="worker-order-detail-value">
|
||||
{String(detailOrder.acceptance?.note || '').trim() || '-'}
|
||||
{String(getWorkerAcceptance(detailOrder).note || '').trim() || '-'}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
@@ -587,6 +645,19 @@ export default function WorkerOrdersPage() {
|
||||
) : (
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
@@ -602,18 +673,57 @@ export default function WorkerOrdersPage() {
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title="提交验收"
|
||||
open={Boolean(submittingOrder)}
|
||||
title={
|
||||
editingOrder
|
||||
? hasSubmittedAcceptance(editingOrder)
|
||||
? '补充验收图片'
|
||||
: '提交验收'
|
||||
: '验收图片'
|
||||
}
|
||||
open={Boolean(editingOrder)}
|
||||
onCancel={() => {
|
||||
setSubmittingOrder(null)
|
||||
setEditingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
form.resetFields()
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
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.Item label="完成说明" name="note">
|
||||
<Form.Item
|
||||
label="完成说明"
|
||||
name="note"
|
||||
extra={
|
||||
editingOrder && !hasSubmittedAcceptance(editingOrder)
|
||||
? '图片和说明可先「保存暂存」,之后随时继续补充,不会丢失;确认完成后再「提交验收」。'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="可补充代练结果、注意事项或截图说明。"
|
||||
@@ -642,6 +752,22 @@ function canSubmitAcceptance(order: WorkOrder) {
|
||||
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) {
|
||||
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[] {
|
||||
return Array.isArray(row.acceptance?.files) ? row.acceptance.files : []
|
||||
return getAcceptanceFilesFromSource(getWorkerAcceptance(row))
|
||||
}
|
||||
|
||||
function getAcceptanceImageUrls(row: WorkOrder): string[] {
|
||||
return Array.isArray(row.acceptance?.imageUrls)
|
||||
? row.acceptance.imageUrls
|
||||
.map((item) => 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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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<string, unknown>
|
||||
acceptance: WorkerAcceptance
|
||||
draftAcceptance?: WorkerAcceptance
|
||||
submittedAt: string | null
|
||||
acceptedAt: string | null
|
||||
createdAt: string
|
||||
@@ -235,13 +245,9 @@ export type WorkOrder = {
|
||||
deadlineAt?: string | null
|
||||
material: Record<string, unknown>
|
||||
requirement: Record<string, unknown>
|
||||
acceptance: {
|
||||
note?: string
|
||||
files?: UploadedFile[]
|
||||
imageUrls?: string[]
|
||||
submittedAt?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
acceptance: WorkerAcceptance
|
||||
/** 暂存的验收草稿(未提交时保存于此,刷新不丢失;提交后清空) */
|
||||
draftAcceptance: WorkerAcceptance
|
||||
problemNote: string
|
||||
worker: null | {
|
||||
workerId: number
|
||||
|
||||
Reference in New Issue
Block a user