优化打手端售后问题单页面展示问题类型处理结果与提交说明
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
||||
App,
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
@@ -20,6 +19,7 @@ import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import { fetchWorkerAfterSalesCases, submitWorkerAfterSalesCaseResponse } from '@/services/worker'
|
||||
import type { UploadedFile, WorkerAfterSalesCase } from '@/types/worker-platform'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
|
||||
type AfterSalesResponseFormValues = {
|
||||
@@ -41,6 +41,13 @@ const STATUS_FILTERS: Array<{ value: StatusFilter; label: string }> = [
|
||||
{ value: 'closed', label: '已关闭' },
|
||||
]
|
||||
|
||||
/** 待办优先:待说明 > 处理中 > 追缴中 > 已关闭,同状态按发起时间倒序。 */
|
||||
const STATUS_WEIGHT: Record<string, number> = {
|
||||
awaiting_worker_response: 0,
|
||||
under_review: 1,
|
||||
recovery_pending: 2,
|
||||
}
|
||||
|
||||
export function formatMoney(value: number | undefined) {
|
||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
@@ -59,11 +66,40 @@ export function resolveWorkerAfterSalesStatusColor(status: string) {
|
||||
return 'green'
|
||||
}
|
||||
|
||||
/** 与后台售后面板保持相同的问题类型口径。 */
|
||||
export function formatWorkerAfterSalesProblemType(value: string) {
|
||||
if (value === 'fake_evidence') return '疑似P图 / 伪造验收'
|
||||
if (value === 'result_not_met') return '实际结果未达标'
|
||||
return '其他售后问题'
|
||||
}
|
||||
|
||||
function resolveWorkerAfterSalesProblemTypeColor(value: string) {
|
||||
if (value === 'fake_evidence') return 'red'
|
||||
if (value === 'result_not_met') return 'volcano'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
export function formatWorkerAfterSalesResolution(value: string) {
|
||||
if (value === 'upheld') return '问题成立'
|
||||
if (value === 'remedied') return '补救完成'
|
||||
if (value === 'not_upheld') return '问题不成立'
|
||||
return ''
|
||||
}
|
||||
|
||||
function resolveWorkerAfterSalesResolutionColor(value: string) {
|
||||
if (value === 'upheld') return 'red'
|
||||
if (value === 'remedied') return 'gold'
|
||||
if (value === 'not_upheld') return 'green'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function isAfterSalesClosed(status: string) {
|
||||
return !['awaiting_worker_response', 'under_review', 'recovery_pending'].includes(status)
|
||||
}
|
||||
|
||||
function matchesFilter(item: WorkerAfterSalesCase, filter: StatusFilter) {
|
||||
if (filter === 'all') return true
|
||||
if (filter === 'closed') {
|
||||
return !['awaiting_worker_response', 'under_review', 'recovery_pending'].includes(item.status)
|
||||
}
|
||||
if (filter === 'closed') return isAfterSalesClosed(item.status)
|
||||
return item.status === filter
|
||||
}
|
||||
|
||||
@@ -77,11 +113,26 @@ export default function WorkerAfterSalesPage() {
|
||||
retry: false,
|
||||
})
|
||||
const allCases = afterSalesCasesQuery.data?.data.items || []
|
||||
const pendingCount = allCases.filter((item) => item.status === 'awaiting_worker_response').length
|
||||
const countByFilter = useMemo(() => {
|
||||
const counts: Record<string, number> = { all: allCases.length }
|
||||
for (const filter of STATUS_FILTERS) {
|
||||
if (filter.value === 'all') continue
|
||||
counts[filter.value] = allCases.filter((item) => matchesFilter(item, filter.value)).length
|
||||
}
|
||||
return counts
|
||||
}, [allCases])
|
||||
const afterSalesCases = useMemo(
|
||||
() => allCases.filter((item) => matchesFilter(item, statusFilter)),
|
||||
() =>
|
||||
allCases
|
||||
.filter((item) => matchesFilter(item, statusFilter))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(STATUS_WEIGHT[a.status] ?? 3) - (STATUS_WEIGHT[b.status] ?? 3) ||
|
||||
String(b.createdAt).localeCompare(String(a.createdAt)),
|
||||
),
|
||||
[allCases, statusFilter],
|
||||
)
|
||||
const pendingCount = allCases.filter((item) => item.status === 'awaiting_worker_response').length
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -89,46 +140,63 @@ export default function WorkerAfterSalesPage() {
|
||||
isMobile ? 'worker-after-sales-page worker-after-sales-mobile' : 'worker-after-sales-page'
|
||||
}
|
||||
>
|
||||
<Card
|
||||
className="worker-after-sales-card"
|
||||
title={
|
||||
<Space size={8}>
|
||||
售后问题单
|
||||
{pendingCount > 0 ? <Tag color="orange">{pendingCount} 条待说明</Tag> : null}
|
||||
{pendingCount > 0 ? (
|
||||
<Alert
|
||||
className="worker-after-sales-reminder"
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`你有 ${pendingCount} 条售后单待提交说明,请尽快处理,逾期可能影响结算与等级`}
|
||||
/>
|
||||
) : null}
|
||||
<div className="worker-after-sales-toolbar">
|
||||
<Segmented
|
||||
options={STATUS_FILTERS.map((filter) => ({
|
||||
value: filter.value,
|
||||
label: (
|
||||
<Space size={4}>
|
||||
{filter.label}
|
||||
{countByFilter[filter.value] > 0 ? (
|
||||
<span className="worker-after-sales-filter-count">
|
||||
{countByFilter[filter.value]}
|
||||
</span>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
),
|
||||
}))}
|
||||
value={statusFilter}
|
||||
onChange={(value) => setStatusFilter(value as StatusFilter)}
|
||||
block={isMobile}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={afterSalesCasesQuery.isFetching}
|
||||
onClick={() => afterSalesCasesQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Segmented
|
||||
options={STATUS_FILTERS}
|
||||
value={statusFilter}
|
||||
onChange={(value) => setStatusFilter(value as StatusFilter)}
|
||||
block={isMobile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{afterSalesCasesQuery.isLoading ? (
|
||||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||||
) : afterSalesCasesQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message="售后问题单加载失败"
|
||||
description="请刷新后重试;若问题持续存在,请联系客服。"
|
||||
/>
|
||||
) : afterSalesCases.length === 0 ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无售后问题单,请保持验收资料真实完整"
|
||||
/>
|
||||
) : (
|
||||
afterSalesCases.map((item) => (
|
||||
<div className="worker-after-sales-list">
|
||||
{afterSalesCases.map((item) => (
|
||||
<WorkerAfterSalesItem key={item.caseId} item={item} isMobile={isMobile} />
|
||||
))
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -147,9 +215,14 @@ function WorkerAfterSalesItem({
|
||||
const [form] = Form.useForm<AfterSalesResponseFormValues>()
|
||||
|
||||
async function submit(values: AfterSalesResponseFormValues) {
|
||||
const response = String(values.response || '').trim()
|
||||
if (!response && files.length === 0) {
|
||||
message.error('请填写说明或至少上传一张补救材料图片')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await submitWorkerAfterSalesCaseResponse(item.caseId, {
|
||||
response: String(values.response || '').trim(),
|
||||
response,
|
||||
files,
|
||||
})
|
||||
message.success('售后说明已提交,等待客服处理')
|
||||
@@ -162,31 +235,135 @@ function WorkerAfterSalesItem({
|
||||
}
|
||||
}
|
||||
|
||||
const resolutionLabel = formatWorkerAfterSalesResolution(item.resolutionAction)
|
||||
const hasRecoveryDetail =
|
||||
Number(item.recoveryAmount || 0) > 0 ||
|
||||
Number(item.pendingDepositDeductionAmount || 0) > 0 ||
|
||||
Number(item.availableDeductionAmount || 0) > 0 ||
|
||||
Number(item.debtAmount || 0) > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="worker-order-detail-value">
|
||||
<Space wrap size={[8, 6]}>
|
||||
<article
|
||||
className={`worker-after-sales-item${item.status === 'awaiting_worker_response' ? ' is-awaiting' : ''}`}
|
||||
>
|
||||
<div className="worker-after-sales-item-head">
|
||||
<Space size={6} wrap>
|
||||
<Tag color={resolveWorkerAfterSalesStatusColor(item.status)}>
|
||||
{formatWorkerAfterSalesStatus(item.status)}
|
||||
</Tag>
|
||||
<Typography.Text strong>
|
||||
{item.productName || item.workOrderNo || item.caseNo}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" copyable={{ text: item.caseNo }}>
|
||||
{item.caseNo}
|
||||
</Typography.Text>
|
||||
<Tag color={resolveWorkerAfterSalesProblemTypeColor(item.problemType)}>
|
||||
{formatWorkerAfterSalesProblemType(item.problemType)}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Typography.Paragraph style={{ margin: '8px 0 4px' }}>
|
||||
<Typography.Text strong ellipsis={{ tooltip: item.productName || item.caseNo }}>
|
||||
{item.productName || item.caseNo}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
发起 {formatDateTime(item.createdAt)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space size={12} wrap className="worker-after-sales-item-meta">
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }} copyable>
|
||||
{item.platformOrderId || item.workOrderNo || item.caseNo}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12 }}
|
||||
copyable={{ text: item.caseNo }}
|
||||
>
|
||||
售后单号 {item.caseNo}
|
||||
</Typography.Text>
|
||||
{item.shareQuantity ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
拼单 {item.shareQuantity} 份
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
|
||||
<div className="worker-after-sales-block">
|
||||
<div className="worker-after-sales-block-label">投诉内容</div>
|
||||
<Typography.Paragraph style={{ marginBottom: item.complaintFiles.length ? 8 : 0 }}>
|
||||
{item.complaintNote || '-'}
|
||||
</Typography.Paragraph>
|
||||
{item.complaintFiles.length > 0 ? (
|
||||
<ImagePreviewList files={item.complaintFiles} size={52} maxVisible={4} />
|
||||
<ImagePreviewList files={item.complaintFiles} size={48} maxVisible={5} />
|
||||
) : null}
|
||||
<div style={{ marginTop: 8 }}>
|
||||
</div>
|
||||
|
||||
{item.workerResponse || item.workerResponseFiles.length > 0 ? (
|
||||
<div className="worker-after-sales-block is-mine">
|
||||
<div className="worker-after-sales-block-label">
|
||||
我提交的说明{item.respondedAt ? ` · ${formatDateTime(item.respondedAt)}` : ''}
|
||||
</div>
|
||||
{item.workerResponse ? (
|
||||
<Typography.Paragraph
|
||||
style={{ marginBottom: item.workerResponseFiles.length ? 8 : 0 }}
|
||||
>
|
||||
{item.workerResponse}
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
{item.workerResponseFiles.length > 0 ? (
|
||||
<ImagePreviewList files={item.workerResponseFiles} size={48} maxVisible={5} />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{resolutionLabel || item.resolutionNote || hasRecoveryDetail ? (
|
||||
<div className="worker-after-sales-block is-result">
|
||||
<div className="worker-after-sales-block-label">处理结果</div>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
{resolutionLabel ? (
|
||||
<Space size={8} wrap>
|
||||
<Tag color={resolveWorkerAfterSalesResolutionColor(item.resolutionAction)}>
|
||||
{resolutionLabel}
|
||||
</Tag>
|
||||
{item.resolvedAt ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{formatDateTime(item.resolvedAt)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
) : null}
|
||||
{item.resolutionNote ? (
|
||||
<Typography.Paragraph style={{ marginBottom: 0 }}>
|
||||
{item.resolutionNote}
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
{hasRecoveryDetail ? (
|
||||
<Space size={12} wrap>
|
||||
{Number(item.recoveryAmount || 0) > 0 ? (
|
||||
<Typography.Text type="danger" strong>
|
||||
追缴 {formatMoney(item.recoveryAmount)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{Number(item.pendingDepositDeductionAmount || 0) > 0 ? (
|
||||
<Typography.Text type="secondary">
|
||||
押金抵扣 {formatMoney(item.pendingDepositDeductionAmount)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{Number(item.availableDeductionAmount || 0) > 0 ? (
|
||||
<Typography.Text type="secondary">
|
||||
余额抵扣 {formatMoney(item.availableDeductionAmount)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{Number(item.debtAmount || 0) > 0 ? (
|
||||
<Typography.Text type="danger" strong>
|
||||
待追缴 {formatMoney(item.debtAmount)},后续验收报酬将自动抵扣
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
) : item.status === 'under_review' ? (
|
||||
<Typography.Text type="secondary">已提交说明,等待客服处理</Typography.Text>
|
||||
) : null}
|
||||
|
||||
{item.status === 'awaiting_worker_response' ? (
|
||||
<div className="worker-after-sales-item-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setFiles([])
|
||||
form.resetFields()
|
||||
@@ -195,20 +372,12 @@ function WorkerAfterSalesItem({
|
||||
>
|
||||
提交说明
|
||||
</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>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<Modal
|
||||
title={`售后说明 · ${item.caseNo}`}
|
||||
title={`售后说明 · ${item.productName || item.caseNo}`}
|
||||
open={responseOpen}
|
||||
width={isMobile ? '92%' : 560}
|
||||
onCancel={() => setResponseOpen(false)}
|
||||
@@ -220,12 +389,39 @@ function WorkerAfterSalesItem({
|
||||
type="warning"
|
||||
showIcon
|
||||
message="请针对售后问题说明情况或提交补救材料,原验收资料不可在此修改。"
|
||||
style={{ marginBottom: 16 }}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<div className="worker-after-sales-block">
|
||||
<div className="worker-after-sales-block-label">
|
||||
投诉内容 · {formatWorkerAfterSalesProblemType(item.problemType)}
|
||||
</div>
|
||||
<Typography.Paragraph style={{ marginBottom: item.complaintFiles.length ? 8 : 0 }}>
|
||||
{item.complaintNote || '-'}
|
||||
</Typography.Paragraph>
|
||||
{item.complaintFiles.length > 0 ? (
|
||||
<ImagePreviewList files={item.complaintFiles} size={44} maxVisible={4} />
|
||||
) : null}
|
||||
</div>
|
||||
<Form form={form} layout="vertical" onFinish={submit} style={{ marginTop: 12 }}>
|
||||
<Form.Item
|
||||
label="说明"
|
||||
name="response"
|
||||
extra="说明与补救材料至少填写一项"
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) =>
|
||||
String(value || '').trim() || files.length > 0
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请填写说明或至少上传一张图片')),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
maxLength={2000}
|
||||
showCount
|
||||
placeholder="请说明当时的处理情况"
|
||||
/>
|
||||
<Typography.Paragraph type="secondary">{item.complaintNote}</Typography.Paragraph>
|
||||
<Form form={form} layout="vertical" onFinish={submit}>
|
||||
<Form.Item label="说明" name="response">
|
||||
<Input.TextArea rows={4} maxLength={2000} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item label="补救材料图片">
|
||||
<ImageUpload
|
||||
|
||||
@@ -2199,3 +2199,103 @@ body {
|
||||
.finance-summary-item strong {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
/* ===== 售后问题单(打手端) ===== */
|
||||
|
||||
.worker-after-sales-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.worker-after-sales-reminder {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.worker-after-sales-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.worker-after-sales-filter-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: #e6f4ff;
|
||||
color: #1677ff;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.worker-after-sales-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.worker-after-sales-item {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-left: 3px solid #cbd5e1;
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.worker-after-sales-item.is-awaiting {
|
||||
border-left-color: #fa8c16;
|
||||
background: #fffdf8;
|
||||
}
|
||||
|
||||
.worker-after-sales-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.worker-after-sales-item-meta {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.worker-after-sales-block {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.worker-after-sales-block.is-mine {
|
||||
background: #f6ffed;
|
||||
border-color: #e6f4dc;
|
||||
}
|
||||
|
||||
.worker-after-sales-block.is-result {
|
||||
background: #fff7e6;
|
||||
border-color: #ffe7ba;
|
||||
}
|
||||
|
||||
.worker-after-sales-block-label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.worker-after-sales-item.is-awaiting .worker-after-sales-item-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.worker-after-sales-mobile .worker-after-sales-toolbar .ant-segmented {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user