优化订单打手信息展示

This commit is contained in:
yml2213
2026-08-21 11:49:42 +08:00
parent db2a401e33
commit 993ddcd034
8 changed files with 192 additions and 42 deletions
@@ -338,6 +338,7 @@ export type ListInput = {
keyword?: string keyword?: string
/** 打手类型筛选:internal 内部打手 / external 外部打手 */ /** 打手类型筛选:internal 内部打手 / external 外部打手 */
workerType?: string workerType?: string
/** 后台打手列表按主键精确定位。 */
workerId?: number workerId?: number
/** 后台待验收筛选:普通单按接单人,拼单按参与打手等级。 */ /** 后台待验收筛选:普通单按接单人,拼单按参与打手等级。 */
workerLevelIds?: number[] workerLevelIds?: number[]
@@ -483,8 +483,9 @@ export async function listWorkerUsers({
status = '', status = '',
keyword = '', keyword = '',
workerType = '', workerType = '',
workerId = 0,
}: ListInput = {}): Promise<{ items: WorkerUserRow[]; total: number }> { }: ListInput = {}): Promise<{ items: WorkerUserRow[]; total: number }> {
const { whereClause, params } = buildWorkerUserWhere({ status, keyword, workerType }) const { whereClause, params } = buildWorkerUserWhere({ status, keyword, workerType, workerId })
const totalResult = await query<{ total: number }>( const totalResult = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM worker_users wu ${whereClause}`, `SELECT COUNT(*)::int AS total FROM worker_users wu ${whereClause}`,
params, params,
@@ -1257,7 +1258,8 @@ function buildWorkerUserWhere({
status = '', status = '',
keyword = '', keyword = '',
workerType = '', workerType = '',
}: Pick<ListInput, 'status' | 'keyword' | 'workerType'>) { workerId = 0,
}: Pick<ListInput, 'status' | 'keyword' | 'workerType' | 'workerId'>) {
const filters: string[] = [] const filters: string[] = []
const params: unknown[] = [] const params: unknown[] = []
if (status) { if (status) {
@@ -1274,6 +1276,10 @@ function buildWorkerUserWhere({
params.push(workerType) params.push(workerType)
filters.push(`wu.worker_type = $${params.length}`) filters.push(`wu.worker_type = $${params.length}`)
} }
if (workerId) {
params.push(workerId)
filters.push(`wu.id = $${params.length}`)
}
return { return {
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '', whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params, params,
@@ -1003,9 +1003,11 @@ export async function deleteAdminWorkProductRule(ruleId: number | string) {
export async function listAdminWorkerUsers(query: JsonObject = {}) { export async function listAdminWorkerUsers(query: JsonObject = {}) {
const page = normalizePage(query.page) const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize) const pageSize = normalizePageSize(query.pageSize)
const workerId = normalizeOptionalId(query.workerId ?? query.worker_id) || 0
const { items, total } = await listWorkerUsers({ const { items, total } = await listWorkerUsers({
page, page,
pageSize, pageSize,
workerId,
status: String(query.status || '').trim(), status: String(query.status || '').trim(),
keyword: String(query.keyword || '').trim(), keyword: String(query.keyword || '').trim(),
workerType: String(query.workerType || query.worker_type || '').trim(), workerType: String(query.workerType || query.worker_type || '').trim(),
@@ -1445,7 +1447,7 @@ function mapAdminWorkerWithdrawalAccount(account: WorkerWithdrawalAccountRow) {
export async function getAdminWorkOrderEvents(workOrderId: number | string) { export async function getAdminWorkOrderEvents(workOrderId: number | string) {
await getRequiredWorkOrder(workOrderId) await getRequiredWorkOrder(workOrderId)
const events = await listWorkOrderEventsByOrderId(workOrderId) const events = await listWorkOrderEventsByOrderId(workOrderId)
return { items: await mapWorkOrderEvents(events) } return { items: await mapWorkOrderEvents(events, { includeWorkerSummary: true }) }
} }
export async function listAdminWorkOrders(query: JsonObject = {}) { export async function listAdminWorkOrders(query: JsonObject = {}) {
@@ -1686,6 +1686,15 @@ export type WorkOrderEventView = {
fromStatus: string fromStatus: string
toStatus: string toStatus: string
payload: JsonObject payload: JsonObject
/** 仅管理端流转记录返回,供查看事件涉及的打手信息。 */
worker?: {
workerId: number
displayName: string
username: string
workerType: string
levelName: string
status: string
}
} }
/** /**
@@ -1703,24 +1712,30 @@ export async function mapWorkOrderEvents(
payload_json: string | Record<string, unknown> payload_json: string | Record<string, unknown>
created_at: string created_at: string
}>, }>,
options: { includeWorkerSummary?: boolean } = {},
): Promise<WorkOrderEventView[]> { ): Promise<WorkOrderEventView[]> {
const workerIds = [ const workerIds = [
...new Set( ...new Set(
events events.map(resolveWorkOrderEventWorkerId).filter((id) => Number.isFinite(id) && id > 0),
.filter((event) => event.actor_type === 'worker')
.map((event) => Number(event.actor_id))
.filter((id) => Number.isFinite(id) && id > 0),
), ),
] ]
const workerNameById = new Map<number, string>() const workers = await Promise.all(workerIds.map((workerId) => getWorkerUserById(workerId)))
for (const workerId of workerIds) { const workerById = new Map(
const worker = await getWorkerUserById(workerId) workers
if (worker) { .filter((worker): worker is WorkerUserRow => Boolean(worker))
workerNameById.set(workerId, worker.display_name || worker.username || `打手#${workerId}`) .map((worker) => [Number(worker.id), worker]),
} )
} const workerNameById = new Map(
[...workerById.entries()].map(([workerId, worker]) => [
workerId,
worker.display_name || worker.username || `打手#${workerId}`,
]),
)
return events.map((event) => ({ return events.map((event) => {
const workerId = resolveWorkOrderEventWorkerId(event)
const worker = workerById.get(workerId)
return {
id: Number(event.id), id: Number(event.id),
time: new Date(event.created_at).toISOString(), time: new Date(event.created_at).toISOString(),
actorType: event.actor_type, actorType: event.actor_type,
@@ -1729,7 +1744,32 @@ export async function mapWorkOrderEvents(
fromStatus: event.from_status, fromStatus: event.from_status,
toStatus: event.to_status, toStatus: event.to_status,
payload: safeParseJson(event.payload_json), payload: safeParseJson(event.payload_json),
})) ...(options.includeWorkerSummary && worker
? {
worker: {
workerId: Number(worker.id),
displayName: worker.display_name || worker.username || `打手#${worker.id}`,
username: worker.username,
workerType: normalizeWorkerType(worker.worker_type),
levelName: worker.level_name || '-',
status: worker.status,
},
}
: {}),
}
})
}
/** 操作人优先,其次读取事件载荷中的关联打手。 */
function resolveWorkOrderEventWorkerId(event: {
actor_type: string
actor_id: string
payload_json: string | Record<string, unknown>
}) {
const actorWorkerId = event.actor_type === 'worker' ? Number(event.actor_id) : 0
if (Number.isInteger(actorWorkerId) && actorWorkerId > 0) return actorWorkerId
const payloadWorkerId = Number(safeParseJson(event.payload_json).workerId || 0)
return Number.isInteger(payloadWorkerId) && payloadWorkerId > 0 ? payloadWorkerId : 0
} }
function resolveWorkOrderEventActorName( function resolveWorkOrderEventActorName(
@@ -1,11 +1,13 @@
import { ClockCircleOutlined } from '@ant-design/icons' import { ClockCircleOutlined, LinkOutlined } from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Empty, Skeleton, Tag, Timeline, Tooltip, Typography } from 'antd' import { Empty, Skeleton, Space, Tag, Timeline, Tooltip, Typography } from 'antd'
import { useNavigate } from 'react-router'
import dayjs from '@/lib/dayjs' import dayjs from '@/lib/dayjs'
import { fetchAdminWorkOrderEvents } from '@/services/admin' import { fetchAdminWorkOrderEvents } from '@/services/admin'
import { fetchWorkerMyOrderEvents } from '@/services/worker' import { fetchWorkerMyOrderEvents } from '@/services/worker'
import type { WorkOrderEvent } from '@/types/worker-platform' import type { WorkOrderEvent } from '@/types/worker-platform'
import { getAdminRole } from '@/utils/admin-auth'
type EventNodeStyle = { type EventNodeStyle = {
/** 节点标题(主文案) */ /** 节点标题(主文案) */
@@ -279,6 +281,8 @@ export default function WorkOrderEventTimeline({
orderId: number orderId: number
viewer: 'admin' | 'worker' viewer: 'admin' | 'worker'
}) { }) {
const navigate = useNavigate()
const canNavigateToWorker = viewer === 'admin' && getAdminRole() !== 'support'
const eventsQuery = useQuery({ const eventsQuery = useQuery({
queryKey: ['work-order-events', viewer, orderId], queryKey: ['work-order-events', viewer, orderId],
queryFn: () => queryFn: () =>
@@ -324,6 +328,28 @@ export default function WorkOrderEventTimeline({
</Typography.Text> </Typography.Text>
) : null} ) : null}
<div className="work-order-event-actor"> <div className="work-order-event-actor">
{event.worker ? (
<Space size={[4, 4]} wrap>
<Tag
color="blue"
className={canNavigateToWorker ? 'work-order-event-worker-link' : undefined}
onClick={() => {
if (!canNavigateToWorker) return
void navigate(
`/admin/worker-platform?tab=workers&workerId=${event.worker?.workerId}`,
)
}}
>
{event.worker.displayName} #{event.worker.workerId}
{canNavigateToWorker ? <LinkOutlined /> : null}
</Tag>
<Typography.Text type="secondary" className="work-order-event-worker-meta">
{event.worker.username} ·{' '}
{event.worker.workerType === 'internal' ? '内部' : '外部'} ·{' '}
{event.worker.levelName} · {formatWorkerStatus(event.worker.status)}
</Typography.Text>
</Space>
) : (
<Tag <Tag
color={ color={
event.actorType === 'system' event.actorType === 'system'
@@ -337,6 +363,7 @@ export default function WorkOrderEventTimeline({
> >
{event.actorName || event.actorType} {event.actorName || event.actorType}
</Tag> </Tag>
)}
</div> </div>
</div> </div>
), ),
@@ -345,3 +372,13 @@ export default function WorkOrderEventTimeline({
return <Timeline items={items} className="work-order-event-timeline" /> return <Timeline items={items} className="work-order-event-timeline" />
} }
function formatWorkerStatus(status: string) {
const labels: Record<string, string> = {
active: '已启用',
pending_review: '待审核',
rejected: '已冻结',
disabled: '已停用',
}
return labels[status] || status || '未知状态'
}
@@ -15,7 +15,8 @@ import {
Typography, Typography,
} from 'antd' } from 'antd'
import type { TableColumnsType } from 'antd' import type { TableColumnsType } from 'antd'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router'
import { import {
creditAdminWorkerWallet, creditAdminWorkerWallet,
@@ -50,10 +51,12 @@ type FreezeWorkerFormValues = {
export default function WorkersPanel() { export default function WorkersPanel() {
const { message, modal } = App.useApp() const { message, modal } = App.useApp()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [searchParams, setSearchParams] = useSearchParams()
const [status, setStatus] = useState('') const [status, setStatus] = useState('')
const [keywordInput, setKeywordInput] = useState('') const [keywordInput, setKeywordInput] = useState('')
const [keyword, setKeyword] = useState('') const [keyword, setKeyword] = useState('')
const [workerType, setWorkerType] = useState('') const [workerType, setWorkerType] = useState('')
const [targetWorkerId, setTargetWorkerId] = useState(0)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE) const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null) const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null)
@@ -74,9 +77,39 @@ export default function WorkersPanel() {
(account) => account.accountChannel === 'alipay', (account) => account.accountChannel === 'alipay',
) )
useEffect(() => {
const workerId = Number(searchParams.get('workerId') || 0)
if (!Number.isInteger(workerId) || workerId <= 0) {
setTargetWorkerId(0)
return
}
setTargetWorkerId(workerId)
setKeywordInput('')
setKeyword('')
setStatus('')
setWorkerType('')
setPage(1)
}, [searchParams])
const workersQuery = useQuery({ const workersQuery = useQuery({
queryKey: ['admin-worker-platform-workers', status, keyword, workerType, page, pageSize], queryKey: [
queryFn: () => fetchAdminWorkerUsers({ status, keyword, workerType, page, pageSize }), 'admin-worker-platform-workers',
status,
keyword,
workerType,
targetWorkerId,
page,
pageSize,
],
queryFn: () =>
fetchAdminWorkerUsers({
status,
keyword,
workerType,
workerId: targetWorkerId || undefined,
page,
pageSize,
}),
}) })
const levelsQuery = useQuery({ const levelsQuery = useQuery({
queryKey: ['admin-worker-platform-levels'], queryKey: ['admin-worker-platform-levels'],
@@ -105,7 +138,15 @@ export default function WorkersPanel() {
setKeyword('') setKeyword('')
setStatus('') setStatus('')
setWorkerType('') setWorkerType('')
setTargetWorkerId(0)
setPage(1) setPage(1)
setSearchParams(
(current) => {
current.delete('workerId')
return current
},
{ replace: true },
)
} }
async function review(worker: WorkerUser, nextStatus: string) { async function review(worker: WorkerUser, nextStatus: string) {
@@ -431,6 +472,9 @@ export default function WorkersPanel() {
} }
bordered={false} bordered={false}
> >
{targetWorkerId ? (
<Typography.Text type="secondary"> #{targetWorkerId}</Typography.Text>
) : null}
<Table<WorkerUser> <Table<WorkerUser>
rowKey="workerId" rowKey="workerId"
loading={workersQuery.isLoading} loading={workersQuery.isLoading}
+12
View File
@@ -292,6 +292,18 @@
margin-inline-end: 0; margin-inline-end: 0;
} }
.work-order-event-worker-link {
cursor: pointer;
}
.work-order-event-worker-link .anticon {
margin-left: 4px;
}
.work-order-event-worker-meta {
font-size: 12px;
}
.work-order-event-desc { .work-order-event-desc {
font-size: 12px; font-size: 12px;
} }
@@ -495,6 +495,14 @@ export type WorkOrderEvent = {
fromStatus: string fromStatus: string
toStatus: string toStatus: string
payload: Record<string, unknown> payload: Record<string, unknown>
worker?: {
workerId: number
displayName: string
username: string
workerType: string
levelName: string
status: string
}
} }
export type Pagination = { export type Pagination = {