优化售后问题单后台位置
This commit is contained in:
@@ -41,6 +41,7 @@ const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'))
|
|||||||
const WorkerHallPage = lazy(() => import('@/pages/worker/WorkerHallPage'))
|
const WorkerHallPage = lazy(() => import('@/pages/worker/WorkerHallPage'))
|
||||||
const WorkerLoginPage = lazy(() => import('@/pages/worker/WorkerLoginPage'))
|
const WorkerLoginPage = lazy(() => import('@/pages/worker/WorkerLoginPage'))
|
||||||
const WorkerOrdersPage = lazy(() => import('@/pages/worker/WorkerOrdersPage'))
|
const WorkerOrdersPage = lazy(() => import('@/pages/worker/WorkerOrdersPage'))
|
||||||
|
const WorkerAfterSalesPage = lazy(() => import('@/pages/worker/WorkerAfterSalesPage'))
|
||||||
const WorkerProfilePage = lazy(() => import('@/pages/worker/WorkerProfilePage'))
|
const WorkerProfilePage = lazy(() => import('@/pages/worker/WorkerProfilePage'))
|
||||||
|
|
||||||
function RequireAdmin() {
|
function RequireAdmin() {
|
||||||
@@ -139,6 +140,7 @@ function WorkerApplication() {
|
|||||||
<Route element={<WorkerLayout />}>
|
<Route element={<WorkerLayout />}>
|
||||||
<Route path="hall" element={<WorkerHallPage />} />
|
<Route path="hall" element={<WorkerHallPage />} />
|
||||||
<Route path="orders" element={<WorkerOrdersPage />} />
|
<Route path="orders" element={<WorkerOrdersPage />} />
|
||||||
|
<Route path="orders/after-sales" element={<WorkerAfterSalesPage />} />
|
||||||
<Route path="profile" element={<WorkerProfilePage />} />
|
<Route path="profile" element={<WorkerProfilePage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ContainerOutlined,
|
ContainerOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
|
SafetyCertificateOutlined,
|
||||||
ShoppingOutlined,
|
ShoppingOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
@@ -19,6 +20,7 @@ import WorkerAnnouncementBanner from '@/components/worker/WorkerAnnouncementBann
|
|||||||
const menuItems: MenuProps['items'] = [
|
const menuItems: MenuProps['items'] = [
|
||||||
{ key: '/hall', icon: <ShoppingOutlined />, label: '抢单大厅' },
|
{ key: '/hall', icon: <ShoppingOutlined />, label: '抢单大厅' },
|
||||||
{ key: '/orders', icon: <ContainerOutlined />, label: '我的订单' },
|
{ key: '/orders', icon: <ContainerOutlined />, label: '我的订单' },
|
||||||
|
{ key: '/orders/after-sales', icon: <SafetyCertificateOutlined />, label: '售后问题单' },
|
||||||
{ key: '/profile', icon: <UserOutlined />, label: '个人中心' },
|
{ key: '/profile', icon: <UserOutlined />, label: '个人中心' },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -97,7 +99,9 @@ export default function WorkerLayout() {
|
|||||||
<nav className="worker-mobile-tabbar" aria-label="移动端底部导航">
|
<nav className="worker-mobile-tabbar" aria-label="移动端底部导航">
|
||||||
{tabItems.map((item) => {
|
{tabItems.map((item) => {
|
||||||
const Icon = item.icon
|
const Icon = item.icon
|
||||||
const isActive = currentKey === item.key
|
const isActive =
|
||||||
|
currentKey === item.key ||
|
||||||
|
(currentKey === '/orders/after-sales' && item.key === '/orders')
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.key}
|
key={item.key}
|
||||||
@@ -354,6 +358,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveSelectedKey(pathname: string) {
|
function resolveSelectedKey(pathname: string) {
|
||||||
|
if (pathname.startsWith('/orders/after-sales')) return '/orders/after-sales'
|
||||||
if (pathname.startsWith('/orders')) return '/orders'
|
if (pathname.startsWith('/orders')) return '/orders'
|
||||||
if (pathname.startsWith('/profile')) return '/profile'
|
if (pathname.startsWith('/profile')) return '/profile'
|
||||||
return '/hall'
|
return '/hall'
|
||||||
|
|||||||
@@ -1,27 +1,35 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Card, Space, Tabs } from 'antd'
|
import { Card, Space, Tabs } from 'antd'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useSearchParams } from 'react-router'
|
import { useLocation, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import PageHeader from '@/components/admin/PageHeader'
|
import PageHeader from '@/components/admin/PageHeader'
|
||||||
|
import type { WorkOrder } from '@/types/worker-platform'
|
||||||
import { fetchAdminWorkerPlatformSummary } from '@/services/admin'
|
import { fetchAdminWorkerPlatformSummary } from '@/services/admin'
|
||||||
import { getAdminRole } from '@/utils/admin-auth'
|
import { getAdminRole } from '@/utils/admin-auth'
|
||||||
import CategoriesPanel from './panels/CategoriesPanel'
|
import AfterSalesPanel, { type AfterSalesPanelHandle } from './panels/AfterSalesPanel'
|
||||||
import AnnouncementPanel from './panels/AnnouncementPanel'
|
import AnnouncementPanel from './panels/AnnouncementPanel'
|
||||||
import CancelRequestsPanel from './panels/CancelRequestsPanel'
|
import CancelRequestsPanel from './panels/CancelRequestsPanel'
|
||||||
import FinancePanel from './panels/FinancePanel'
|
import CategoriesPanel from './panels/CategoriesPanel'
|
||||||
import FeedbacksPanel from './panels/FeedbacksPanel'
|
import FeedbacksPanel from './panels/FeedbacksPanel'
|
||||||
|
import FinancePanel from './panels/FinancePanel'
|
||||||
import HallConfigPanel from './panels/HallConfigPanel'
|
import HallConfigPanel from './panels/HallConfigPanel'
|
||||||
import LevelsPanel from './panels/LevelsPanel'
|
import LevelsPanel from './panels/LevelsPanel'
|
||||||
import NotificationsPanel from './panels/NotificationsPanel'
|
import NotificationsPanel from './panels/NotificationsPanel'
|
||||||
import ProductRulesPanel from './panels/ProductRulesPanel'
|
|
||||||
import ProductMatchPanel from './panels/ProductMatchPanel'
|
import ProductMatchPanel from './panels/ProductMatchPanel'
|
||||||
|
import ProductRulesPanel from './panels/ProductRulesPanel'
|
||||||
import WorkersPanel from './panels/WorkersPanel'
|
import WorkersPanel from './panels/WorkersPanel'
|
||||||
import WorkOrdersPanel from './panels/WorkOrdersPanel'
|
import WorkOrdersPanel from './panels/WorkOrdersPanel'
|
||||||
|
|
||||||
|
type AfterSalesLocationState = {
|
||||||
|
createOrder?: WorkOrder
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminWorkerPlatformPage() {
|
export default function AdminWorkerPlatformPage() {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
|
const location = useLocation()
|
||||||
const [activeTab, setActiveTab] = useState(() => loadActiveTab(searchParams.get('tab')))
|
const [activeTab, setActiveTab] = useState(() => loadActiveTab(searchParams.get('tab')))
|
||||||
|
const afterSalesPanelRef = useRef<AfterSalesPanelHandle>(null)
|
||||||
const role = getAdminRole()
|
const role = getAdminRole()
|
||||||
const isAdmin = role === 'admin'
|
const isAdmin = role === 'admin'
|
||||||
const isSupport = role === 'support'
|
const isSupport = role === 'support'
|
||||||
@@ -34,14 +42,33 @@ export default function AdminWorkerPlatformPage() {
|
|||||||
}
|
}
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const state = location.state as AfterSalesLocationState | null
|
||||||
|
if (state?.createOrder) {
|
||||||
|
setActiveTab('after-sales')
|
||||||
|
afterSalesPanelRef.current?.openCreateModal(state.createOrder)
|
||||||
|
// 清掉 state,避免刷新或后退时重复弹窗。
|
||||||
|
window.history.replaceState({}, '')
|
||||||
|
}
|
||||||
|
}, [location.state])
|
||||||
|
|
||||||
function changeTab(nextTab: string) {
|
function changeTab(nextTab: string) {
|
||||||
setActiveTab(nextTab)
|
setActiveTab(nextTab)
|
||||||
localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, nextTab)
|
localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, nextTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const afterSalesTab = {
|
||||||
|
key: 'after-sales',
|
||||||
|
label: '售后问题单',
|
||||||
|
children: <AfterSalesPanel ref={afterSalesPanelRef} />,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<PageHeader title="接单平台" description="管理打手、接单工单、等级权限和本地 Mock 工单。" />
|
<PageHeader
|
||||||
|
title="接单平台"
|
||||||
|
description="管理打手、接单工单、等级权限、售后追责和本地 Mock 工单。"
|
||||||
|
/>
|
||||||
<SummaryCards />
|
<SummaryCards />
|
||||||
<Tabs
|
<Tabs
|
||||||
destroyOnHidden={false}
|
destroyOnHidden={false}
|
||||||
@@ -53,11 +80,13 @@ export default function AdminWorkerPlatformPage() {
|
|||||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||||
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
||||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||||
|
afterSalesTab,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||||
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
||||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||||
|
afterSalesTab,
|
||||||
...(isAdmin
|
...(isAdmin
|
||||||
? [{ key: 'announcement', label: '公告管理', children: <AnnouncementPanel /> }]
|
? [{ key: 'announcement', label: '公告管理', children: <AnnouncementPanel /> }]
|
||||||
: []),
|
: []),
|
||||||
@@ -85,6 +114,7 @@ function loadActiveTabPreference(): string {
|
|||||||
return saved === 'orders' ||
|
return saved === 'orders' ||
|
||||||
saved === 'cancel-requests' ||
|
saved === 'cancel-requests' ||
|
||||||
saved === 'feedbacks' ||
|
saved === 'feedbacks' ||
|
||||||
|
saved === 'after-sales' ||
|
||||||
saved === 'announcement' ||
|
saved === 'announcement' ||
|
||||||
saved === 'rules' ||
|
saved === 'rules' ||
|
||||||
saved === 'product-match' ||
|
saved === 'product-match' ||
|
||||||
@@ -107,6 +137,7 @@ function isWorkerPlatformTab(tab: string | null): tab is string {
|
|||||||
'orders',
|
'orders',
|
||||||
'cancel-requests',
|
'cancel-requests',
|
||||||
'feedbacks',
|
'feedbacks',
|
||||||
|
'after-sales',
|
||||||
'announcement',
|
'announcement',
|
||||||
'rules',
|
'rules',
|
||||||
'product-match',
|
'product-match',
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import JsonPreview from '@/components/admin/JsonPreview'
|
|||||||
import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
|
import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
|
||||||
import ImageUpload from '@/components/files/ImageUpload'
|
import ImageUpload from '@/components/files/ImageUpload'
|
||||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||||
import AfterSalesPanel, { type AfterSalesPanelHandle } from './AfterSalesPanel'
|
|
||||||
import {
|
import {
|
||||||
acceptAdminWorkOrder,
|
acceptAdminWorkOrder,
|
||||||
acceptAdminWorkOrderShare,
|
acceptAdminWorkOrderShare,
|
||||||
@@ -135,7 +134,6 @@ export default function WorkOrdersPanel() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const afterSalesPanelRef = useRef<AfterSalesPanelHandle>(null)
|
|
||||||
const canEditRequirementFields = hasAdminRole('operator')
|
const canEditRequirementFields = hasAdminRole('operator')
|
||||||
const canViewStatistics = hasAdminRole('operator')
|
const canViewStatistics = hasAdminRole('operator')
|
||||||
const [statuses, setStatuses] = useState<string[]>([])
|
const [statuses, setStatuses] = useState<string[]>([])
|
||||||
@@ -989,7 +987,11 @@ export default function WorkOrdersPanel() {
|
|||||||
<Button
|
<Button
|
||||||
icon={<WarningOutlined />}
|
icon={<WarningOutlined />}
|
||||||
danger
|
danger
|
||||||
onClick={() => afterSalesPanelRef.current?.openCreateModal(row)}
|
onClick={() =>
|
||||||
|
navigate('/admin/worker-platform?tab=after-sales', {
|
||||||
|
state: { createOrder: row },
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
售后
|
售后
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1165,7 +1167,6 @@ export default function WorkOrdersPanel() {
|
|||||||
scroll={{ x: 1240 }}
|
scroll={{ x: 1240 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<AfterSalesPanel ref={afterSalesPanelRef} />
|
|
||||||
<Drawer
|
<Drawer
|
||||||
className="worker-order-detail-drawer"
|
className="worker-order-detail-drawer"
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -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 { 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 { useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
fetchWorkerAfterSalesCases,
|
||||||
fetchWorkerCancelRequests,
|
fetchWorkerCancelRequests,
|
||||||
fetchWorkerOrderFeedbacks,
|
fetchWorkerOrderFeedbacks,
|
||||||
fetchWorkerMyOrderOverview,
|
fetchWorkerMyOrderOverview,
|
||||||
@@ -20,7 +22,6 @@ import {
|
|||||||
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
|
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
|
||||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||||
import { buildWorkerOrderQuery } from '@/utils/worker-order-query'
|
import { buildWorkerOrderQuery } from '@/utils/worker-order-query'
|
||||||
import WorkerAfterSalesSection from './WorkerAfterSalesSection'
|
|
||||||
import { WorkerOrderDesktopList } from './WorkerOrderDesktopList'
|
import { WorkerOrderDesktopList } from './WorkerOrderDesktopList'
|
||||||
import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer'
|
import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer'
|
||||||
import { WorkerOrderMobileList } from './WorkerOrderMobileList'
|
import { WorkerOrderMobileList } from './WorkerOrderMobileList'
|
||||||
@@ -144,6 +145,15 @@ export default function WorkerOrdersPage() {
|
|||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const queryClient = useQueryClient()
|
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 [status, setStatus] = useState('in_progress')
|
||||||
const [keywordInput, setKeywordInput] = useState('')
|
const [keywordInput, setKeywordInput] = useState('')
|
||||||
const [keyword, setKeyword] = useState('')
|
const [keyword, setKeyword] = useState('')
|
||||||
@@ -441,7 +451,38 @@ export default function WorkerOrdersPage() {
|
|||||||
|
|
||||||
return (
|
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 ? (
|
{isMobile ? (
|
||||||
<WorkerOrderMobileList
|
<WorkerOrderMobileList
|
||||||
statusOptions={STATUS_OPTIONS}
|
statusOptions={STATUS_OPTIONS}
|
||||||
|
|||||||
Reference in New Issue
Block a user