新增打手问题反馈
This commit is contained in:
@@ -9,6 +9,7 @@ import { getAdminRole } from '@/utils/admin-auth'
|
||||
import CategoriesPanel from './panels/CategoriesPanel'
|
||||
import CancelRequestsPanel from './panels/CancelRequestsPanel'
|
||||
import FinancePanel from './panels/FinancePanel'
|
||||
import FeedbacksPanel from './panels/FeedbacksPanel'
|
||||
import LevelsPanel from './panels/LevelsPanel'
|
||||
import NotificationsPanel from './panels/NotificationsPanel'
|
||||
import ProductRulesPanel from './panels/ProductRulesPanel'
|
||||
@@ -49,10 +50,12 @@ export default function AdminWorkerPlatformPage() {
|
||||
? [
|
||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||
]
|
||||
: [
|
||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
|
||||
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
||||
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
||||
@@ -75,6 +78,7 @@ function loadActiveTabPreference(): string {
|
||||
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
|
||||
return saved === 'orders' ||
|
||||
saved === 'cancel-requests' ||
|
||||
saved === 'feedbacks' ||
|
||||
saved === 'rules' ||
|
||||
saved === 'product-match' ||
|
||||
saved === 'categories' ||
|
||||
@@ -94,6 +98,7 @@ function isWorkerPlatformTab(tab: string | null): tab is string {
|
||||
return [
|
||||
'orders',
|
||||
'cancel-requests',
|
||||
'feedbacks',
|
||||
'rules',
|
||||
'product-match',
|
||||
'categories',
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { App, Button, Card, Form, Input, Modal, Space, Table, Tag, Typography } from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router'
|
||||
|
||||
import { fetchAdminWorkerOrderFeedbacks, resolveAdminWorkerOrderFeedback } from '@/services/admin'
|
||||
import type { WorkerOrderFeedback } from '@/types/worker-platform'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type FeedbackReplyFormValues = {
|
||||
reply?: string
|
||||
}
|
||||
|
||||
export default function FeedbacksPanel() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [target, setTarget] = useState<WorkerOrderFeedback | null>(null)
|
||||
const [form] = Form.useForm<FeedbackReplyFormValues>()
|
||||
const feedbacksQuery = useQuery({
|
||||
queryKey: ['admin-worker-order-feedbacks', 'pending'],
|
||||
queryFn: () => fetchAdminWorkerOrderFeedbacks('pending'),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const feedbackId = Number(searchParams.get('feedbackId') || 0)
|
||||
if (!Number.isInteger(feedbackId) || feedbackId <= 0) return
|
||||
const feedback = (feedbacksQuery.data?.data.items || []).find(
|
||||
(item) => item.feedbackId === feedbackId,
|
||||
)
|
||||
if (!feedback) return
|
||||
setTarget(feedback)
|
||||
form.resetFields()
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
current.delete('feedbackId')
|
||||
return current
|
||||
},
|
||||
{ replace: true },
|
||||
)
|
||||
}, [feedbacksQuery.data, form, searchParams, setSearchParams])
|
||||
|
||||
async function submitReply(values: FeedbackReplyFormValues) {
|
||||
if (!target) return
|
||||
try {
|
||||
await resolveAdminWorkerOrderFeedback(target.feedbackId, {
|
||||
reply: String(values.reply || '').trim(),
|
||||
})
|
||||
message.success('问题反馈已处理')
|
||||
setTarget(null)
|
||||
form.resetFields()
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-worker-order-feedbacks'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
|
||||
])
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '处理问题反馈失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<WorkerOrderFeedback> = [
|
||||
{
|
||||
title: '订单',
|
||||
width: 260,
|
||||
render: (_, feedback) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong>{feedback.productName || '-'}</Typography.Text>
|
||||
<Typography.Text type="secondary">{feedback.workOrderNo || '-'}</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '打手',
|
||||
width: 180,
|
||||
render: (_, feedback) => (
|
||||
<Space wrap>
|
||||
<Typography.Text>{feedback.workerName || `打手 #${feedback.workerId}`}</Typography.Text>
|
||||
{feedback.shareQuantity ? (
|
||||
<Tag color="purple">拼单 {feedback.shareQuantity} 份</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '反馈内容',
|
||||
render: (_, feedback) => (
|
||||
<Typography.Text ellipsis={{ tooltip: feedback.content }}>
|
||||
{feedback.content}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '反馈时间',
|
||||
width: 180,
|
||||
render: (_, feedback) => formatAdminDateTime(feedback.createdAt),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
render: (_, feedback) => (
|
||||
<Button type="primary" size="small" onClick={() => setTarget(feedback)}>
|
||||
处理
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
title={`待处理问题反馈(${feedbacksQuery.data?.data.items.length || 0})`}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={feedbacksQuery.isFetching}
|
||||
onClick={() => feedbacksQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<Table<WorkerOrderFeedback>
|
||||
rowKey="feedbackId"
|
||||
size="small"
|
||||
loading={feedbacksQuery.isLoading}
|
||||
dataSource={feedbacksQuery.data?.data.items || []}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
locale={{ emptyText: '暂无待处理问题反馈' }}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
<Modal
|
||||
title="处理问题反馈"
|
||||
open={Boolean(target)}
|
||||
onCancel={() => {
|
||||
setTarget(null)
|
||||
form.resetFields()
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
okText="完成处理"
|
||||
destroyOnHidden
|
||||
>
|
||||
{target ? (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Typography.Text>
|
||||
{target.workerName || `打手 #${target.workerId}`}:{target.content}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
完成处理不会更改订单状态、验收或资金,仅向打手记录处理回复。
|
||||
</Typography.Text>
|
||||
<Form form={form} layout="vertical" onFinish={submitReply}>
|
||||
<Form.Item
|
||||
label="处理回复"
|
||||
name="reply"
|
||||
rules={[{ required: true, whitespace: true, message: '请填写处理回复' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
showCount
|
||||
placeholder="请说明已如何处理,或请打手后续操作"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
) : null}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const notificationEventRows: NotificationEventRow[] = [
|
||||
{ key: 'acceptance_reminded', name: '催验收', description: '打手在验收后催促处理' },
|
||||
{ key: 'material_required', name: '待补资料工单', description: '新工单等待后台补充资料' },
|
||||
{ key: 'cancel_requested', name: '撤单申请', description: '打手提交新的撤单申请,等待客服审核' },
|
||||
{ key: 'feedback_submitted', name: '问题反馈', description: '打手反馈客户资料或订单处理问题' },
|
||||
]
|
||||
|
||||
export default function NotificationsPanel() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
CopyOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
MessageOutlined,
|
||||
PictureOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
@@ -35,6 +36,7 @@ import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import {
|
||||
fetchWorkerCancelRequests,
|
||||
fetchWorkerOrderFeedbacks,
|
||||
fetchWorkerMyOrderOverview,
|
||||
fetchWorkerMyOrders,
|
||||
fetchWorkerProfile,
|
||||
@@ -45,6 +47,7 @@ import {
|
||||
supplementWorkerAcceptedOrderEvidence,
|
||||
submitWorkerAcceptance,
|
||||
submitWorkerCancelRequest,
|
||||
submitWorkerOrderFeedback,
|
||||
} from '@/services/worker'
|
||||
import type {
|
||||
CollectField,
|
||||
@@ -79,6 +82,10 @@ type CancelRequestFormValues = {
|
||||
reason?: string
|
||||
}
|
||||
|
||||
type FeedbackFormValues = {
|
||||
content?: string
|
||||
}
|
||||
|
||||
function buildWorkerOrderQuery(status: string, keyword: string, page: number, pageSize: number) {
|
||||
return {
|
||||
keyword,
|
||||
@@ -201,10 +208,12 @@ export default function WorkerOrdersPage() {
|
||||
const [editingOrder, setEditingOrder] = useState<WorkOrder | null>(null)
|
||||
const [noteOrder, setNoteOrder] = useState<WorkOrder | null>(null)
|
||||
const [cancelRequestOrder, setCancelRequestOrder] = useState<WorkOrder | null>(null)
|
||||
const [feedbackOrder, setFeedbackOrder] = useState<WorkOrder | null>(null)
|
||||
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
||||
const [form] = Form.useForm<AcceptanceFormValues>()
|
||||
const [noteForm] = Form.useForm<WorkerNoteFormValues>()
|
||||
const [cancelRequestForm] = Form.useForm<CancelRequestFormValues>()
|
||||
const [feedbackForm] = Form.useForm<FeedbackFormValues>()
|
||||
|
||||
const ordersQuery = useQuery({
|
||||
queryKey: ['worker-my-orders', status, keyword, page, pageSize],
|
||||
@@ -234,6 +243,11 @@ export default function WorkerOrdersPage() {
|
||||
queryFn: fetchWorkerCancelRequests,
|
||||
retry: false,
|
||||
})
|
||||
const feedbacksQuery = useQuery({
|
||||
queryKey: ['worker-order-feedbacks'],
|
||||
queryFn: fetchWorkerOrderFeedbacks,
|
||||
retry: false,
|
||||
})
|
||||
const overviewByStatus = new Map(
|
||||
(overviewQuery.data?.data.items || []).map((item) => [item.status, item]),
|
||||
)
|
||||
@@ -243,6 +257,7 @@ export default function WorkerOrdersPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-cancel-requests'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-order-feedbacks'] }),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -349,6 +364,27 @@ export default function WorkerOrdersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function openFeedbackModal(order: WorkOrder) {
|
||||
markOrderAsRead(order)
|
||||
setFeedbackOrder(order)
|
||||
feedbackForm.resetFields()
|
||||
}
|
||||
|
||||
async function submitFeedback(values: FeedbackFormValues) {
|
||||
if (!feedbackOrder) return
|
||||
try {
|
||||
await submitWorkerOrderFeedback(feedbackOrder.workOrderId, {
|
||||
content: String(values.content || '').trim(),
|
||||
})
|
||||
message.success('问题反馈已提交,等待客服处理')
|
||||
setFeedbackOrder(null)
|
||||
feedbackForm.resetFields()
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '提交问题反馈失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOrderNote(values: WorkerNoteFormValues) {
|
||||
if (!noteOrder) return
|
||||
try {
|
||||
@@ -684,6 +720,13 @@ export default function WorkerOrdersPage() {
|
||||
申请取消
|
||||
</Button>
|
||||
) : null}
|
||||
{canSubmitFeedback(row) ? (
|
||||
<Tooltip title="反馈客户资料或订单处理问题">
|
||||
<Button icon={<MessageOutlined />} onClick={() => openFeedbackModal(row)}>
|
||||
反馈
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
@@ -995,6 +1038,15 @@ export default function WorkerOrdersPage() {
|
||||
申请取消
|
||||
</Button>
|
||||
) : null}
|
||||
{canSubmitFeedback(order) ? (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<MessageOutlined />}
|
||||
onClick={() => openFeedbackModal(order)}
|
||||
>
|
||||
反馈
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1163,6 +1215,11 @@ export default function WorkerOrdersPage() {
|
||||
申请取消
|
||||
</Button>
|
||||
) : null}
|
||||
{canSubmitFeedback(detailOrder) ? (
|
||||
<Button icon={<MessageOutlined />} onClick={() => openFeedbackModal(detailOrder)}>
|
||||
问题反馈
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
@@ -1279,6 +1336,52 @@ export default function WorkerOrdersPage() {
|
||||
</div>
|
||||
) : null}
|
||||
</Drawer>
|
||||
<Modal
|
||||
title={`问题反馈 · ${getDisplayOrderNo(feedbackOrder)}`}
|
||||
open={Boolean(feedbackOrder)}
|
||||
width={isMobile ? '92%' : 540}
|
||||
onCancel={() => {
|
||||
setFeedbackOrder(null)
|
||||
feedbackForm.resetFields()
|
||||
}}
|
||||
onOk={() => feedbackForm.submit()}
|
||||
okText="提交反馈"
|
||||
destroyOnHidden
|
||||
>
|
||||
<Typography.Paragraph type="secondary">
|
||||
可反馈客户资料错误、账号异常或无法继续处理等问题。客服处理后会在反馈记录中回复。
|
||||
</Typography.Paragraph>
|
||||
<Form form={feedbackForm} layout="vertical" onFinish={submitFeedback}>
|
||||
<Form.Item
|
||||
label="问题说明"
|
||||
name="content"
|
||||
rules={[{ required: true, whitespace: true, message: '请填写问题说明' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
showCount
|
||||
placeholder="例如:客户提供的游戏账号密码错误,无法登录"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{(feedbacksQuery.data?.data.items || []).filter(
|
||||
(item) => item.workOrderId === feedbackOrder?.workOrderId,
|
||||
).length > 0 ? (
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Typography.Text type="secondary">该订单最近反馈</Typography.Text>
|
||||
{(feedbacksQuery.data?.data.items || [])
|
||||
.filter((item) => item.workOrderId === feedbackOrder?.workOrderId)
|
||||
.slice(0, 3)
|
||||
.map((item) => (
|
||||
<Typography.Text key={item.feedbackId} type="secondary">
|
||||
{item.status === 'resolved' ? '已处理' : '待处理'}:{item.content}
|
||||
{item.reply ? `;客服回复:${item.reply}` : ''}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</Space>
|
||||
) : null}
|
||||
</Modal>
|
||||
<Modal
|
||||
title={`申请取消 · ${getDisplayOrderNo(cancelRequestOrder)}`}
|
||||
open={Boolean(cancelRequestOrder)}
|
||||
@@ -1482,6 +1585,11 @@ function canRequestCancel(order: WorkOrder) {
|
||||
return order.status === 'in_progress'
|
||||
}
|
||||
|
||||
function canSubmitFeedback(order: WorkOrder) {
|
||||
if (order.myShare) return order.myShare.status !== 'cancelled'
|
||||
return Boolean(order.assignedAt || order.worker)
|
||||
}
|
||||
|
||||
function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
|
||||
if (!workerCanAutoAcceptWithoutEvidence) return false
|
||||
if (order.myShare) return order.myShare.status === 'joined' && order.status !== 'accepted'
|
||||
|
||||
Reference in New Issue
Block a user