优化打手详情信息展示

This commit is contained in:
yml2213
2026-09-01 14:44:06 +08:00
parent e72ef7da0a
commit 48fcdbff07
5 changed files with 558 additions and 3 deletions
@@ -95,7 +95,7 @@ export default function WorkerDetailDrawer({
return ( return (
<Drawer <Drawer
className="worker-detail-drawer" className="worker-detail-drawer"
width={860} width="min(1120px, calc(100vw - 24px))"
open={Boolean(worker)} open={Boolean(worker)}
destroyOnHidden destroyOnHidden
onClose={onClose} onClose={onClose}
@@ -266,6 +266,7 @@ function UnfreezesSection({ workerId }: { workerId: number }) {
columns={columns} columns={columns}
dataSource={data?.items || []} dataSource={data?.items || []}
loading={unfreezesQuery.isFetching} loading={unfreezesQuery.isFetching}
scroll={{ x: 930 }}
pagination={buildAdminTablePagination({ pagination={buildAdminTablePagination({
page, page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE, pageSize: ADMIN_DEFAULT_PAGE_SIZE,
@@ -321,7 +322,14 @@ function LedgersSection({ workerId }: { workerId: number }) {
{ {
title: '关联订单', title: '关联订单',
width: 150, width: 150,
render: (_, row) => row.relatedPlatformOrderId || row.relatedWorkOrderId || '-', render: (_, row) => {
const orderNo = String(row.relatedPlatformOrderId || row.relatedWorkOrderId || '-').trim()
return (
<Typography.Text ellipsis={{ tooltip: orderNo }} copyable={orderNo !== '-'}>
{orderNo}
</Typography.Text>
)
},
}, },
{ {
title: '说明', title: '说明',
@@ -353,6 +361,7 @@ function LedgersSection({ workerId }: { workerId: number }) {
columns={columns} columns={columns}
dataSource={data?.items || []} dataSource={data?.items || []}
loading={ledgersQuery.isFetching} loading={ledgersQuery.isFetching}
scroll={{ x: 1020 }}
pagination={buildAdminTablePagination({ pagination={buildAdminTablePagination({
page, page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE, pageSize: ADMIN_DEFAULT_PAGE_SIZE,
@@ -377,7 +386,10 @@ function OrdersSection({ workerId }: { workerId: number }) {
title: '平台单号', title: '平台单号',
width: 160, width: 160,
ellipsis: true, ellipsis: true,
render: (_, row) => row.platformOrderId || row.workOrderNo || row.workOrderId, render: (_, row) => {
const orderNo = String(row.platformOrderId || row.workOrderNo || row.workOrderId).trim()
return <Typography.Text ellipsis={{ tooltip: orderNo }}>{orderNo}</Typography.Text>
},
}, },
{ {
title: '商品', title: '商品',
@@ -414,6 +426,7 @@ function OrdersSection({ workerId }: { workerId: number }) {
columns={columns} columns={columns}
dataSource={data?.items || []} dataSource={data?.items || []}
loading={ordersQuery.isFetching} loading={ordersQuery.isFetching}
scroll={{ x: 900 }}
pagination={buildAdminTablePagination({ pagination={buildAdminTablePagination({
page, page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE, pageSize: ADMIN_DEFAULT_PAGE_SIZE,
+65
View File
@@ -0,0 +1,65 @@
diff --git a/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx b/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx
index c184b61a..c3074a6a 100644
--- a/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx
+++ b/apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx
@@ -95,7 +95,7 @@ export default function WorkerDetailDrawer({
return (
<Drawer
className="worker-detail-drawer"
- width={860}
+ width="min(1120px, calc(100vw - 24px))"
open={Boolean(worker)}
destroyOnHidden
onClose={onClose}
@@ -266,6 +266,7 @@ function UnfreezesSection({ workerId }: { workerId: number }) {
columns={columns}
dataSource={data?.items || []}
loading={unfreezesQuery.isFetching}
+ scroll={{ x: 930 }}
pagination={buildAdminTablePagination({
page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
@@ -321,7 +322,14 @@ function LedgersSection({ workerId }: { workerId: number }) {
{
title: '关联订单',
width: 150,
- render: (_, row) => row.relatedPlatformOrderId || row.relatedWorkOrderId || '-',
+ render: (_, row) => {
+ const orderNo = String(row.relatedPlatformOrderId || row.relatedWorkOrderId || '-').trim()
+ return (
+ <Typography.Text ellipsis={{ tooltip: orderNo }} copyable={orderNo !== '-'}>
+ {orderNo}
+ </Typography.Text>
+ )
+ },
},
{
title: '说明',
@@ -353,6 +361,7 @@ function LedgersSection({ workerId }: { workerId: number }) {
columns={columns}
dataSource={data?.items || []}
loading={ledgersQuery.isFetching}
+ scroll={{ x: 1020 }}
pagination={buildAdminTablePagination({
page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
@@ -377,7 +386,10 @@ function OrdersSection({ workerId }: { workerId: number }) {
title: '平台单号',
width: 160,
ellipsis: true,
- render: (_, row) => row.platformOrderId || row.workOrderNo || row.workOrderId,
+ render: (_, row) => {
+ const orderNo = String(row.platformOrderId || row.workOrderNo || row.workOrderId).trim()
+ return <Typography.Text ellipsis={{ tooltip: orderNo }}>{orderNo}</Typography.Text>
+ },
},
{
title: '商品',
@@ -414,6 +426,7 @@ function OrdersSection({ workerId }: { workerId: number }) {
columns={columns}
dataSource={data?.items || []}
loading={ordersQuery.isFetching}
+ scroll={{ x: 900 }}
pagination={buildAdminTablePagination({
page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
+438
View File
@@ -0,0 +1,438 @@
import { useState } from 'react'
import { Card, Descriptions, Drawer, Select, Space, Table, Tabs, Tag, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import {
fetchAdminWorkOrders,
fetchAdminWorkerDetail,
fetchAdminWorkerDepositUnfreezes,
fetchAdminWorkerWalletLedgers,
} from '@/services/admin'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import { formatAdminDateTime } from '@/utils/admin-time'
import type {
WorkOrder,
WorkerDepositUnfreeze,
WorkerUser,
WorkerWalletLedger,
} from '@/types/worker-platform'
import { formatMoney, formatStatus, formatWorkerStatus, resolveStatusColor } from './shared'
/** 与打手端 worker-profile-view-utils 的流水文案保持一致;未知类型原样展示。 */
const LEDGER_TYPE_LABELS: Record<string, string> = {
manual_credit: '人工充值',
withdraw_freeze: '提现冻结',
withdraw_paid: '提现打款',
withdraw_release: '提现退回',
deposit_freeze: '冻结押金',
deposit_release: '释放押金',
deposit_pending_unfreeze: '待解冻金额',
reward_pending_unfreeze: '报酬待解冻',
deposit_unfreeze: '押金已解冻',
reward_unfreeze: '报酬已解冻',
deposit_deduction: '扣除押金',
reward_settlement: '结算报酬',
sharing_reward: '拼单报酬',
after_sales_deposit_deduction: '售后扣减押金',
after_sales_balance_deduction: '售后扣减余额',
after_sales_debt_offset: '抵扣售后欠款',
after_sales_refund: '售后追缴退还',
}
function formatLedgerType(ledgerType: string) {
return LEDGER_TYPE_LABELS[ledgerType] || ledgerType || '-'
}
function resolveLedgerTagColor(ledgerType: string) {
if (
[
'deposit_freeze',
'deposit_pending_unfreeze',
'reward_pending_unfreeze',
'after_sales_debt_offset',
].includes(ledgerType)
)
return 'gold'
if (
['deposit_release', 'deposit_unfreeze', 'reward_unfreeze', 'after_sales_refund'].includes(
ledgerType,
)
)
return 'green'
if (
[
'deposit_deduction',
'after_sales_deposit_deduction',
'after_sales_balance_deduction',
].includes(ledgerType)
)
return 'red'
if (['reward_settlement', 'sharing_reward'].includes(ledgerType)) return 'cyan'
if (ledgerType === 'manual_credit') return 'blue'
if (ledgerType === 'withdraw_freeze') return 'orange'
if (ledgerType === 'withdraw_paid') return 'purple'
if (ledgerType === 'withdraw_release') return 'green'
return 'default'
}
function formatUnfreezeStatus(status: string) {
if (status === 'pending') return { label: '待解冻', color: 'orange' }
if (status === 'released') return { label: '已到账', color: 'green' }
if (status === 'deducted') return { label: '已扣减', color: 'red' }
return { label: status || '-', color: 'default' }
}
export default function WorkerDetailDrawer({
worker,
onClose,
}: {
worker: WorkerUser | null
onClose: () => void
}) {
const workerId = worker?.workerId || 0
return (
<Drawer
className="worker-detail-drawer"
width="min(1120px, calc(100vw - 24px))"
open={Boolean(worker)}
destroyOnHidden
onClose={onClose}
title={
worker ? (
<Space size={8} wrap>
<span>{worker.displayName || worker.username}</span>
<Tag color={worker.workerType === 'internal' ? 'blue' : 'default'}>
{worker.workerType === 'internal' ? '内部' : '外部'}
</Tag>
<Tag color={worker.status === 'active' ? 'green' : 'orange'}>
{formatWorkerStatus(worker.status)}
</Tag>
</Space>
) : null
}
>
<Tabs
items={[
{ key: 'overview', label: '概览', children: <OverviewSection workerId={workerId} /> },
{
key: 'unfreezes',
label: '待解冻',
children: <UnfreezesSection workerId={workerId} />,
},
{
key: 'ledgers',
label: '资金流水',
children: <LedgersSection workerId={workerId} />,
},
{ key: 'orders', label: '他的订单', children: <OrdersSection workerId={workerId} /> },
]}
/>
</Drawer>
)
}
function OverviewSection({ workerId }: { workerId: number }) {
const detailQuery = useQuery({
queryKey: ['admin-worker-detail', workerId],
queryFn: () => fetchAdminWorkerDetail(workerId),
enabled: workerId > 0,
})
const detail = detailQuery.data?.data
if (!workerId) return null
if (!detail) {
return <Typography.Text type="secondary"></Typography.Text>
}
const { worker, summary, orderOverview } = detail
return (
<div className="worker-order-detail-stack">
<Card size="small" title="基本信息">
<Descriptions bordered size="small" column={{ xs: 1, md: 2 }}>
<Descriptions.Item label="昵称">{worker.displayName || '-'}</Descriptions.Item>
<Descriptions.Item label="账号">{worker.username}</Descriptions.Item>
<Descriptions.Item label="手机号">{worker.phone || '-'}</Descriptions.Item>
<Descriptions.Item label="等级">{worker.level?.name || '-'}</Descriptions.Item>
<Descriptions.Item label="邀请人">
{worker.inviter ? `${worker.inviter.displayName || worker.inviter.username}` : '-'}
</Descriptions.Item>
<Descriptions.Item label="注册时间">
{formatAdminDateTime(worker.createdAt)}
</Descriptions.Item>
</Descriptions>
</Card>
<Card size="small" title="钱包">
<Descriptions bordered size="small" column={{ xs: 1, md: 2 }}>
<Descriptions.Item label="可用余额">
{formatMoney(worker.wallet.availableAmount)}
</Descriptions.Item>
<Descriptions.Item label="冻结押金">
{formatMoney(worker.wallet.frozenDepositAmount)}
</Descriptions.Item>
<Descriptions.Item label="待解冻金额">
{formatMoney(worker.wallet.pendingUnfreezeAmount)}
</Descriptions.Item>
<Descriptions.Item label="累计充值">
{formatMoney(worker.wallet.totalCreditedAmount)}
</Descriptions.Item>
<Descriptions.Item label="累计结算">
{formatMoney(worker.wallet.totalSettledAmount)}
</Descriptions.Item>
</Descriptions>
</Card>
<Card size="small" title="接单统计">
<Descriptions bordered size="small" column={{ xs: 1, md: 3 }}>
<Descriptions.Item label="进行中">{summary.activeOrderCount}</Descriptions.Item>
<Descriptions.Item label="已完成">{summary.acceptedOrderCount}</Descriptions.Item>
<Descriptions.Item label="超时">
{summary.timeoutOrderCount > 0 ? <Tag color="red">{summary.timeoutOrderCount}</Tag> : 0}
</Descriptions.Item>
</Descriptions>
</Card>
<Card size="small" title="订单概览(按状态)">
{orderOverview.length === 0 ? (
<Typography.Text type="secondary"></Typography.Text>
) : (
<Space size={[12, 8]} wrap>
{orderOverview.map((item) => (
<Tag key={item.status} color={resolveStatusColor(item.status)}>
{formatStatus(item.status)} {item.orderCount} /{' '}
{formatMoney(item.settlementAmount)}
</Tag>
))}
</Space>
)}
</Card>
</div>
)
}
function UnfreezesSection({ workerId }: { workerId: number }) {
const [page, setPage] = useState(1)
const unfreezesQuery = useQuery({
queryKey: ['admin-worker-detail-unfreezes', workerId, page],
queryFn: () =>
fetchAdminWorkerDepositUnfreezes(workerId, { page, pageSize: ADMIN_DEFAULT_PAGE_SIZE }),
enabled: workerId > 0,
})
const data = unfreezesQuery.data?.data
const columns: TableColumnsType<WorkerDepositUnfreeze> = [
{
title: '资金类型',
width: 110,
render: (_, row) => {
const labels: Record<string, string> = {
deposit: '押金',
reward: '报酬',
legacy_combined: '历史合并',
}
return <Tag>{labels[row.fundType] || row.fundType}</Tag>
},
},
{
title: '金额',
width: 110,
render: (_, row) => formatMoney(row.amount),
},
{
title: '状态',
width: 100,
render: (_, row) => {
const status = formatUnfreezeStatus(row.status)
return <Tag color={status.color}>{status.label}</Tag>
},
},
{
title: '到期时间',
width: 170,
render: (_, row) => formatAdminDateTime(row.unfreezeAt),
},
{
title: '到账时间',
width: 170,
render: (_, row) => (row.releasedAt ? formatAdminDateTime(row.releasedAt) : '-'),
},
{ title: '关联工单', dataIndex: 'workOrderId', width: 100 },
{
title: '创建时间',
width: 170,
render: (_, row) => formatAdminDateTime(row.createdAt),
},
]
return (
<Table<WorkerDepositUnfreeze>
rowKey="id"
size="small"
columns={columns}
dataSource={data?.items || []}
loading={unfreezesQuery.isFetching}
scroll={{ x: 930 }}
pagination={buildAdminTablePagination({
page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
total: data?.pagination.total || 0,
onChange: setPage,
})}
/>
)
}
function LedgersSection({ workerId }: { workerId: number }) {
const [page, setPage] = useState(1)
const [ledgerType, setLedgerType] = useState('')
const ledgersQuery = useQuery({
queryKey: ['admin-worker-detail-ledgers', workerId, page, ledgerType],
queryFn: () =>
fetchAdminWorkerWalletLedgers(workerId, {
page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
ledgerType: ledgerType || undefined,
}),
enabled: workerId > 0,
})
const data = ledgersQuery.data?.data
const columns: TableColumnsType<WorkerWalletLedger> = [
{
title: '类型',
width: 130,
render: (_, row) => (
<Tag color={resolveLedgerTagColor(row.ledgerType)}>{formatLedgerType(row.ledgerType)}</Tag>
),
},
{
title: '金额',
width: 110,
render: (_, row) => (
<Typography.Text type={row.amount >= 0 ? 'success' : 'danger'}>
{row.amount >= 0 ? '+' : '-'}
{formatMoney(Math.abs(row.amount))}
</Typography.Text>
),
},
{
title: '变动后余额',
width: 110,
render: (_, row) => formatMoney(row.balanceAfter),
},
{
title: '冻结金额',
width: 110,
render: (_, row) => formatMoney(row.frozenAfter),
},
{
title: '关联订单',
width: 150,
render: (_, row) => {
const orderNo = String(row.relatedPlatformOrderId || row.relatedWorkOrderId || '-').trim()
return (
<Typography.Text ellipsis={{ tooltip: orderNo }} copyable={orderNo !== '-'}>
{orderNo}
</Typography.Text>
)
},
},
{
title: '说明',
ellipsis: true,
render: (_, row) => row.note || '-',
},
{
title: '时间',
width: 170,
render: (_, row) => formatAdminDateTime(row.createdAt),
},
]
return (
<div className="worker-order-detail-stack">
<Select
allowClear
value={ledgerType || undefined}
style={{ width: 180 }}
placeholder="全部类型"
options={Object.entries(LEDGER_TYPE_LABELS).map(([value, label]) => ({ value, label }))}
onChange={(next) => {
setLedgerType(next || '')
setPage(1)
}}
/>
<Table<WorkerWalletLedger>
rowKey="ledgerId"
size="small"
columns={columns}
dataSource={data?.items || []}
loading={ledgersQuery.isFetching}
scroll={{ x: 1020 }}
pagination={buildAdminTablePagination({
page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
total: data?.pagination.total || 0,
onChange: setPage,
})}
/>
</div>
)
}
function OrdersSection({ workerId }: { workerId: number }) {
const [page, setPage] = useState(1)
const ordersQuery = useQuery({
queryKey: ['admin-worker-detail-orders', workerId, page],
queryFn: () => fetchAdminWorkOrders({ workerId, page, pageSize: ADMIN_DEFAULT_PAGE_SIZE }),
enabled: workerId > 0,
})
const data = ordersQuery.data?.data
const columns: TableColumnsType<WorkOrder> = [
{
title: '平台单号',
width: 160,
ellipsis: true,
render: (_, row) => {
const orderNo = String(row.platformOrderId || row.workOrderNo || row.workOrderId).trim()
return <Typography.Text ellipsis={{ tooltip: orderNo }}>{orderNo}</Typography.Text>
},
},
{
title: '商品',
ellipsis: true,
render: (_, row) => row.productName || '-',
},
{
title: '状态',
width: 110,
render: (_, row) => (
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
),
},
{
title: '方式',
width: 90,
render: (_, row) => (row.sharing?.enabled ? `拼单` : '整单'),
},
{
title: '报酬',
width: 110,
render: (_, row) => formatMoney(row.rewardAmount),
},
{
title: '创建时间',
width: 170,
render: (_, row) => formatAdminDateTime(row.createdAt),
},
]
return (
<Table<WorkOrder>
rowKey="workOrderId"
size="small"
columns={columns}
dataSource={data?.items || []}
loading={ordersQuery.isFetching}
scroll={{ x: 900 }}
pagination={buildAdminTablePagination({
page,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
total: data?.pagination.total || 0,
onChange: setPage,
})}
/>
)
}
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
set -eu
target="${1:-$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/MODIFIED_FILE.tsx}"
repo="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
git -C "$repo" show e72ef7da:apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx > "$target"
printf 'rollback restored e72ef7da content: %s\n' "$target"
+31
View File
@@ -0,0 +1,31 @@
打手详情抽屉布局优化验证
改变分支/字段:
- WorkerDetailDrawer:抽屉宽度改为 min(1120px, calc(100vw - 24px)),适配桌面与小屏。
- 待解冻、资金流水、他的订单:增加横向滚动,保留完整列宽。
- 长订单号:增加省略提示与复制入口。
工件:
- MODIFIED_FILE: /Users/yml/codes/order_site/worker-detail-artifacts/MODIFIED_FILE.tsx
- DIFF_FILE: /Users/yml/codes/order_site/worker-detail-artifacts/DIFF_FILE.patch
- VERIFICATION.txt: /Users/yml/codes/order_site/worker-detail-artifacts/VERIFICATION.txt
- ROLLBACK.sh: /Users/yml/codes/order_site/worker-detail-artifacts/ROLLBACK.sh
原始文件 blobe72ef7da:apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx
修改文件 sha256dae5768684766a20e331509f19085c173489eb455ae22ace609ac678da47e2d4
BASELINE
- 命令:cd /Users/yml/codes/order_site/apps/frontend && npm test
- 输入:e72ef7da 基线工作树
- 字面结果:tests 15pass 15fail 0skipped 0exit 0
MODIFIED
- 命令:cd /Users/yml/codes/order_site/apps/frontend && npm run format:check && npm run lint:check && npm run typecheck && npm test && npm run build
- 输入:当前修改工作树
- 字面结果:格式通过;Lint 通过;类型检查通过;tests 15、pass 15、fail 0;生产构建成功;exit 0
ROLLBACK
- 命令:cp /Users/yml/codes/order_site/worker-detail-artifacts/MODIFIED_FILE.tsx /tmp/worker-detail-rollback-test.tsx && /Users/yml/codes/order_site/worker-detail-artifacts/ROLLBACK.sh /tmp/worker-detail-rollback-test.tsx && cmp /tmp/worker-detail-rollback-test.tsx <(git -C /Users/yml/codes/order_site show e72ef7da:apps/frontend/src/pages/admin/panels/WorkerDetailDrawer.tsx)
- 输入:修改副本 /tmp/worker-detail-rollback-test.tsx
- 字面结果:rollback restored e72ef7da contentcmp exit 0ROLLBACK.sh exit 0
- 恢复状态:另一份副本恢复基线内容,MODIFIED_FILE 保持修改内容。