增加售后相关的

This commit is contained in:
yml2213
2026-08-21 14:30:00 +08:00
parent 874b093a4b
commit f4d32a7c07
18 changed files with 1903 additions and 3 deletions
@@ -214,6 +214,29 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
return { title: '问题单已处置:取消订单并释放押金', color: 'orange' }
case 'problem_cancel_deduct':
return { title: '问题单已处置:取消订单并扣除押金', color: 'orange' }
case 'after_sales_case_created': {
const caseNo = readPayload(event, 'caseNo')
return {
title: `已发起验收后售后问题单${caseNo ? `${caseNo}` : ''}`,
color: 'red',
}
}
case 'after_sales_case_responded':
return { title: '打手已提交售后说明,等待客服处理', color: 'orange' }
case 'after_sales_case_resolved': {
const action = readPayload(event, 'action')
const recoveryAmount = Number(readPayload(event, 'recoveryAmount') || 0)
if (action === 'upheld') {
return {
title: `售后问题成立,追缴 ${recoveryAmount > 0 ? `${(recoveryAmount / 100).toFixed(2)}` : '0 元'}`,
color: 'red',
}
}
return {
title: action === 'remedied' ? '售后已补救完成' : '售后问题不成立,已关闭',
color: 'green',
}
}
// —— 取消节点 ——
case 'reopened_from_cancelled':
@@ -354,6 +354,7 @@ function applyAdminRealtimeEvent(
void queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-order-sharing', event.entityId],
})
void queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-after-sales-cases'] })
if (isRecord(event.data)) {
const data = event.data
patchAdminWorkOrderCaches(queryClient, { ...event, data })
@@ -164,6 +164,7 @@ function applyWorkerRealtimeEvent(
const data = event.data
if (event.scopes.includes('worker_my_orders')) {
patchWorkerMyOrderCaches(queryClient, { ...event, data })
void queryClient.invalidateQueries({ queryKey: ['worker-after-sales-cases'] })
}
if (event.scopes.includes('worker_hall')) {
patchWorkerHallCaches(queryClient, { ...event, data })
@@ -51,10 +51,12 @@ import {
cancelAdminWorkOrder,
cancelAdminWorkOrderShare,
createAdminMockWorkOrder,
createAdminAfterSalesCase,
deductAdminWorkOrderPendingDeposit,
deleteAdminWorkOrder,
fetchAdminWorkCategories,
fetchAdminWorkOrderSharing,
fetchAdminAfterSalesCases,
fetchAdminWorkerLevels,
fetchAdminWorkerUsers,
fetchAdminWorkOrders,
@@ -63,6 +65,7 @@ import {
publishAdminWorkOrder,
reopenAdminCancelledWorkOrder,
resolveAdminProblemWorkOrder,
resolveAdminAfterSalesCase,
submitAdminWorkOrderMaterial,
unpublishAdminWorkOrder,
updateAdminWorkOrder,
@@ -75,6 +78,7 @@ import type {
WorkOrder,
WorkOrderShare,
WorkOrderStatistics,
WorkerAfterSalesCase,
} from '@/types/worker-platform'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import { hasAdminRole } from '@/utils/admin-auth'
@@ -129,6 +133,18 @@ type CancelShareFormValues = {
reason?: string
}
type AfterSalesCreateFormValues = {
shareId?: number
problemType?: string
complaintNote?: string
}
type AfterSalesResolutionFormValues = {
action?: 'not_upheld' | 'remedied' | 'upheld'
resolutionNote?: string
recoveryAmount?: number
}
export default function WorkOrdersPanel() {
const { message, modal } = App.useApp()
const navigate = useNavigate()
@@ -160,12 +176,17 @@ export default function WorkOrdersPanel() {
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
const [cancelOrder, setCancelOrder] = useState<WorkOrder | null>(null)
const [afterSalesOrder, setAfterSalesOrder] = useState<WorkOrder | null>(null)
const [afterSalesCase, setAfterSalesCase] = useState<WorkerAfterSalesCase | null>(null)
const [afterSalesFiles, setAfterSalesFiles] = useState<UploadedFile[]>([])
const [problemForm] = Form.useForm()
const [resolutionForm] = Form.useForm()
const [materialForm] = Form.useForm<MaterialFormValues>()
const [cancelForm] = Form.useForm<CancelOrderFormValues>()
const [cancelShareForm] = Form.useForm<CancelShareFormValues>()
const [deductForm] = Form.useForm<{ amount?: number; note?: string }>()
const [afterSalesCreateForm] = Form.useForm<AfterSalesCreateFormValues>()
const [afterSalesResolutionForm] = Form.useForm<AfterSalesResolutionFormValues>()
const [editForm] = Form.useForm<{
productName?: string
platformOrderId?: string
@@ -258,6 +279,15 @@ export default function WorkOrdersPanel() {
queryFn: () => fetchAdminWorkOrderSharing(Number(sharingOrder?.workOrderId)),
enabled: Boolean(sharingOrder),
})
const afterSalesSharingQuery = useQuery({
queryKey: ['admin-worker-platform-after-sales-sharing', afterSalesOrder?.workOrderId],
queryFn: () => fetchAdminWorkOrderSharing(Number(afterSalesOrder?.workOrderId)),
enabled: Boolean(afterSalesOrder?.sharing?.enabled),
})
const afterSalesCasesQuery = useQuery({
queryKey: ['admin-worker-platform-after-sales-cases'],
queryFn: () => fetchAdminAfterSalesCases({ page: 1, pageSize: 100 }),
})
const assignWorkersQuery = useQuery({
queryKey: ['admin-worker-platform-workers', 'assign', assignWorkerKeyword],
queryFn: () =>
@@ -414,6 +444,9 @@ export default function WorkOrdersPanel() {
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-summary'],
}),
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-after-sales-cases'],
}),
])
}
@@ -625,6 +658,60 @@ export default function WorkOrdersPanel() {
})
}
function openAfterSalesCreateModal(row: WorkOrder) {
setAfterSalesOrder(row)
setAfterSalesFiles([])
afterSalesCreateForm.setFieldsValue({
shareId: undefined,
problemType: 'fake_evidence',
complaintNote: '',
})
}
async function submitAfterSalesCreate(values: AfterSalesCreateFormValues) {
if (!afterSalesOrder) return
const succeeded = await runAction(
() =>
createAdminAfterSalesCase(afterSalesOrder.workOrderId, {
shareId: values.shareId || null,
problemType: values.problemType || 'fake_evidence',
complaintNote: String(values.complaintNote || '').trim(),
files: afterSalesFiles,
}),
'售后问题单已创建,已通知责任打手说明',
)
if (!succeeded) return
setAfterSalesOrder(null)
setAfterSalesFiles([])
afterSalesCreateForm.resetFields()
}
function openAfterSalesResolutionModal(item: WorkerAfterSalesCase) {
setAfterSalesCase(item)
afterSalesResolutionForm.setFieldsValue({
action: 'upheld',
resolutionNote: '',
recoveryAmount: Number(item.recoveryAmount || 0) / 100,
})
}
async function submitAfterSalesResolution(values: AfterSalesResolutionFormValues) {
const action = values.action
if (!afterSalesCase || !action) return
const succeeded = await runAction(
() =>
resolveAdminAfterSalesCase(afterSalesCase.caseId, {
action,
resolutionNote: String(values.resolutionNote || '').trim(),
recoveryAmount: action === 'upheld' ? Number(values.recoveryAmount || 0) : 0,
}),
'售后问题单已处置',
)
if (!succeeded) return
setAfterSalesCase(null)
afterSalesResolutionForm.resetFields()
}
async function submitDeduct(values: { amount?: number; note?: string }) {
if (!deductOrder) return
const succeeded = await runAction(
@@ -983,6 +1070,15 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{row.status === 'accepted' ? (
<Button
icon={<WarningOutlined />}
danger
onClick={() => openAfterSalesCreateModal(row)}
>
</Button>
) : null}
{row.capabilities?.canMarkProblem ? (
<Button icon={<WarningOutlined />} danger onClick={() => setProblemOrder(row)}>
@@ -1154,6 +1250,138 @@ export default function WorkOrdersPanel() {
scroll={{ x: 1240 }}
/>
</Card>
<Card
title="验收后售后问题单"
extra={
<Button
icon={<ReloadOutlined />}
loading={afterSalesCasesQuery.isFetching}
onClick={() => afterSalesCasesQuery.refetch()}
>
</Button>
}
bordered={false}
>
<Table<WorkerAfterSalesCase>
rowKey="caseId"
loading={afterSalesCasesQuery.isLoading}
dataSource={afterSalesCasesQuery.data?.data.items || []}
pagination={false}
scroll={{ x: 1160 }}
columns={[
{
title: '售后单',
width: 190,
render: (_, item) => (
<div className="cell-stack">
<Typography.Text strong>{item.caseNo}</Typography.Text>
<Typography.Text type="secondary">{item.workOrderNo || '-'}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatAdminDateTime(item.createdAt)}
</Typography.Text>
</div>
),
},
{
title: '责任打手',
width: 150,
render: (_, item) => (
<div className="cell-stack">
<Typography.Text>{item.workerName || `打手#${item.workerId}`}</Typography.Text>
{item.shareQuantity ? (
<Tag color="purple"> {item.shareQuantity} </Tag>
) : null}
</div>
),
},
{
title: '投诉与证据',
width: 260,
render: (_, item) => (
<div className="cell-stack">
<Tag color="red">{formatAfterSalesProblemType(item.problemType)}</Tag>
<Typography.Text ellipsis={{ tooltip: item.complaintNote }}>
{item.complaintNote || '-'}
</Typography.Text>
{item.complaintFiles.length > 0 ? (
<ImagePreviewList files={item.complaintFiles} size={36} maxVisible={3} />
) : null}
{getAfterSalesSnapshotFiles(item).length > 0 ? (
<Space size={6} align="center">
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
<ImagePreviewList
files={getAfterSalesSnapshotFiles(item)}
size={36}
maxVisible={3}
/>
</Space>
) : null}
</div>
),
},
{
title: '打手说明',
width: 250,
render: (_, item) => (
<div className="cell-stack">
<Typography.Text ellipsis={{ tooltip: item.workerResponse }}>
{item.workerResponse || '暂未提交'}
</Typography.Text>
{item.workerResponseFiles.length > 0 ? (
<ImagePreviewList files={item.workerResponseFiles} size={36} maxVisible={3} />
) : null}
{item.respondedAt ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatAdminDateTime(item.respondedAt)}
</Typography.Text>
) : null}
</div>
),
},
{
title: '处置与追缴',
width: 240,
render: (_, item) => (
<div className="cell-stack">
<Tag color={resolveAfterSalesStatusColor(item.status)}>
{formatAfterSalesStatus(item.status)}
</Tag>
{item.resolutionAction ? (
<Typography.Text type="secondary" ellipsis={{ tooltip: item.resolutionNote }}>
{formatAfterSalesResolution(item.resolutionAction)} ·{' '}
{item.resolutionNote || '-'}
</Typography.Text>
) : null}
{item.resolutionAction === 'upheld' ? (
<Typography.Text type={item.debtAmount > 0 ? 'danger' : undefined}>
{formatMoney(item.recoveryAmount)} {formatMoney(item.debtAmount)}
</Typography.Text>
) : null}
</div>
),
},
{
title: '操作',
width: 120,
render: (_, item) =>
['awaiting_worker_response', 'under_review'].includes(item.status) ? (
<Button
type="primary"
size="small"
onClick={() => openAfterSalesResolutionModal(item)}
>
</Button>
) : (
<Typography.Text type="secondary"></Typography.Text>
),
},
]}
/>
</Card>
<Drawer
className="worker-order-detail-drawer"
title={
@@ -1486,6 +1714,132 @@ export default function WorkOrdersPanel() {
</Form>
</Modal>
<Modal
title={`发起验收后售后 · ${getDisplayOrderNo(afterSalesOrder)}`}
open={Boolean(afterSalesOrder)}
onCancel={() => {
setAfterSalesOrder(null)
setAfterSalesFiles([])
afterSalesCreateForm.resetFields()
}}
onOk={() => afterSalesCreateForm.submit()}
destroyOnHidden
>
{afterSalesOrder ? (
<Form form={afterSalesCreateForm} layout="vertical" onFinish={submitAfterSalesCreate}>
<Alert
showIcon
type="warning"
message="售后不会回退已验收工单;原验收资料会在创建时固化。"
style={{ marginBottom: 16 }}
/>
{(afterSalesSharingQuery.data?.data.shares || []).some(
(share) => share.status === 'accepted',
) ? (
<Form.Item
label="责任拼单份额"
name="shareId"
rules={[{ required: true, message: '请选择责任打手的已验收份额' }]}
>
<Select
loading={afterSalesSharingQuery.isLoading}
placeholder="请选择责任拼单份额"
options={(afterSalesSharingQuery.data?.data.shares || [])
.filter((share) => share.status === 'accepted')
.map((share) => ({
value: share.shareId,
label: `${share.worker?.displayName || share.worker?.username || `打手#${share.workerId}`} · ${share.quantity}`,
}))}
/>
</Form.Item>
) : null}
<Form.Item label="问题类型" name="problemType" rules={[{ required: true }]}>
<Select
options={[
{ value: 'fake_evidence', label: '疑似 P 图 / 伪造验收' },
{ value: 'result_not_met', label: '实际结果未达标' },
{ value: 'other', label: '其他售后问题' },
]}
/>
</Form.Item>
<Form.Item
label="用户投诉说明"
name="complaintNote"
rules={[{ required: true, message: '请填写用户投诉说明' }]}
>
<Input.TextArea rows={4} placeholder="记录用户反馈、核查结论及需打手说明的事项" />
</Form.Item>
<Form.Item label="用户证据图片">
<ImageUpload
scene="collect-material"
scope="admin"
value={afterSalesFiles}
onChange={setAfterSalesFiles}
maxCount={10}
showPasteHint
/>
</Form.Item>
</Form>
) : null}
</Modal>
<Modal
title={`处置售后问题单 · ${afterSalesCase?.caseNo || ''}`}
open={Boolean(afterSalesCase)}
onCancel={() => {
setAfterSalesCase(null)
afterSalesResolutionForm.resetFields()
}}
onOk={() => afterSalesResolutionForm.submit()}
destroyOnHidden
>
{afterSalesCase ? (
<Form
form={afterSalesResolutionForm}
layout="vertical"
onFinish={submitAfterSalesResolution}
>
<Form.Item
label="处置结论"
name="action"
rules={[{ required: true, message: '请选择处置结论' }]}
>
<Select
options={[
{ value: 'upheld', label: '问题成立,执行追缴' },
{ value: 'remedied', label: '已补救完成,不追缴' },
{ value: 'not_upheld', label: '问题不成立,关闭售后' },
]}
/>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(previous, current) => previous.action !== current.action}
>
{({ getFieldValue }) =>
getFieldValue('action') === 'upheld' ? (
<Form.Item
label="追缴金额"
name="recoveryAmount"
rules={[{ required: true, message: '请输入追缴金额' }]}
extra="系统依次扣减该责任打手的本单待解冻押金、可用余额;不足部分转为欠款,并从后续验收报酬自动抵扣。"
>
<InputNumber min={0} precision={2} addonAfter="元" className="full-width" />
</Form.Item>
) : null
}
</Form.Item>
<Form.Item
label="处置备注"
name="resolutionNote"
rules={[{ required: true, message: '请填写处置备注' }]}
>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
) : null}
</Modal>
<Modal
title="处置问题单"
open={Boolean(resolutionOrder)}
@@ -2294,6 +2648,58 @@ function formatRequirementFieldsText(fields: CollectField[]): string {
.join('\n')
}
function formatAfterSalesProblemType(value: string): string {
if (value === 'fake_evidence') return '疑似 P 图 / 伪造验收'
if (value === 'result_not_met') return '实际结果未达标'
return '其他售后问题'
}
function formatAfterSalesStatus(value: string): string {
if (value === 'awaiting_worker_response') return '待打手说明'
if (value === 'under_review') return '客服处理中'
if (value === 'recovery_pending') return '追缴中'
return '已关闭'
}
function resolveAfterSalesStatusColor(value: string): string {
if (value === 'awaiting_worker_response') return 'orange'
if (value === 'under_review') return 'blue'
if (value === 'recovery_pending') return 'red'
return 'green'
}
function formatAfterSalesResolution(value: string): string {
if (value === 'upheld') return '问题成立'
if (value === 'remedied') return '补救完成'
if (value === 'not_upheld') return '问题不成立'
return value || '-'
}
function getAfterSalesSnapshotFiles(item: WorkerAfterSalesCase): UploadedFile[] {
const snapshot = asRecord(item.acceptanceSnapshot)
const acceptance = asRecord(snapshot.acceptance)
const files = Array.isArray(acceptance.files) ? acceptance.files : []
const imageUrls = Array.isArray(acceptance.imageUrls) ? acceptance.imageUrls : []
return [
...files.map((value) => asRecord(value)),
...imageUrls.map((url) => asRecord({ url: String(url || '') })),
]
.map((file) => {
const url = String(file.url || '').trim()
if (!url) return null
return {
objectKey: String(file.objectKey || file.object_key || ''),
url,
thumbnailUrl: String(file.thumbnailUrl || file.thumbnail_url || url),
mediumUrl: String(file.mediumUrl || file.medium_url || url),
filename: String(file.filename || '验收图片'),
contentType: String(file.contentType || ''),
size: Number(file.size || 0),
} satisfies UploadedFile
})
.filter((file): file is UploadedFile => Boolean(file))
}
function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void> }) {
const { message } = App.useApp()
const [loading, setLoading] = useState(false)
@@ -10,6 +10,7 @@ import {
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
App,
Alert,
Button,
Card,
Descriptions,
@@ -36,6 +37,7 @@ import ImagePreviewList from '@/components/files/ImagePreviewList'
import ImageUpload from '@/components/files/ImageUpload'
import {
fetchWorkerCancelRequests,
fetchWorkerAfterSalesCases,
fetchWorkerOrderFeedbacks,
fetchWorkerMyOrderOverview,
fetchWorkerMyOrders,
@@ -47,6 +49,7 @@ import {
supplementWorkerAcceptedOrderEvidence,
submitWorkerAcceptance,
submitWorkerCancelRequest,
submitWorkerAfterSalesCaseResponse,
submitWorkerOrderFeedback,
} from '@/services/worker'
import type {
@@ -54,6 +57,7 @@ import type {
UploadedFile,
WorkOrder,
WorkerAcceptance,
WorkerAfterSalesCase,
} from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import { getImageIdentity } from '@/utils/image-identity'
@@ -87,6 +91,10 @@ type FeedbackFormValues = {
content?: string
}
type AfterSalesResponseFormValues = {
response?: string
}
type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default'
const PRESET_NOTE_TAGS: Array<{ label: string; theme: NoteTheme }> = [
@@ -201,11 +209,14 @@ export default function WorkerOrdersPage() {
const [noteOrder, setNoteOrder] = useState<WorkOrder | null>(null)
const [cancelRequestOrder, setCancelRequestOrder] = useState<WorkOrder | null>(null)
const [feedbackOrder, setFeedbackOrder] = useState<WorkOrder | null>(null)
const [afterSalesCase, setAfterSalesCase] = useState<WorkerAfterSalesCase | null>(null)
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
const [afterSalesResponseFiles, setAfterSalesResponseFiles] = useState<UploadedFile[]>([])
const [form] = Form.useForm<AcceptanceFormValues>()
const [noteForm] = Form.useForm<WorkerNoteFormValues>()
const [cancelRequestForm] = Form.useForm<CancelRequestFormValues>()
const [feedbackForm] = Form.useForm<FeedbackFormValues>()
const [afterSalesResponseForm] = Form.useForm<AfterSalesResponseFormValues>()
const ordersQuery = useQuery({
queryKey: ['worker-my-orders', status, keyword, page, pageSize],
@@ -240,9 +251,15 @@ export default function WorkerOrdersPage() {
queryFn: fetchWorkerOrderFeedbacks,
retry: false,
})
const afterSalesCasesQuery = useQuery({
queryKey: ['worker-after-sales-cases'],
queryFn: fetchWorkerAfterSalesCases,
retry: false,
})
const overviewByStatus = new Map(
(overviewQuery.data?.data.items || []).map((item) => [item.status, item]),
)
const afterSalesCases = afterSalesCasesQuery.data?.data.items || []
async function refreshAll() {
await Promise.all([
@@ -250,6 +267,7 @@ export default function WorkerOrdersPage() {
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
queryClient.invalidateQueries({ queryKey: ['worker-cancel-requests'] }),
queryClient.invalidateQueries({ queryKey: ['worker-order-feedbacks'] }),
queryClient.invalidateQueries({ queryKey: ['worker-after-sales-cases'] }),
])
}
@@ -377,6 +395,29 @@ export default function WorkerOrdersPage() {
}
}
function openAfterSalesResponseModal(item: WorkerAfterSalesCase) {
setAfterSalesCase(item)
setAfterSalesResponseFiles([])
afterSalesResponseForm.resetFields()
}
async function submitAfterSalesResponse(values: AfterSalesResponseFormValues) {
if (!afterSalesCase) return
try {
await submitWorkerAfterSalesCaseResponse(afterSalesCase.caseId, {
response: String(values.response || '').trim(),
files: afterSalesResponseFiles,
})
message.success('售后说明已提交,等待客服处理')
setAfterSalesCase(null)
setAfterSalesResponseFiles([])
afterSalesResponseForm.resetFields()
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '提交售后说明失败')
}
}
async function saveOrderNote(values: WorkerNoteFormValues) {
if (!noteOrder) return
try {
@@ -727,6 +768,65 @@ export default function WorkerOrdersPage() {
return (
<>
{afterSalesCases.length > 0 ? (
<Card
className="worker-after-sales-card"
title="售后问题单"
size="small"
extra={
<Button
size="small"
icon={<ReloadOutlined />}
loading={afterSalesCasesQuery.isFetching}
onClick={() => afterSalesCasesQuery.refetch()}
>
</Button>
}
>
<Space direction="vertical" size={12} style={{ width: '100%' }}>
{afterSalesCases.map((item) => (
<div key={item.caseId} className="worker-order-detail-value">
<Space wrap size={[8, 6]}>
<Tag color={resolveWorkerAfterSalesStatusColor(item.status)}>
{formatWorkerAfterSalesStatus(item.status)}
</Tag>
<Typography.Text strong>
{item.productName || item.workOrderNo || item.caseNo}
</Typography.Text>
<Typography.Text type="secondary">{item.caseNo}</Typography.Text>
</Space>
<Typography.Paragraph style={{ margin: '8px 0 4px' }}>
{item.complaintNote || '-'}
</Typography.Paragraph>
{item.complaintFiles.length > 0 ? (
<ImagePreviewList files={item.complaintFiles} size={52} maxVisible={4} />
) : null}
{item.status === 'awaiting_worker_response' ? (
<Button
type="primary"
size="small"
style={{ marginTop: 8 }}
onClick={() => openAfterSalesResponseModal(item)}
>
</Button>
) : item.status === 'under_review' ? (
<Typography.Text type="secondary"></Typography.Text>
) : item.status === 'recovery_pending' ? (
<Typography.Text type="danger">
{formatMoney(item.debtAmount)}
</Typography.Text>
) : item.resolutionNote ? (
<Typography.Text type="secondary">
{item.resolutionNote}
</Typography.Text>
) : null}
</div>
))}
</Space>
</Card>
) : null}
{isMobile ? (
<div className="worker-orders-mobile-page">
{/* 顶部水平滑动状态切换胶囊条 */}
@@ -1406,6 +1506,52 @@ export default function WorkerOrdersPage() {
</Form.Item>
</Form>
</Modal>
<Modal
title={`售后说明 · ${afterSalesCase?.caseNo || ''}`}
open={Boolean(afterSalesCase)}
width={isMobile ? '92%' : 560}
onCancel={() => {
setAfterSalesCase(null)
setAfterSalesResponseFiles([])
afterSalesResponseForm.resetFields()
}}
onOk={() => afterSalesResponseForm.submit()}
okText="提交说明"
destroyOnHidden
>
{afterSalesCase ? (
<>
<Alert
type="warning"
showIcon
message="请针对售后问题说明情况或提交补救材料,原验收资料不可在此修改。"
style={{ marginBottom: 16 }}
/>
<Typography.Paragraph type="secondary">
{afterSalesCase.complaintNote}
</Typography.Paragraph>
<Form
form={afterSalesResponseForm}
layout="vertical"
onFinish={submitAfterSalesResponse}
>
<Form.Item label="说明" name="response">
<Input.TextArea rows={4} maxLength={2000} showCount />
</Form.Item>
<Form.Item label="补救材料图片">
<ImageUpload
scene="worker-acceptance"
scope="worker"
value={afterSalesResponseFiles}
onChange={setAfterSalesResponseFiles}
maxCount={10}
showPasteHint
/>
</Form.Item>
</Form>
</>
) : null}
</Modal>
<Modal
title={`订单备注 · ${getDisplayOrderNo(noteOrder)}`}
open={Boolean(noteOrder)}
@@ -1815,6 +1961,20 @@ function getSettlementLabel(status: string) {
return '预计'
}
function formatWorkerAfterSalesStatus(status: string) {
if (status === 'awaiting_worker_response') return '待说明'
if (status === 'under_review') return '处理中'
if (status === 'recovery_pending') return '追缴中'
return '已关闭'
}
function resolveWorkerAfterSalesStatusColor(status: string) {
if (status === 'awaiting_worker_response') return 'orange'
if (status === 'under_review') return 'blue'
if (status === 'recovery_pending') return 'red'
return 'green'
}
function formatStatus(status: string) {
const labels: Record<string, string> = {
open: '已超时',
@@ -16,6 +16,8 @@ import type {
WorkerFinanceRequest,
WorkerCancelRequest,
WorkerOrderFeedback,
WorkerAfterSalesCase,
WorkerAfterSalesCaseEvent,
WorkerPlatformNotificationConfig,
WorkerProductMatchConfig,
WorkerAnnouncementConfig,
@@ -432,6 +434,48 @@ export function fetchAdminWorkOrders(params?: Record<string, unknown>) {
>('/api/v1/admin/worker-platform/orders', params)
}
export function fetchAdminAfterSalesCases(params?: Record<string, unknown>) {
return apiGet<WorkerListResponse<WorkerAfterSalesCase>>(
'/api/v1/admin/worker-platform/after-sales-cases',
params,
)
}
export function fetchAdminAfterSalesCaseEvents(caseId: number) {
return apiGet<{ items: WorkerAfterSalesCaseEvent[] }>(
`/api/v1/admin/worker-platform/after-sales-cases/${caseId}/events`,
)
}
export function createAdminAfterSalesCase(
workOrderId: number,
payload: {
shareId?: number | null
problemType?: string
complaintNote: string
files?: UploadedFile[]
},
) {
return apiPost<{ case: WorkerAfterSalesCase }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/after-sales-cases`,
payload,
)
}
export function resolveAdminAfterSalesCase(
caseId: number,
payload: {
action: 'not_upheld' | 'remedied' | 'upheld'
resolutionNote: string
recoveryAmount?: number
},
) {
return apiPost<{ case: WorkerAfterSalesCase }>(
`/api/v1/admin/worker-platform/after-sales-cases/${caseId}/resolve`,
payload,
)
}
export function fetchAdminWorkOrderEvents(workOrderId: number) {
return apiGet<{ items: WorkOrderEvent[] }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/events`,
+15
View File
@@ -10,6 +10,7 @@ import type {
WorkerCancelRequestRisk,
WorkerAnnouncementConfig,
WorkerOrderFeedback,
WorkerAfterSalesCase,
WorkerHallOrdersResponse,
WorkerLeaderboardResponse,
WorkerLeaderboardPeriod,
@@ -241,6 +242,20 @@ export function fetchWorkerOrderFeedbacks() {
return apiGet<{ items: WorkerOrderFeedback[] }>('/api/v1/worker/orders/feedbacks')
}
export function fetchWorkerAfterSalesCases() {
return apiGet<{ items: WorkerAfterSalesCase[] }>('/api/v1/worker/orders/after-sales-cases')
}
export function submitWorkerAfterSalesCaseResponse(
caseId: number,
payload: { response?: string; files?: UploadedFile[] },
) {
return apiPost<{ case: WorkerAfterSalesCase }>(
`/api/v1/worker/orders/after-sales-cases/${caseId}/response`,
payload,
)
}
export function submitWorkerOrderFeedback(workOrderId: number, payload: { content: string }) {
return apiPost<{ feedback: WorkerOrderFeedback }>(
`/api/v1/worker/orders/${workOrderId}/feedbacks`,
@@ -151,6 +151,48 @@ export type WorkerOrderFeedback = {
handledAt: string | null
}
export type WorkerAfterSalesCase = {
caseId: number
caseNo: string
workOrderId: number
shareId: number | null
workerId: number
workOrderNo: string
platformOrderId: string
productName: string
shopName: string
workerName: string
shareQuantity: number | null
problemType: string
complaintNote: string
complaintFiles: UploadedFile[]
acceptanceSnapshot: Record<string, unknown>
status: 'awaiting_worker_response' | 'under_review' | 'recovery_pending' | 'closed' | string
workerResponse: string
workerResponseFiles: UploadedFile[]
respondedAt: string | null
resolutionAction: 'not_upheld' | 'remedied' | 'upheld' | string
resolutionNote: string
recoveryAmount: number
pendingDepositDeductionAmount: number
availableDeductionAmount: number
debtAmount: number
createdBy: string
resolvedBy: string
resolvedAt: string | null
createdAt: string
updatedAt: string
}
export type WorkerAfterSalesCaseEvent = {
eventId: number
actorType: string
actorId: string
eventType: string
payload: Record<string, unknown>
createdAt: string
}
export type WorkerFinanceConfig = {
depositUnfreezeDays: number
recharge: {