优化售后问题单后台位置
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
App,
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Segmented,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
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 { useIsMobile } from '@/utils/use-is-mobile'
|
||||
|
||||
type AfterSalesResponseFormValues = {
|
||||
response?: string
|
||||
}
|
||||
|
||||
type StatusFilter =
|
||||
| 'all'
|
||||
| 'awaiting_worker_response'
|
||||
| 'under_review'
|
||||
| 'recovery_pending'
|
||||
| 'closed'
|
||||
|
||||
const STATUS_FILTERS: Array<{ value: StatusFilter; label: string }> = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'awaiting_worker_response', label: '待说明' },
|
||||
{ value: 'under_review', label: '处理中' },
|
||||
{ value: 'recovery_pending', label: '追缴中' },
|
||||
{ value: 'closed', label: '已关闭' },
|
||||
]
|
||||
|
||||
export function formatMoney(value: number | undefined) {
|
||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
export function formatWorkerAfterSalesStatus(status: string) {
|
||||
if (status === 'awaiting_worker_response') return '待说明'
|
||||
if (status === 'under_review') return '处理中'
|
||||
if (status === 'recovery_pending') return '追缴中'
|
||||
return '已关闭'
|
||||
}
|
||||
|
||||
export 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 matchesFilter(item: WorkerAfterSalesCase, filter: StatusFilter) {
|
||||
if (filter === 'all') return true
|
||||
if (filter === 'closed') {
|
||||
return !['awaiting_worker_response', 'under_review', 'recovery_pending'].includes(item.status)
|
||||
}
|
||||
return item.status === filter
|
||||
}
|
||||
|
||||
export default function WorkerAfterSalesPage() {
|
||||
const isMobile = useIsMobile()
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
|
||||
|
||||
const afterSalesCasesQuery = useQuery({
|
||||
queryKey: ['worker-after-sales-cases'],
|
||||
queryFn: fetchWorkerAfterSalesCases,
|
||||
retry: false,
|
||||
})
|
||||
const allCases = afterSalesCasesQuery.data?.data.items || []
|
||||
const afterSalesCases = useMemo(
|
||||
() => allCases.filter((item) => matchesFilter(item, statusFilter)),
|
||||
[allCases, statusFilter],
|
||||
)
|
||||
const pendingCount = allCases.filter((item) => item.status === 'awaiting_worker_response').length
|
||||
|
||||
return (
|
||||
<section
|
||||
className={
|
||||
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}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<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}
|
||||
/>
|
||||
{afterSalesCasesQuery.isLoading ? (
|
||||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||||
) : afterSalesCases.length === 0 ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无售后问题单,请保持验收资料真实完整"
|
||||
/>
|
||||
) : (
|
||||
afterSalesCases.map((item) => (
|
||||
<WorkerAfterSalesItem key={item.caseId} item={item} isMobile={isMobile} />
|
||||
))
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkerAfterSalesItem({
|
||||
item,
|
||||
isMobile,
|
||||
}: {
|
||||
item: WorkerAfterSalesCase
|
||||
isMobile: boolean
|
||||
}) {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [responseOpen, setResponseOpen] = useState(false)
|
||||
const [files, setFiles] = useState<UploadedFile[]>([])
|
||||
const [form] = Form.useForm<AfterSalesResponseFormValues>()
|
||||
|
||||
async function submit(values: AfterSalesResponseFormValues) {
|
||||
try {
|
||||
await submitWorkerAfterSalesCaseResponse(item.caseId, {
|
||||
response: String(values.response || '').trim(),
|
||||
files,
|
||||
})
|
||||
message.success('售后说明已提交,等待客服处理')
|
||||
await queryClient.invalidateQueries({ queryKey: ['worker-after-sales-cases'] })
|
||||
setResponseOpen(false)
|
||||
setFiles([])
|
||||
form.resetFields()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '提交失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div 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" copyable={{ text: item.caseNo }}>
|
||||
{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}
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{item.status === 'awaiting_worker_response' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setFiles([])
|
||||
form.resetFields()
|
||||
setResponseOpen(true)
|
||||
}}
|
||||
>
|
||||
提交说明
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={`售后说明 · ${item.caseNo}`}
|
||||
open={responseOpen}
|
||||
width={isMobile ? '92%' : 560}
|
||||
onCancel={() => setResponseOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
okText="提交说明"
|
||||
destroyOnHidden
|
||||
>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="请针对售后问题说明情况或提交补救材料,原验收资料不可在此修改。"
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<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
|
||||
scene="worker-acceptance"
|
||||
scope="worker"
|
||||
value={files}
|
||||
onChange={setFiles}
|
||||
maxCount={10}
|
||||
showPasteHint
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { App, Alert, Button, Card, Form, Input, Modal, Space, Tag, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
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'
|
||||
|
||||
type AfterSalesResponseFormValues = {
|
||||
response?: string
|
||||
}
|
||||
|
||||
function formatMoney(value: number | undefined) {
|
||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
export default function WorkerAfterSalesSection({ isMobile }: { isMobile: boolean }) {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [afterSalesCase, setAfterSalesCase] = useState<WorkerAfterSalesCase | null>(null)
|
||||
const [afterSalesResponseFiles, setAfterSalesResponseFiles] = useState<UploadedFile[]>([])
|
||||
const [afterSalesResponseForm] = Form.useForm<AfterSalesResponseFormValues>()
|
||||
|
||||
const afterSalesCasesQuery = useQuery({
|
||||
queryKey: ['worker-after-sales-cases'],
|
||||
queryFn: fetchWorkerAfterSalesCases,
|
||||
retry: false,
|
||||
})
|
||||
const afterSalesCases = afterSalesCasesQuery.data?.data.items || []
|
||||
|
||||
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('售后说明已提交,等待客服处理')
|
||||
await queryClient.invalidateQueries({ queryKey: ['worker-after-sales-cases'] })
|
||||
setAfterSalesCase(null)
|
||||
setAfterSalesResponseFiles([])
|
||||
afterSalesResponseForm.resetFields()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '提交失败')
|
||||
}
|
||||
}
|
||||
|
||||
if (afterSalesCases.length === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { App, Form } from 'antd'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { App, Badge, Button, Form, Space, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
fetchWorkerAfterSalesCases,
|
||||
fetchWorkerCancelRequests,
|
||||
fetchWorkerOrderFeedbacks,
|
||||
fetchWorkerMyOrderOverview,
|
||||
@@ -20,7 +22,6 @@ import {
|
||||
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
import { buildWorkerOrderQuery } from '@/utils/worker-order-query'
|
||||
import WorkerAfterSalesSection from './WorkerAfterSalesSection'
|
||||
import { WorkerOrderDesktopList } from './WorkerOrderDesktopList'
|
||||
import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer'
|
||||
import { WorkerOrderMobileList } from './WorkerOrderMobileList'
|
||||
@@ -144,6 +145,15 @@ export default function WorkerOrdersPage() {
|
||||
const isMobile = useIsMobile()
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const afterSalesQuery = useQuery({
|
||||
queryKey: ['worker-after-sales-cases'],
|
||||
queryFn: fetchWorkerAfterSalesCases,
|
||||
retry: false,
|
||||
})
|
||||
const afterSalesPendingCount = (afterSalesQuery.data?.data.items || []).filter(
|
||||
(item) => item.status === 'awaiting_worker_response',
|
||||
).length
|
||||
const [status, setStatus] = useState('in_progress')
|
||||
const [keywordInput, setKeywordInput] = useState('')
|
||||
const [keyword, setKeyword] = useState('')
|
||||
@@ -441,7 +451,38 @@ export default function WorkerOrdersPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkerAfterSalesSection isMobile={isMobile} />
|
||||
<Button
|
||||
type="text"
|
||||
block={false}
|
||||
className="worker-after-sales-entry"
|
||||
onClick={() => navigate('/orders/after-sales')}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
<Space
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 8,
|
||||
padding: '10px 14px',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<Space size={8}>
|
||||
<Typography.Text strong>售后问题单</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
验收后如被投诉,可在此查看与说明
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
{afterSalesPendingCount > 0 ? (
|
||||
<Badge count={afterSalesPendingCount} color="orange" showZero={false} />
|
||||
) : (
|
||||
<Typography.Text type="secondary">进入 ></Typography.Text>
|
||||
)}
|
||||
</Space>
|
||||
</Button>
|
||||
{isMobile ? (
|
||||
<WorkerOrderMobileList
|
||||
statusOptions={STATUS_OPTIONS}
|
||||
|
||||
Reference in New Issue
Block a user