feat(worker): 彻底重构售后问题单与工单仲裁中心视觉与交互体验

This commit is contained in:
yml2213
2026-09-12 19:16:12 +08:00
parent 402c10e9b6
commit fb08f4552e
2 changed files with 1178 additions and 311 deletions
@@ -1,4 +1,16 @@
import { ReloadOutlined } from '@ant-design/icons'
import {
AlertOutlined,
AuditOutlined,
CheckCircleFilled,
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
DollarCircleOutlined,
FormOutlined,
ReloadOutlined,
SafetyCertificateOutlined,
UserOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
App,
@@ -122,6 +134,7 @@ export default function WorkerAfterSalesPage() {
}
return counts
}, [allCases])
const afterSalesCases = useMemo(
() =>
allCases
@@ -140,27 +153,109 @@ export default function WorkerAfterSalesPage() {
isMobile ? 'worker-after-sales-page worker-after-sales-mobile' : 'worker-after-sales-page'
}
>
{/* 待办紧急提醒横幅 */}
{pendingCount > 0 ? (
<Alert
className="worker-after-sales-reminder"
type="warning"
showIcon
message={`你有 ${pendingCount} 条售后单待提交说明,请尽快处理,逾期可能影响结算与等级`}
/>
<div className="worker-as-pending-alert">
<div className="worker-as-alert-left">
<span className="worker-as-alert-icon-wrap">
<AlertOutlined />
</span>
<div className="worker-as-alert-text">
<div className="worker-as-alert-title">
您有 <strong>{pendingCount}</strong> 笔售后单待提交说明
</div>
<div className="worker-as-alert-desc">
买家已发起售后投诉,请尽快提交完成凭据或情况答辩,逾期可能影响您的资金与接单等级。
</div>
</div>
</div>
<Button
type="primary"
className="worker-as-alert-btn"
onClick={() => setStatusFilter('awaiting_worker_response')}
>
立即处理待办 ({pendingCount})
</Button>
</div>
) : null}
{/* 4卡片争议处理概览网格 */}
<div className="worker-as-kpi-grid">
<div
className={`worker-as-kpi-card is-awaiting ${statusFilter === 'awaiting_worker_response' ? 'is-active' : ''}`}
onClick={() =>
setStatusFilter(
statusFilter === 'awaiting_worker_response' ? 'all' : 'awaiting_worker_response',
)
}
>
<div className="worker-as-kpi-icon-wrap amber">
<AlertOutlined />
</div>
<div className="worker-as-kpi-info">
<div className="worker-as-kpi-label">
待我举证
{pendingCount > 0 && <span className="worker-as-kpi-pulse-dot" />}
</div>
<div className="worker-as-kpi-value">{countByFilter.awaiting_worker_response || 0}</div>
<div className="worker-as-kpi-sub">
{pendingCount > 0 ? `${pendingCount} 笔需紧急说明` : '暂无待处理'}
</div>
</div>
</div>
<div
className={`worker-as-kpi-card is-review ${statusFilter === 'under_review' ? 'is-active' : ''}`}
onClick={() => setStatusFilter(statusFilter === 'under_review' ? 'all' : 'under_review')}
>
<div className="worker-as-kpi-icon-wrap blue">
<AuditOutlined />
</div>
<div className="worker-as-kpi-info">
<div className="worker-as-kpi-label">平台仲裁中</div>
<div className="worker-as-kpi-value">{countByFilter.under_review || 0}</div>
<div className="worker-as-kpi-sub">客服公正审核中</div>
</div>
</div>
<div
className={`worker-as-kpi-card is-recovery ${statusFilter === 'recovery_pending' ? 'is-active' : ''}`}
onClick={() =>
setStatusFilter(statusFilter === 'recovery_pending' ? 'all' : 'recovery_pending')
}
>
<div className="worker-as-kpi-icon-wrap rose">
<DollarCircleOutlined />
</div>
<div className="worker-as-kpi-info">
<div className="worker-as-kpi-label">待追缴/扣款</div>
<div className="worker-as-kpi-value">{countByFilter.recovery_pending || 0}</div>
<div className="worker-as-kpi-sub">存在待追缴或抵扣款</div>
</div>
</div>
<div
className={`worker-as-kpi-card is-closed ${statusFilter === 'closed' ? 'is-active' : ''}`}
onClick={() => setStatusFilter(statusFilter === 'closed' ? 'all' : 'closed')}
>
<div className="worker-as-kpi-icon-wrap emerald">
<CheckCircleFilled />
</div>
<div className="worker-as-kpi-info">
<div className="worker-as-kpi-label">已结案归档</div>
<div className="worker-as-kpi-value">{countByFilter.closed || 0}</div>
<div className="worker-as-kpi-sub">历史完结记录</div>
</div>
</div>
</div>
{/* 桌面端与移动端顶部筛选与控制栏 */}
<div className="worker-after-sales-desktop-header">
<div className="worker-after-sales-header-left">
<Typography.Title level={3} className="worker-after-sales-heading">
售后问题单
售后维权与工单仲裁
</Typography.Title>
{pendingCount > 0 ? (
<span className="worker-after-sales-alert-pill">
<span className="worker-after-sales-pulse-dot" />
{pendingCount} 条待提交说明
</span>
) : (
<span className="worker-after-sales-normal-pill">无待办售后单</span>
)}
<span className="worker-as-total-badge">共 {allCases.length} 笔工单记录</span>
</div>
<div className="worker-after-sales-header-center">
@@ -197,7 +292,9 @@ export default function WorkerAfterSalesPage() {
</div>
{afterSalesCasesQuery.isLoading ? (
<Typography.Text type="secondary">加载中…</Typography.Text>
<div className="worker-as-loading-state">
<Typography.Text type="secondary">正在加载售后问题单…</Typography.Text>
</div>
) : afterSalesCasesQuery.isError ? (
<Alert
type="error"
@@ -206,10 +303,12 @@ export default function WorkerAfterSalesPage() {
description="请刷新后重试;若问题持续存在,请联系客服。"
/>
) : afterSalesCases.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无售后问题单,请保持验收资料真实完整"
/>
<div className="worker-as-empty-card">
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无匹配的售后问题单,请保持验收资料真实完整"
/>
</div>
) : (
<div className="worker-after-sales-list">
{afterSalesCases.map((item) => (
@@ -260,180 +359,294 @@ function WorkerAfterSalesItem({
Number(item.recoveryAmount || 0) > 0 ||
Number(item.pendingDepositDeductionAmount || 0) > 0 ||
Number(item.availableDeductionAmount || 0) > 0 ||
Number(item.debtAmount || 0) > 0
Number(item.debtAmount || 0) > 0 ||
Number(item.refundedAmount || 0) > 0
return (
<>
<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)}>
<article className={`worker-as-case-card is-status-${item.status}`}>
{/* 卡片头部:状态、问题类型、工单标题、订单单号 */}
<div className="worker-as-card-header">
<div className="worker-as-card-header-left">
<span className={`worker-as-status-badge is-${item.status}`}>
<span className="worker-as-status-dot" />
{formatWorkerAfterSalesStatus(item.status)}
</Tag>
<Tag color={resolveWorkerAfterSalesProblemTypeColor(item.problemType)}>
</span>
<Tag
color={resolveWorkerAfterSalesProblemTypeColor(item.problemType)}
className="worker-as-problem-tag"
>
{formatWorkerAfterSalesProblemType(item.problemType)}
</Tag>
</Space>
<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={{ text: item.workOrderNo || item.caseNo }}
>
工单号 {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
strong
className="worker-as-product-title"
ellipsis={{ tooltip: item.productName || item.caseNo }}
>
{item.productName || item.caseNo}
</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={48} maxVisible={5} />
) : null}
</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} />
{item.shareQuantity ? (
<Tag className="worker-as-share-tag">拼单 {item.shareQuantity} 份</Tag>
) : 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}
{Number(item.refundedAmount || 0) > 0 ? (
<Typography.Text type="success" strong>
已退还追缴 {formatMoney(item.refundedAmount)} 至可用余额
</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"
onClick={() => {
setFiles([])
form.resetFields()
setResponseOpen(true)
<div className="worker-as-card-header-right">
<span className="worker-as-meta-time">
<ClockCircleOutlined style={{ marginRight: 4 }} />
发起 {formatDateTime(item.createdAt)}
</span>
<Typography.Text
className="worker-as-meta-code"
type="secondary"
copyable={{
text: item.workOrderNo || item.caseNo,
tooltips: ['复制工单号', '已复制'],
}}
>
提交说明
</Button>
工单 {item.workOrderNo || item.caseNo}
</Typography.Text>
<Typography.Text
className="worker-as-meta-code"
type="secondary"
copyable={{ text: item.caseNo, tooltips: ['复制售后单号', '已复制'] }}
>
售后单 {item.caseNo}
</Typography.Text>
</div>
</div>
{/* 主体对比区域:买家投诉诉求 vs 打手答辩举证 (并排双列) */}
<div className="worker-as-card-body">
{/* 左列:买家投诉诉求 */}
<div className="worker-as-col worker-as-buyer-col">
<div className="worker-as-col-header">
<div className="worker-as-avatar-pill buyer">
<UserOutlined />
</div>
<span className="worker-as-col-title">买家投诉诉求</span>
</div>
<div className="worker-as-box buyer-box">
<div className="worker-as-box-text">
{item.complaintNote ? (
<p className="worker-as-complaint-text">{item.complaintNote}</p>
) : (
<p className="worker-as-empty-text">买家未填写文字描述</p>
)}
</div>
{item.complaintFiles.length > 0 && (
<div className="worker-as-box-files">
<div className="worker-as-files-label">
买家举证材料 ({item.complaintFiles.length}):
</div>
<ImagePreviewList files={item.complaintFiles} size={54} maxVisible={6} />
</div>
)}
</div>
</div>
{/* 右列:打手答辩与举证 */}
<div className="worker-as-col worker-as-worker-col">
<div className="worker-as-col-header">
<div className="worker-as-avatar-pill worker">
<SafetyCertificateOutlined />
</div>
<span className="worker-as-col-title">我的答辩与说明</span>
{item.respondedAt && (
<span className="worker-as-col-subtitle">
提交于 {formatDateTime(item.respondedAt)}
</span>
)}
</div>
{item.workerResponse || item.workerResponseFiles.length > 0 ? (
<div className="worker-as-box worker-box">
<div className="worker-as-box-text">
{item.workerResponse ? (
<p className="worker-as-worker-text">{item.workerResponse}</p>
) : null}
</div>
{item.workerResponseFiles.length > 0 && (
<div className="worker-as-box-files">
<div className="worker-as-files-label">
补救举证材料 ({item.workerResponseFiles.length}):
</div>
<ImagePreviewList files={item.workerResponseFiles} size={54} maxVisible={6} />
</div>
)}
</div>
) : item.status === 'awaiting_worker_response' ? (
<div className="worker-as-box awaiting-cta-box">
<div className="worker-as-cta-content">
<div className="worker-as-cta-icon-wrap">
<AlertOutlined className="worker-as-cta-icon" />
</div>
<div className="worker-as-cta-info">
<h4 className="worker-as-cta-title">等待您提交说明材料</h4>
<p className="worker-as-cta-desc">
买家已发起售后申诉,请针对上述问题提交文字说明或补救凭据,以便平台客服公允判定。
</p>
</div>
</div>
<Button
type="primary"
className="worker-as-submit-btn"
icon={<FormOutlined />}
onClick={() => {
setFiles([])
form.resetFields()
setResponseOpen(true)
}}
>
立即提交说明与凭据
</Button>
</div>
) : item.status === 'under_review' ? (
<div className="worker-as-box neutral-state-box">
<ClockCircleOutlined className="worker-as-neutral-icon" />
<p className="worker-as-neutral-text">
未补充文字材料,平台客服正在结合买家举证与订单原始验收凭据进行裁决审核。
</p>
</div>
) : (
<div className="worker-as-box neutral-state-box is-closed">
<CheckCircleOutlined className="worker-as-neutral-icon" />
<p className="worker-as-neutral-text">本工单已结案(未补充答辩记录)</p>
</div>
)}
</div>
</div>
{/* 底部横幅:平台仲裁裁决与资金明细 */}
{resolutionLabel ||
item.resolutionNote ||
hasRecoveryDetail ||
item.status === 'under_review' ? (
<div
className={`worker-as-resolution-banner ${item.status === 'under_review' && !resolutionLabel ? 'is-pending' : ''}`}
>
<div className="worker-as-resolution-head">
<div className="worker-as-resolution-head-left">
<span className="worker-as-gavel-icon">
<AuditOutlined />
</span>
<span className="worker-as-resolution-title">平台仲裁判定</span>
{resolutionLabel ? (
<Tag
color={resolveWorkerAfterSalesResolutionColor(item.resolutionAction)}
className="worker-as-resolution-tag"
>
{item.resolutionAction === 'upheld' && (
<CloseCircleOutlined style={{ marginRight: 4 }} />
)}
{item.resolutionAction === 'not_upheld' && (
<CheckCircleOutlined style={{ marginRight: 4 }} />
)}
{resolutionLabel}
</Tag>
) : item.status === 'under_review' ? (
<Tag color="blue" className="worker-as-resolution-tag">
客服审核中
</Tag>
) : null}
{item.resolvedAt ? (
<span className="worker-as-resolution-time">
裁决时间:{formatDateTime(item.resolvedAt)}
</span>
) : null}
</div>
</div>
{item.resolutionNote && (
<div className="worker-as-resolution-note">
<span className="note-label">判定说明:</span>
<span className="note-content">{item.resolutionNote}</span>
</div>
)}
{hasRecoveryDetail && (
<div className="worker-as-financial-breakdown">
<span className="financial-label">资金处理明细:</span>
<div className="financial-chips-wrap">
{Number(item.recoveryAmount || 0) > 0 && (
<span className="financial-chip is-recovery">
<span className="chip-name">追缴金额</span>
<span className="chip-amount">{formatMoney(item.recoveryAmount)}</span>
</span>
)}
{Number(item.pendingDepositDeductionAmount || 0) > 0 && (
<span className="financial-chip is-deposit">
<span className="chip-name">待解冻资金抵扣</span>
<span className="chip-amount">
{formatMoney(item.pendingDepositDeductionAmount)}
</span>
</span>
)}
{Number(item.availableDeductionAmount || 0) > 0 && (
<span className="financial-chip is-available">
<span className="chip-name">余额抵扣</span>
<span className="chip-amount">
{formatMoney(item.availableDeductionAmount)}
</span>
</span>
)}
{Number(item.debtAmount || 0) > 0 && (
<span className="financial-chip is-debt">
<span className="chip-name">待追缴欠款</span>
<span className="chip-amount">{formatMoney(item.debtAmount)}</span>
<span className="chip-hint">(后续报酬自动抵扣)</span>
</span>
)}
{Number(item.refundedAmount || 0) > 0 && (
<span className="financial-chip is-refunded">
<span className="chip-name">退还追缴</span>
<span className="chip-amount">{formatMoney(item.refundedAmount)}</span>
<span className="chip-hint">(已入可用余额)</span>
</span>
)}
</div>
</div>
)}
</div>
) : null}
</article>
{/* 提交说明材料弹窗 */}
<Modal
title={`售后说明 · ${item.productName || item.caseNo}`}
title={`提交售后说明 · ${item.productName || item.caseNo}`}
open={responseOpen}
width={isMobile ? '92%' : 560}
width={isMobile ? '94%' : 580}
onCancel={() => setResponseOpen(false)}
onOk={() => form.submit()}
okText="提交说明"
okText="确认提交说明"
destroyOnHidden
className="worker-as-submit-modal"
>
<Alert
type="warning"
showIcon
message="请针对售后问题说明情况或提交补救材料,原验收资料不可在此修改。"
style={{ marginBottom: 12 }}
message="请针对售后问题如实说明情况或提交补救凭证。原订单验收资料不可在此修改。"
style={{ marginBottom: 14 }}
/>
<div className="worker-after-sales-block">
<div className="worker-after-sales-block-label">
投诉内容 · {formatWorkerAfterSalesProblemType(item.problemType)}
{/* 弹窗内展示买家投诉内容供参考 */}
<div className="worker-as-modal-buyer-summary">
<div className="modal-summary-title">
<UserOutlined style={{ marginRight: 6, color: '#ef4444' }} />
买家投诉内容 · {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 className="modal-summary-content">
{item.complaintNote || '(买家未填写文字描述)'}
</div>
{item.complaintFiles.length > 0 && (
<div className="modal-summary-files">
<ImagePreviewList files={item.complaintFiles} size={46} maxVisible={4} />
</div>
)}
</div>
<Form form={form} layout="vertical" onFinish={submit} style={{ marginTop: 12 }}>
<Form form={form} layout="vertical" onFinish={submit} style={{ marginTop: 14 }}>
<Form.Item
label="说明"
label="我的说明与答辩"
name="response"
extra="说明与补救材料至少填写一项"
rules={[
@@ -441,7 +654,7 @@ function WorkerAfterSalesItem({
validator: (_, value) =>
String(value || '').trim() || files.length > 0
? Promise.resolve()
: Promise.reject(new Error('请填写说明或至少上传一张图片')),
: Promise.reject(new Error('请填写说明或至少上传一张补救材料图片')),
},
]}
>
@@ -449,10 +662,10 @@ function WorkerAfterSalesItem({
rows={4}
maxLength={2000}
showCount
placeholder="请说明当时的处理情况"
placeholder="请详细客观说明当时订单处理情况、与买家沟通情况或补救事实…"
/>
</Form.Item>
<Form.Item label="补救材料图片">
<Form.Item label="补救举证材料图片 (支持粘贴直接上传)">
<ImageUpload
scene="worker-acceptance"
scope="worker"