优化订单打手信息展示
This commit is contained in:
@@ -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,33 +1712,64 @@ 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) => {
|
||||||
id: Number(event.id),
|
const workerId = resolveWorkOrderEventWorkerId(event)
|
||||||
time: new Date(event.created_at).toISOString(),
|
const worker = workerById.get(workerId)
|
||||||
actorType: event.actor_type,
|
return {
|
||||||
actorName: resolveWorkOrderEventActorName(event, workerNameById),
|
id: Number(event.id),
|
||||||
eventType: event.event_type,
|
time: new Date(event.created_at).toISOString(),
|
||||||
fromStatus: event.from_status,
|
actorType: event.actor_type,
|
||||||
toStatus: event.to_status,
|
actorName: resolveWorkOrderEventActorName(event, workerNameById),
|
||||||
payload: safeParseJson(event.payload_json),
|
eventType: event.event_type,
|
||||||
}))
|
fromStatus: event.from_status,
|
||||||
|
toStatus: event.to_status,
|
||||||
|
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,19 +328,42 @@ export default function WorkOrderEventTimeline({
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="work-order-event-actor">
|
<div className="work-order-event-actor">
|
||||||
<Tag
|
{event.worker ? (
|
||||||
color={
|
<Space size={[4, 4]} wrap>
|
||||||
event.actorType === 'system'
|
<Tag
|
||||||
? 'default'
|
color="blue"
|
||||||
: event.actorType === 'worker'
|
className={canNavigateToWorker ? 'work-order-event-worker-link' : undefined}
|
||||||
? 'blue'
|
onClick={() => {
|
||||||
: event.actorType === 'admin'
|
if (!canNavigateToWorker) return
|
||||||
? 'green'
|
void navigate(
|
||||||
: 'default'
|
`/admin/worker-platform?tab=workers&workerId=${event.worker?.workerId}`,
|
||||||
}
|
)
|
||||||
>
|
}}
|
||||||
{event.actorName || event.actorType}
|
>
|
||||||
</Tag>
|
{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
|
||||||
|
color={
|
||||||
|
event.actorType === 'system'
|
||||||
|
? 'default'
|
||||||
|
: event.actorType === 'worker'
|
||||||
|
? 'blue'
|
||||||
|
: event.actorType === 'admin'
|
||||||
|
? 'green'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{event.actorName || event.actorType}
|
||||||
|
</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}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user