新增打手问题反馈

This commit is contained in:
yml2213
2026-08-20 19:56:10 +08:00
parent e504b3a207
commit 20b1066ba3
18 changed files with 788 additions and 0 deletions
@@ -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'