add:新增同一个工单同一人可以多次拼单
This commit is contained in:
@@ -23,6 +23,11 @@ const notificationEventRows: NotificationEventRow[] = [
|
||||
{ key: 'recharge_requested', name: '充值申请', description: '打手提交新的充值申请' },
|
||||
{ key: 'acceptance_submitted', name: '提交验收', description: '打手提交订单验收资料' },
|
||||
{ key: 'acceptance_reminded', name: '催验收', description: '打手在验收后催促处理' },
|
||||
{
|
||||
key: 'order_accepted',
|
||||
name: '整单验收完成',
|
||||
description: '全部份额验收完成(含拼单统一验收)',
|
||||
},
|
||||
{ key: 'material_required', name: '待补资料工单', description: '新工单等待后台补充资料' },
|
||||
{
|
||||
key: 'cancel_requested',
|
||||
|
||||
@@ -114,6 +114,7 @@ import {
|
||||
asRecord,
|
||||
formatMoney,
|
||||
formatSharingShareStatus,
|
||||
formatSharingShareCancelledReason,
|
||||
formatStatus,
|
||||
resolveGiftPhaseColor,
|
||||
resolveSharingShareStatusColor,
|
||||
@@ -3145,6 +3146,7 @@ export default function WorkOrdersPanel() {
|
||||
dataSource={sharingShares}
|
||||
loading={sharingQuery.isFetching}
|
||||
pagination={false}
|
||||
scroll={{ x: 1280 }}
|
||||
columns={[
|
||||
{
|
||||
title: '打手',
|
||||
@@ -3159,11 +3161,38 @@ export default function WorkOrdersPanel() {
|
||||
{
|
||||
title: '状态',
|
||||
render: (_, share) => (
|
||||
<Tag color={resolveSharingShareStatusColor(share.status)}>
|
||||
{formatSharingShareStatus(share.status)}
|
||||
</Tag>
|
||||
<Space size={4} wrap>
|
||||
<Tag color={resolveSharingShareStatusColor(share.status)}>
|
||||
{formatSharingShareStatus(share.status)}
|
||||
</Tag>
|
||||
{share.status === 'cancelled' && share.cancelledReason ? (
|
||||
<Tag>{formatSharingShareCancelledReason(share.cancelledReason)}</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '参与时间',
|
||||
render: (_, share) =>
|
||||
share.createdAt ? formatAdminDateTime(share.createdAt) : '-',
|
||||
},
|
||||
{
|
||||
title: '截止时间',
|
||||
render: (_, share) =>
|
||||
share.deadlineAt ? (
|
||||
<span>
|
||||
{formatAdminDateTime(share.deadlineAt)}
|
||||
{share.timeoutMinutes ? ` · ${share.timeoutMinutes} 分钟` : null}
|
||||
</span>
|
||||
) : (
|
||||
<Typography.Text type="secondary">不限时</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '超时策略',
|
||||
render: (_, share) =>
|
||||
share.timeoutPolicy ? formatTimeoutPolicyLabel(share.timeoutPolicy) : '-',
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
render: (_, share) =>
|
||||
|
||||
@@ -252,6 +252,16 @@ export function formatSharingShareStatus(status: string) {
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
/** 撤单原因:份额级独立撤单后,后台需要一眼看出是谁撤的。 */
|
||||
export function formatSharingShareCancelledReason(reason: string) {
|
||||
const labels: Record<string, string> = {
|
||||
timeout: '超时失效',
|
||||
admin: '后台撤单',
|
||||
worker_request: '打手退单',
|
||||
}
|
||||
return labels[reason] || reason
|
||||
}
|
||||
|
||||
export function formatAcceptanceMode(mode: string) {
|
||||
if (mode === 'friend_gift') return '好友赠送'
|
||||
return '标准验收'
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
resolveHallRewardAmount,
|
||||
} from '@/utils/worker-product-name'
|
||||
import { formatDateTime, formatPublishedElapsedTime } from '@/utils/date-time'
|
||||
import { createRequestId } from '@/utils/request-id'
|
||||
|
||||
type HallSummary = {
|
||||
pendingAcceptanceCount: number
|
||||
@@ -314,7 +315,7 @@ export default function WorkerHallPage() {
|
||||
async function submitGrab(order: WorkOrder) {
|
||||
setGrabbingId(order.workOrderId)
|
||||
try {
|
||||
await grabWorkerOrder(order.workOrderId)
|
||||
await grabWorkerOrder(order.workOrderId, { requestId: createRequestId('grab') })
|
||||
message.success('抢单成功,已进入我的订单')
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
@@ -335,6 +336,7 @@ export default function WorkerHallPage() {
|
||||
try {
|
||||
await joinWorkerSharingOrder(sharingOrder.workOrderId, {
|
||||
quantity: sharingQuantity,
|
||||
requestId: createRequestId('share'),
|
||||
})
|
||||
message.success('拼单成功,已进入我的订单')
|
||||
setSharingOrder(null)
|
||||
@@ -346,6 +348,15 @@ export default function WorkerHallPage() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 我在该工单的份额摘要:参与次数 + 累计份数 */
|
||||
function resolveMyShareSummary(order: WorkOrder): { count: number; quantity: number } {
|
||||
const shares = order.myShares?.length ? order.myShares : order.myShare ? [order.myShare] : []
|
||||
return {
|
||||
count: shares.length,
|
||||
quantity: shares.reduce((total, share) => total + Number(share.quantity || 0), 0),
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSharingRemaining(order: WorkOrder): number {
|
||||
return resolveHallRemainingQuantity(order)
|
||||
}
|
||||
@@ -700,18 +711,18 @@ export default function WorkerHallPage() {
|
||||
</div>
|
||||
|
||||
<div className="worker-hall-mobile-card-btn-group">
|
||||
{order.sharing?.enabled &&
|
||||
(sharingRemaining > 0 || order.myShare) &&
|
||||
!disabledReason ? (
|
||||
{order.sharing?.enabled && sharingRemaining > 0 && !disabledReason ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
className="worker-hall-sharing-button"
|
||||
disabled={Boolean(disabledReason) || Boolean(order.myShare)}
|
||||
disabled={Boolean(disabledReason)}
|
||||
onClick={() => openSharingModal(order)}
|
||||
>
|
||||
{order.myShare
|
||||
? `已拼 ${order.myShare.quantity} 份`
|
||||
{resolveMyShareSummary(order).count > 0
|
||||
? `已参与 ${resolveMyShareSummary(order).count} 次 · 共 ${
|
||||
resolveMyShareSummary(order).quantity
|
||||
} 份 / 再拼`
|
||||
: `拼单 ${formatMoney(order.sharing.unitReward)}/份`}
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -1152,18 +1163,18 @@ export default function WorkerHallPage() {
|
||||
|
||||
<div className="worker-hall-card-actions">
|
||||
<div className="worker-hall-card-grab-row">
|
||||
{order.sharing?.enabled &&
|
||||
(sharingRemaining > 0 || order.myShare) &&
|
||||
!disabledReason ? (
|
||||
{order.sharing?.enabled && sharingRemaining > 0 && !disabledReason ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="worker-hall-sharing-button"
|
||||
disabled={Boolean(disabledReason) || Boolean(order.myShare)}
|
||||
disabled={Boolean(disabledReason)}
|
||||
onClick={() => openSharingModal(order)}
|
||||
>
|
||||
{order.myShare
|
||||
? `已拼 ${order.myShare.quantity} 份`
|
||||
{resolveMyShareSummary(order).count > 0
|
||||
? `已参与 ${resolveMyShareSummary(order).count} 次 · 共 ${
|
||||
resolveMyShareSummary(order).quantity
|
||||
} 份 / 再拼`
|
||||
: `拼单 ${formatMoney(order.sharing.unitReward)}/份`}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { Alert, Button, Card, Empty, Input, Space, Table, Tabs, Typography } from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import type { Pagination, WorkerOrderOverviewItem, WorkOrder } from '@/types/worker-platform'
|
||||
|
||||
@@ -29,6 +30,7 @@ export function WorkerOrderDesktopList({
|
||||
page,
|
||||
pageSize,
|
||||
columns,
|
||||
renderExpandedRow,
|
||||
loading,
|
||||
searching,
|
||||
refreshing,
|
||||
@@ -49,6 +51,7 @@ export function WorkerOrderDesktopList({
|
||||
page: number
|
||||
pageSize: number
|
||||
columns: TableColumnsType<WorkOrder>
|
||||
renderExpandedRow?: (order: WorkOrder) => ReactNode
|
||||
loading: boolean
|
||||
searching: boolean
|
||||
refreshing: boolean
|
||||
@@ -262,6 +265,15 @@ export function WorkerOrderDesktopList({
|
||||
dataSource={orders}
|
||||
columns={columns}
|
||||
className="worker-orders-table"
|
||||
expandable={
|
||||
renderExpandedRow
|
||||
? {
|
||||
expandedRowRender: renderExpandedRow,
|
||||
rowExpandable: (order) => (order.myShares?.length || 0) > 1,
|
||||
columnWidth: 42,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
locale={{
|
||||
emptyText: error ? (
|
||||
<Empty description={error instanceof Error ? error.message : '读取我的订单失败'} />
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Alert, Button, Empty, Input, Pagination, Spin, Tag } from 'antd'
|
||||
import { Alert, Button, Empty, Input, Pagination, Spin, Tag, Typography } from 'antd'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { DeadlineCountdown } from '@/components/DeadlineCountdown'
|
||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
@@ -53,6 +54,7 @@ export function WorkerOrderMobileList({
|
||||
pagination,
|
||||
page,
|
||||
pageSize,
|
||||
renderShareTable,
|
||||
loading,
|
||||
searching,
|
||||
refreshing,
|
||||
@@ -81,6 +83,7 @@ export function WorkerOrderMobileList({
|
||||
pagination: PaginationData | undefined
|
||||
page: number
|
||||
pageSize: number
|
||||
renderShareTable?: (order: WorkOrder) => ReactNode
|
||||
loading: boolean
|
||||
searching: boolean
|
||||
refreshing: boolean
|
||||
@@ -173,6 +176,7 @@ export function WorkerOrderMobileList({
|
||||
<WorkerOrderMobileCard
|
||||
key={order.workOrderId}
|
||||
order={order}
|
||||
renderShareTable={renderShareTable}
|
||||
onOpenDetail={onOpenDetail}
|
||||
onOpenAcceptance={onOpenAcceptance}
|
||||
onOpenGiftMaterial={onOpenGiftMaterial}
|
||||
@@ -203,6 +207,7 @@ export function WorkerOrderMobileList({
|
||||
|
||||
function WorkerOrderMobileCard({
|
||||
order,
|
||||
renderShareTable,
|
||||
onOpenDetail,
|
||||
onOpenAcceptance,
|
||||
onOpenGiftMaterial,
|
||||
@@ -326,14 +331,27 @@ function WorkerOrderMobileCard({
|
||||
? '押金: 已解除冻结'
|
||||
: `押金: ${formatMoney(order.freezeDepositAmount)}`}
|
||||
</span>
|
||||
{order.myShare ? (
|
||||
{order.myShare || order.myShares?.length ? (
|
||||
<span className="worker-order-mobile-sub-item">
|
||||
已拼 {order.myShare.quantity} 份 ({formatMoney(order.myShare.shareReward)})
|
||||
已参与 {order.myShares?.length || 1} 次 · 共{' '}
|
||||
{order.myShares?.reduce((total, share) => total + share.quantity, 0) ||
|
||||
order.myShare?.quantity ||
|
||||
0}{' '}
|
||||
份
|
||||
</span>
|
||||
) : null}
|
||||
<span className="worker-order-mobile-sub-time">{formatDateTime(order.assignedAt)}</span>
|
||||
</div>
|
||||
|
||||
{(order.myShares?.length || 0) > 1 && renderShareTable ? (
|
||||
<div className="worker-order-mobile-shares">
|
||||
<Typography.Text strong style={{ fontSize: 12 }}>
|
||||
拼单明细({order.myShares?.length} 份)
|
||||
</Typography.Text>
|
||||
{renderShareTable(order)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{order.status === 'problem' && String(order.problemNote || '').trim() ? (
|
||||
<div className="worker-order-mobile-problem-box">
|
||||
<strong>问题:</strong>
|
||||
|
||||
@@ -83,7 +83,9 @@ export function buildWorkerOrderTableColumns({
|
||||
</Space>
|
||||
<Space size={[4, 4]} wrap>
|
||||
{row.categoryName ? <Tag color="blue">{row.categoryName}</Tag> : null}
|
||||
{row.myShare ? <Tag color="purple">拼单</Tag> : null}
|
||||
{row.myShares?.length || row.myShare ? (
|
||||
<Tag color="purple">拼单 {row.myShares?.length || 1} 次</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
@@ -165,9 +167,13 @@ export function buildWorkerOrderTableColumns({
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
押金:{formatMoney(row.freezeDepositAmount)}
|
||||
</Typography.Text>
|
||||
{row.myShare ? (
|
||||
{row.myShare || row.myShares?.length ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{row.myShare.quantity} 份 · {formatMoney(row.myShare.shareReward)}
|
||||
{row.myShares?.length || 1} 次 ·{' '}
|
||||
{row.myShares?.reduce((total, share) => total + share.quantity, 0) ||
|
||||
row.myShare?.quantity ||
|
||||
0}{' '}
|
||||
份
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -239,48 +245,57 @@ export function buildWorkerOrderTableColumns({
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<div className="worker-order-actions">
|
||||
<Space size={6}>
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
aria-label="查看详情"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => onOpenDetail(row)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{canSubmitGiftMaterial(row) ? (
|
||||
<Button type="primary" onClick={() => onOpenGiftMaterial(row)}>
|
||||
提交资料
|
||||
</Button>
|
||||
) : null}
|
||||
{canEditAcceptanceImages(row) ? (
|
||||
<Button
|
||||
icon={<PictureOutlined />}
|
||||
type={canSubmitAcceptance(row) ? 'primary' : 'default'}
|
||||
onClick={() => onOpenAcceptance(row)}
|
||||
>
|
||||
{hasSubmittedAcceptance(row) ? '补充图片' : '验收'}
|
||||
</Button>
|
||||
) : null}
|
||||
{canRemindAcceptance(row) ? (
|
||||
<Button onClick={() => onRemindAcceptance(row)}>催验收</Button>
|
||||
) : null}
|
||||
{canRequestCancel(row) ? (
|
||||
<Button danger onClick={() => onOpenCancelRequest(row)}>
|
||||
申请取消
|
||||
</Button>
|
||||
) : null}
|
||||
{canSubmitFeedback(row) ? (
|
||||
<Tooltip title="反馈客户资料或订单处理问题">
|
||||
<Button icon={<MessageOutlined />} onClick={() => onOpenFeedback(row)}>
|
||||
反馈
|
||||
</Button>
|
||||
render: (_, row) => {
|
||||
const shareCount = row.myShares?.length || (row.myShare ? 1 : 0)
|
||||
const hasMultipleShares = shareCount > 1
|
||||
return (
|
||||
<div className="worker-order-actions">
|
||||
<Space size={6} wrap>
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
aria-label="查看详情"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => onOpenDetail(row)}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
{canSubmitGiftMaterial(row) ? (
|
||||
<Button type="primary" onClick={() => onOpenGiftMaterial(row)}>
|
||||
提交资料
|
||||
</Button>
|
||||
) : null}
|
||||
{!hasMultipleShares && canEditAcceptanceImages(row) ? (
|
||||
<Button
|
||||
icon={<PictureOutlined />}
|
||||
type={canSubmitAcceptance(row) ? 'primary' : 'default'}
|
||||
onClick={() => onOpenAcceptance(row)}
|
||||
>
|
||||
{hasSubmittedAcceptance(row) ? '补充图片' : '验收'}
|
||||
</Button>
|
||||
) : null}
|
||||
{!hasMultipleShares && canRemindAcceptance(row) ? (
|
||||
<Button onClick={() => onRemindAcceptance(row)}>催验收</Button>
|
||||
) : null}
|
||||
{!hasMultipleShares && canRequestCancel(row) ? (
|
||||
<Button danger onClick={() => onOpenCancelRequest(row)}>
|
||||
申请取消
|
||||
</Button>
|
||||
) : null}
|
||||
{hasMultipleShares ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
展开行查看 {shareCount} 份份额操作
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{canSubmitFeedback(row) ? (
|
||||
<Tooltip title="反馈客户资料或订单处理问题">
|
||||
<Button icon={<MessageOutlined />} onClick={() => onOpenFeedback(row)}>
|
||||
反馈
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { App, Badge, Button, Form, Space, Typography } from 'antd'
|
||||
import { App, Badge, Button, Form, Modal, Radio, Space, Tag, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
@@ -22,11 +22,13 @@ import {
|
||||
} from '@/services/worker'
|
||||
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
|
||||
import { isExplicitOrderNoSearch, normalizeSearchText } from '@/utils/admin-realtime-work-orders'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
import { buildWorkerOrderQuery } from '@/utils/worker-order-query'
|
||||
import { WorkerOrderDesktopList } from './WorkerOrderDesktopList'
|
||||
import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer'
|
||||
import { WorkerOrderMobileList } from './WorkerOrderMobileList'
|
||||
import { WorkerOrderShareTable } from './WorkerOrderShareTable'
|
||||
import {
|
||||
type AcceptanceFormValues,
|
||||
type CancelRequestFormValues,
|
||||
@@ -41,6 +43,7 @@ import {
|
||||
canSupplementAcceptedEvidence,
|
||||
getCopyOrderInfoItems,
|
||||
getMaterialScreenshots,
|
||||
getOrderShares,
|
||||
getUniqueAcceptanceFiles,
|
||||
getWorkerAcceptance,
|
||||
getWorkerDraft,
|
||||
@@ -165,6 +168,15 @@ export default function WorkerOrdersPage() {
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
||||
const [editingOrder, setEditingOrder] = useState<WorkOrder | null>(null)
|
||||
/** 本次验收/补图针对的份额;null = 整单 */
|
||||
const [editingShareId, setEditingShareId] = useState<number | null>(null)
|
||||
/** 多份额时先让打手选择要操作哪一份 */
|
||||
const [sharePicker, setSharePicker] = useState<{
|
||||
order: WorkOrder
|
||||
action: 'accept' | 'cancel'
|
||||
} | null>(null)
|
||||
const [pickedShareId, setPickedShareId] = useState<number>(0)
|
||||
const [cancelShareId, setCancelShareId] = useState<number | null>(null)
|
||||
const [noteOrder, setNoteOrder] = useState<WorkOrder | null>(null)
|
||||
const [cancelRequestOrder, setCancelRequestOrder] = useState<WorkOrder | null>(null)
|
||||
const [feedbackOrder, setFeedbackOrder] = useState<WorkOrder | null>(null)
|
||||
@@ -292,10 +304,41 @@ export default function WorkerOrdersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 把订单对象替换成「指定份额」视角,让验收弹窗读写该份额的验收/草稿。 */
|
||||
function buildShareEditingOrder(order: WorkOrder, shareId: number | null): WorkOrder {
|
||||
if (!shareId) return order
|
||||
const share = getOrderShares(order).find((item) => item.shareId === shareId)
|
||||
if (!share) return order
|
||||
return {
|
||||
...order,
|
||||
myShare: share,
|
||||
// 已验收的份额走补图分支;其它份额沿用母单状态。
|
||||
status: share.status === 'accepted' ? 'accepted' : order.status,
|
||||
}
|
||||
}
|
||||
|
||||
function openAcceptanceModal(order: WorkOrder) {
|
||||
markOrderAsRead(order)
|
||||
setEditingOrder(order)
|
||||
if (canSupplementAcceptedEvidence(order)) {
|
||||
const shares = getOrderShares(order)
|
||||
if (shares.length > 1) {
|
||||
setPickedShareId(shares[0]?.shareId || 0)
|
||||
setSharePicker({ order, action: 'accept' })
|
||||
return
|
||||
}
|
||||
openAcceptanceWithShare(order, shares[0]?.shareId ?? null)
|
||||
}
|
||||
|
||||
/** 子表里直接对某一份份额发起验收/补图。 */
|
||||
function openAcceptanceForShare(order: WorkOrder, shareId: number) {
|
||||
markOrderAsRead(order)
|
||||
openAcceptanceWithShare(order, shareId)
|
||||
}
|
||||
|
||||
function openAcceptanceWithShare(order: WorkOrder, shareId: number | null) {
|
||||
const editingTarget = buildShareEditingOrder(order, shareId)
|
||||
setEditingShareId(shareId)
|
||||
setEditingOrder(editingTarget)
|
||||
if (canSupplementAcceptedEvidence(editingTarget)) {
|
||||
// 已验收补图只上传新增文件,原有验收图片由服务端保留。
|
||||
setAcceptanceFiles([])
|
||||
form.resetFields()
|
||||
@@ -303,9 +346,9 @@ export default function WorkerOrdersPage() {
|
||||
return
|
||||
}
|
||||
// 已提交:以已提交的验收资料为初始内容;未提交:以暂存草稿为初始内容
|
||||
const source = hasSubmittedAcceptance(order)
|
||||
? getWorkerAcceptance(order)
|
||||
: getWorkerDraft(order)
|
||||
const source = hasSubmittedAcceptance(editingTarget)
|
||||
? getWorkerAcceptance(editingTarget)
|
||||
: getWorkerDraft(editingTarget)
|
||||
setAcceptanceFiles(getUniqueAcceptanceFiles(source))
|
||||
form.setFieldsValue({ note: String(source?.note || '') })
|
||||
setDetailOrder(null)
|
||||
@@ -356,6 +399,21 @@ export default function WorkerOrdersPage() {
|
||||
|
||||
function openCancelRequestModal(order: WorkOrder) {
|
||||
markOrderAsRead(order)
|
||||
const shares = getOrderShares(order)
|
||||
if (shares.length > 1) {
|
||||
setPickedShareId(shares[0]?.shareId || 0)
|
||||
setSharePicker({ order, action: 'cancel' })
|
||||
return
|
||||
}
|
||||
setCancelShareId(shares[0]?.shareId ?? null)
|
||||
setCancelRequestOrder(order)
|
||||
cancelRequestForm.resetFields()
|
||||
}
|
||||
|
||||
/** 子表里直接对某一份份额申请取消。 */
|
||||
function openCancelRequestForShare(order: WorkOrder, shareId: number) {
|
||||
markOrderAsRead(order)
|
||||
setCancelShareId(shareId)
|
||||
setCancelRequestOrder(order)
|
||||
cancelRequestForm.resetFields()
|
||||
}
|
||||
@@ -365,6 +423,7 @@ export default function WorkerOrdersPage() {
|
||||
try {
|
||||
await submitWorkerCancelRequest(cancelRequestOrder.workOrderId, {
|
||||
reason: String(values.reason || '').trim(),
|
||||
shareId: cancelShareId,
|
||||
})
|
||||
message.success('取消申请已提交,等待客服审核')
|
||||
setCancelRequestOrder(null)
|
||||
@@ -430,6 +489,7 @@ export default function WorkerOrdersPage() {
|
||||
try {
|
||||
await supplementWorkerAcceptedOrderEvidence(editingOrder.workOrderId, {
|
||||
files: acceptanceFiles,
|
||||
...(editingShareId ? { shareId: editingShareId } : {}),
|
||||
})
|
||||
message.success('验收图片已补充')
|
||||
setEditingOrder(null)
|
||||
@@ -456,6 +516,7 @@ export default function WorkerOrdersPage() {
|
||||
await submitWorkerAcceptance(editingOrder.workOrderId, {
|
||||
note: values.note,
|
||||
files: acceptanceFiles,
|
||||
...(editingShareId ? { shareId: editingShareId } : {}),
|
||||
})
|
||||
message.success(canAutoAccept ? '已自动验收,可在 24 小时内补充图片' : '已提交验收')
|
||||
setEditingOrder(null)
|
||||
@@ -477,6 +538,7 @@ export default function WorkerOrdersPage() {
|
||||
await saveWorkerAcceptanceDraft(editingOrder.workOrderId, {
|
||||
note: values.note,
|
||||
files: acceptanceFiles,
|
||||
...(editingShareId ? { shareId: editingShareId } : {}),
|
||||
})
|
||||
message.success(
|
||||
hasSubmittedAcceptance(editingOrder) ? '验收图片已更新' : '验收图片已暂存,可继续补充',
|
||||
@@ -501,6 +563,20 @@ export default function WorkerOrdersPage() {
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
function renderShareTable(order: WorkOrder) {
|
||||
return (
|
||||
<WorkerOrderShareTable
|
||||
order={order}
|
||||
mobile={isMobile}
|
||||
onOpenAcceptance={openAcceptanceForShare}
|
||||
onOpenCancelRequest={openCancelRequestForShare}
|
||||
onRemindAcceptance={(target) => {
|
||||
void submitAcceptanceReminder(target)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columns = buildWorkerOrderTableColumns({
|
||||
onOpenDetail: openOrderDetail,
|
||||
onOpenAcceptance: openAcceptanceModal,
|
||||
@@ -562,6 +638,7 @@ export default function WorkerOrdersPage() {
|
||||
pagination={pagination}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
renderShareTable={renderShareTable}
|
||||
loading={ordersQuery.isLoading}
|
||||
searching={ordersQuery.isFetching}
|
||||
refreshing={ordersQuery.isFetching || overviewQuery.isFetching}
|
||||
@@ -611,6 +688,7 @@ export default function WorkerOrdersPage() {
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
columns={columns}
|
||||
renderExpandedRow={renderShareTable}
|
||||
loading={ordersQuery.isLoading}
|
||||
searching={ordersQuery.isFetching}
|
||||
refreshing={ordersQuery.isFetching || overviewQuery.isFetching}
|
||||
@@ -695,6 +773,57 @@ export default function WorkerOrdersPage() {
|
||||
onGiftMaterialFilesChange={setGiftMaterialFiles}
|
||||
onSubmitGiftMaterial={submitGiftMaterial}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={Boolean(sharePicker)}
|
||||
title={sharePicker?.action === 'cancel' ? '选择要撤销的份额' : '选择要验收的份额'}
|
||||
okText={sharePicker?.action === 'cancel' ? '提交退单申请' : '继续'}
|
||||
cancelText="取消"
|
||||
onCancel={() => setSharePicker(null)}
|
||||
onOk={() => {
|
||||
if (!sharePicker) return
|
||||
if (sharePicker.action === 'accept') {
|
||||
openAcceptanceWithShare(sharePicker.order, pickedShareId || null)
|
||||
} else {
|
||||
setCancelShareId(pickedShareId || null)
|
||||
setCancelRequestOrder(sharePicker.order)
|
||||
cancelRequestForm.resetFields()
|
||||
}
|
||||
setSharePicker(null)
|
||||
}}
|
||||
>
|
||||
<Radio.Group
|
||||
value={pickedShareId}
|
||||
onChange={(event) => setPickedShareId(Number(event.target.value))}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 10 }}
|
||||
>
|
||||
{sharePicker && sharePicker.action === 'cancel' ? (
|
||||
<Radio value={0}>全部有效份额(整单退单)</Radio>
|
||||
) : null}
|
||||
{sharePicker
|
||||
? getOrderShares(sharePicker.order).map((share) => (
|
||||
<Radio key={share.shareId} value={share.shareId}>
|
||||
<Space size={6} wrap>
|
||||
<span>#{share.shareId}</span>
|
||||
<Tag color="purple">{share.quantity} 份</Tag>
|
||||
<span>
|
||||
{share.status === 'joined'
|
||||
? '进行中'
|
||||
: share.status === 'submitted'
|
||||
? '待验收'
|
||||
: share.status === 'accepted'
|
||||
? '已验收'
|
||||
: share.status}
|
||||
</span>
|
||||
<span style={{ color: '#8c8c8c' }}>
|
||||
{share.deadlineAt ? `${formatDateTime(share.deadlineAt)} 截止` : '不限时'}
|
||||
</span>
|
||||
</Space>
|
||||
</Radio>
|
||||
))
|
||||
: null}
|
||||
</Radio.Group>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
UploadedFile,
|
||||
WorkerAcceptance,
|
||||
WorkOrder,
|
||||
WorkOrderShare,
|
||||
} from '@/types/worker-platform'
|
||||
import { getImageIdentity } from '@/utils/image-identity'
|
||||
|
||||
@@ -33,6 +34,13 @@ export function patchWorkOrderReadState(current: unknown, workOrderId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 同一打手在该工单的全部有效份额;单份额订单回退为 myShare 一条。 */
|
||||
export function getOrderShares(order: WorkOrder | null | undefined): WorkOrderShare[] {
|
||||
if (!order) return []
|
||||
if (order.myShares?.length) return order.myShares
|
||||
return order.myShare ? [order.myShare] : []
|
||||
}
|
||||
|
||||
/** 好友赠送验收模式(整单,不参与拼单)。 */
|
||||
export function isFriendGiftOrder(order: WorkOrder) {
|
||||
return order.acceptanceMode === 'friend_gift' && !order.myShare
|
||||
|
||||
@@ -220,12 +220,18 @@ export function fetchWorkerHallLeaderboard(period: WorkerLeaderboardPeriod = 'al
|
||||
return apiGet<WorkerLeaderboardResponse>('/api/v1/worker/hall/leaderboard', { period })
|
||||
}
|
||||
|
||||
export function grabWorkerOrder(workOrderId: number) {
|
||||
return apiPost<{ order: WorkOrder }>(`/api/v1/worker/hall/orders/${workOrderId}/grab`)
|
||||
export function grabWorkerOrder(workOrderId: number, payload: { requestId?: string } = {}) {
|
||||
return apiPost<{ order: WorkOrder; duplicated?: boolean }>(
|
||||
`/api/v1/worker/hall/orders/${workOrderId}/grab`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function joinWorkerSharingOrder(workOrderId: number, payload: { quantity: number }) {
|
||||
return apiPost<{ share: WorkOrderShare }>(
|
||||
export function joinWorkerSharingOrder(
|
||||
workOrderId: number,
|
||||
payload: { quantity: number; requestId?: string },
|
||||
) {
|
||||
return apiPost<{ share: WorkOrderShare; duplicated?: boolean }>(
|
||||
`/api/v1/worker/hall/orders/${workOrderId}/join-sharing`,
|
||||
payload,
|
||||
)
|
||||
@@ -249,7 +255,10 @@ export function fetchWorkerCancelRequests() {
|
||||
)
|
||||
}
|
||||
|
||||
export function submitWorkerCancelRequest(workOrderId: number, payload: { reason: string }) {
|
||||
export function submitWorkerCancelRequest(
|
||||
workOrderId: number,
|
||||
payload: { reason: string; shareId?: number | null },
|
||||
) {
|
||||
return apiPost<{ request: WorkerCancelRequest; risk: WorkerCancelRequestRisk }>(
|
||||
`/api/v1/worker/orders/${workOrderId}/cancel-requests`,
|
||||
payload,
|
||||
@@ -294,9 +303,9 @@ export function saveWorkerOrderNote(workOrderId: number, note: string) {
|
||||
|
||||
export function submitWorkerAcceptance(
|
||||
workOrderId: number,
|
||||
payload: { note?: string; imageUrls?: string[]; files?: UploadedFile[] },
|
||||
payload: { note?: string; imageUrls?: string[]; files?: UploadedFile[]; shareId?: number },
|
||||
) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
return apiPost<{ order: WorkOrder; share?: WorkOrderShare }>(
|
||||
`/api/v1/worker/orders/${workOrderId}/submit-acceptance`,
|
||||
payload,
|
||||
)
|
||||
@@ -321,9 +330,9 @@ export function remindWorkerAcceptance(workOrderId: number) {
|
||||
|
||||
export function saveWorkerAcceptanceDraft(
|
||||
workOrderId: number,
|
||||
payload: { note?: string; imageUrls?: string[]; files?: UploadedFile[] },
|
||||
payload: { note?: string; imageUrls?: string[]; files?: UploadedFile[]; shareId?: number },
|
||||
) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
return apiPost<{ order: WorkOrder; share?: WorkOrderShare }>(
|
||||
`/api/v1/worker/orders/${workOrderId}/acceptance-draft`,
|
||||
payload,
|
||||
)
|
||||
@@ -331,9 +340,9 @@ export function saveWorkerAcceptanceDraft(
|
||||
|
||||
export function supplementWorkerAcceptedOrderEvidence(
|
||||
workOrderId: number,
|
||||
payload: { files: UploadedFile[] },
|
||||
payload: { files: UploadedFile[]; shareId?: number },
|
||||
) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
return apiPost<{ order: WorkOrder; share?: WorkOrderShare }>(
|
||||
`/api/v1/worker/orders/${workOrderId}/acceptance-evidence`,
|
||||
payload,
|
||||
)
|
||||
|
||||
@@ -4853,3 +4853,26 @@ body {
|
||||
padding-top: 6px;
|
||||
border-top: 1px dashed #bfdbfe;
|
||||
}
|
||||
|
||||
/* 我的订单:一行一工单,展开后的份额子表 */
|
||||
.worker-orders-table .ant-table-expanded-row > td {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.worker-order-share-table .ant-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.worker-order-share-table .ant-table-thead > tr > th {
|
||||
background: #f5f5f5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-shares {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 8px 12px 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #f0f0f0;
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ export type WorkerPlatformNotificationEventKey =
|
||||
| 'recharge_requested'
|
||||
| 'acceptance_submitted'
|
||||
| 'acceptance_reminded'
|
||||
| 'order_accepted'
|
||||
| 'material_required'
|
||||
| 'cancel_requested'
|
||||
| 'feedback_submitted'
|
||||
@@ -539,6 +540,11 @@ export type WorkOrderShare = {
|
||||
acceptedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
/** 本份额独立截止时间(参与时固化);null = 不限时 */
|
||||
deadlineAt?: string | null
|
||||
timeoutMinutes?: number
|
||||
timeoutPolicy?: string
|
||||
cancelledReason?: string
|
||||
worker: null | {
|
||||
workerId: number
|
||||
username: string
|
||||
@@ -620,6 +626,8 @@ export type WorkOrder = {
|
||||
freezeDepositAmount?: number
|
||||
sharing?: WorkOrderSharingConfig
|
||||
myShare?: WorkOrderShare | null
|
||||
/** 同一打手在该工单的全部有效份额(一行一工单 + 展开份额) */
|
||||
myShares?: WorkOrderShare[]
|
||||
sharingProgress?: {
|
||||
joinedQuantity: number
|
||||
pendingSubmissionCount: number
|
||||
|
||||
Reference in New Issue
Block a user