优化打手端我的订单与个人中心界面布局

This commit is contained in:
yml2213
2026-08-02 10:32:51 +08:00
parent 93df3d1c9b
commit c9195c623d
3 changed files with 225 additions and 264 deletions
@@ -9,18 +9,15 @@ import {
App,
Button,
Card,
Col,
Descriptions,
Drawer,
Empty,
Form,
Input,
Modal,
Row,
Segmented,
Space,
Statistic,
Table,
Tabs,
Tag,
Typography,
} from 'antd'
@@ -40,24 +37,14 @@ import { formatDateTime } from '@/utils/date-time'
const STATUS_OPTIONS = [
{ value: '', label: '全部订单' },
{ value: 'open', label: '已超时' },
{ value: 'in_progress', label: '代练中' },
{ value: 'pending_acceptance', label: '待验收' },
{ value: 'problem', label: '问题单' },
{ value: 'open', label: '已超时' },
{ value: 'accepted', label: '已验收' },
{ value: 'cancelled', label: '已取消' },
] as const
const STATUS_SUMMARY_ITEMS = [
{ key: 'all', label: '全部订单', note: '当前账号历史接单总数' },
{ key: 'in_progress', label: '代练中', note: '正在执行中的订单' },
{ key: 'pending_acceptance', label: '待验收', note: '已提交,等待审核' },
{ key: 'problem', label: '问题单', note: '需要补充或重新处理' },
{ key: 'open', label: '已超时', note: '超时被系统退回的订单数' },
{ key: 'accepted', label: '已验收', note: '已完成并结算的订单' },
{ key: 'cancelled', label: '已取消', note: '已结束且不再继续' },
] as const
type AcceptanceFormValues = {
note?: string
}
@@ -171,7 +158,7 @@ export default function WorkerOrdersPage() {
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单信息',
minWidth: 320,
minWidth: 280,
render: (_, row) => (
<div className="cell-stack">
<Typography.Link onClick={() => setDetailOrder(row)}>
@@ -179,12 +166,10 @@ export default function WorkerOrdersPage() {
</Typography.Link>
<Space size={[6, 6]} wrap>
{row.categoryName ? <Tag>{row.categoryName}</Tag> : null}
<Typography.Text type="secondary">
{row.workOrderNo}
</Typography.Text>
{row.myShare ? <Tag color="purple"></Tag> : null}
</Space>
<Typography.Text type="secondary">
{row.platformOrderId || '-'}
{row.workOrderNo} · {row.platformOrderId || '-'}
</Typography.Text>
<Typography.Text type="secondary">
{formatDateTime(row.assignedAt)}
@@ -192,6 +177,25 @@ export default function WorkerOrdersPage() {
</div>
),
},
{
title: '状态',
width: 170,
render: (_, row) => (
<div className="cell-stack">
<Space wrap size={6}>
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
) : null}
</Space>
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
<Typography.Text type="danger" ellipsis>
{row.problemNote}
</Typography.Text>
) : null}
</div>
),
},
{
title: '奖励 / 押金',
width: 150,
@@ -201,77 +205,46 @@ export default function WorkerOrdersPage() {
<Typography.Text type="secondary">
{formatMoney(row.freezeDepositAmount)}
</Typography.Text>
</div>
),
},
{
title: '我的拼单',
width: 170,
render: (_, row) =>
row.myShare ? (
<div className="cell-stack">
<Tag color="purple">
{formatSharingShareStatus(row.myShare.status)}
</Tag>
<Typography.Text>
{row.myShare.quantity} × {formatMoney(row.myShare.unitReward)}
</Typography.Text>
{row.myShare ? (
<Typography.Text type="secondary">
{formatMoney(row.myShare.shareReward)}
{row.myShare.quantity} · {formatMoney(row.myShare.shareReward)}
</Typography.Text>
</div>
) : (
<Typography.Text type="secondary">-</Typography.Text>
),
},
{
title: '当前状态',
width: 180,
render: (_, row) => (
<div className="cell-stack">
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
) : null}
<Typography.Text type="secondary">
{getStatusHint(row.status)}
</Typography.Text>
</div>
),
},
{
title: '验收信息',
minWidth: 210,
render: (_, row) => (
<div className="cell-stack">
<ImagePreviewList
files={getAcceptanceFiles(row)}
imageUrls={getAcceptanceImageUrls(row)}
size={46}
/>
<Typography.Text type="secondary">
{formatDateTime(getAcceptanceSubmittedAt(row))}
</Typography.Text>
{row.acceptance?.note ? (
<Typography.Text ellipsis>{row.acceptance.note}</Typography.Text>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
),
},
{
title: '问题备注',
minWidth: 180,
render: (_, row) => (
<Typography.Text ellipsis>
{String(row.problemNote || '').trim() || '-'}
</Typography.Text>
),
minWidth: 200,
render: (_, row) => {
const submittedAt = getAcceptanceSubmittedAt(row)
const hasAcceptance =
getAcceptanceFiles(row).length > 0 || getAcceptanceImageUrls(row).length > 0
if (!hasAcceptance && !submittedAt) {
return <Typography.Text type="secondary"></Typography.Text>
}
return (
<div className="cell-stack">
<ImagePreviewList
files={getAcceptanceFiles(row)}
imageUrls={getAcceptanceImageUrls(row)}
size={40}
/>
<Typography.Text type="secondary">
{formatDateTime(submittedAt)}
</Typography.Text>
{row.acceptance?.note ? (
<Typography.Text type="secondary" ellipsis>
{row.acceptance.note}
</Typography.Text>
) : null}
</div>
)
},
},
{
title: '操作',
width: 280,
width: 260,
render: (_, row) => (
<Space wrap>
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(row)}>
@@ -287,10 +260,7 @@ export default function WorkerOrdersPage() {
</Button>
) : null}
{canCancelOrder(row) ? (
<Button
danger
onClick={() => openCancelModal(row)}
>
<Button danger onClick={() => openCancelModal(row)}>
</Button>
) : null}
@@ -312,73 +282,56 @@ export default function WorkerOrdersPage() {
</Button>
</div>
<Row gutter={[16, 16]}>
{STATUS_SUMMARY_ITEMS.map((item) => (
<Col key={item.key} xs={24} sm={12} xl={8} xxl={4}>
<Card size="small">
<Statistic
title={item.label}
value={Number(overviewQuery.data?.[item.key] || 0)}
suffix="单"
loading={overviewQuery.isLoading}
/>
<Typography.Text type="secondary">{item.note}</Typography.Text>
</Card>
</Col>
))}
</Row>
<Card bordered={false}>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<Space wrap size={12}>
<Space.Compact style={{ width: '100%', maxWidth: 520 }}>
<Input
allowClear
value={keywordInput}
placeholder="搜索工单号 / 订单号 / 商品名"
prefix={<SearchOutlined />}
onChange={(event) => {
const nextValue = event.target.value
setKeywordInput(nextValue)
if (!nextValue.trim()) {
setKeyword('')
setPage(1)
}
}}
onPressEnter={applyKeywordSearch}
/>
<Button
icon={<SearchOutlined />}
loading={ordersQuery.isFetching}
onClick={applyKeywordSearch}
>
</Button>
</Space.Compact>
<Typography.Text type="secondary">
{STATUS_OPTIONS.find((item) => item.value === status)?.label || '全部订单'}
</Typography.Text>
</Space>
<Tabs
activeKey={status || 'all'}
items={STATUS_OPTIONS.map((item) => {
const countKey = item.value || 'all'
const count = Number(overviewQuery.data?.[countKey] || 0)
return {
key: item.value || 'all',
label: (
<span>
{item.label}
{count > 0 ? (
<span className="worker-order-tab-count">{count}</span>
) : null}
</span>
),
}
})}
onChange={(nextKey) => {
const nextStatus = nextKey === 'all' ? '' : nextKey
setStatus(nextStatus)
setPage(1)
}}
/>
<Segmented
block
value={status || 'all'}
options={STATUS_OPTIONS.map((item) => {
const countKey = item.value || 'all'
return {
value: item.value || 'all',
label: `${item.label} (${Number(overviewQuery.data?.[countKey] || 0)})`,
<Space.Compact style={{ width: '100%', maxWidth: 460 }}>
<Input
allowClear
value={keywordInput}
placeholder="搜索工单号 / 订单号 / 商品名"
prefix={<SearchOutlined />}
onChange={(event) => {
const nextValue = event.target.value
setKeywordInput(nextValue)
if (!nextValue.trim()) {
setKeyword('')
setPage(1)
}
})}
onChange={(value) => {
const nextStatus = String(value) === 'all' ? '' : String(value)
setStatus(nextStatus)
setPage(1)
}}
onPressEnter={applyKeywordSearch}
/>
</Space>
<Button
icon={<SearchOutlined />}
loading={ordersQuery.isFetching}
onClick={applyKeywordSearch}
>
</Button>
</Space.Compact>
<Table<WorkOrder>
rowKey="workOrderId"
@@ -417,7 +370,7 @@ export default function WorkerOrdersPage() {
setPageSize(nextPageSize)
},
}}
scroll={{ x: 1160 }}
scroll={{ x: 1060 }}
/>
</Space>
</Card>
@@ -762,13 +715,3 @@ function resolveStatusColor(status: string) {
if (status === 'open') return 'red'
return 'blue'
}
function formatSharingShareStatus(status: string) {
const labels: Record<string, string> = {
joined: '已参与',
submitted: '已提交验收',
accepted: '已验收',
cancelled: '已取消',
}
return labels[status] || status
}
@@ -2,11 +2,9 @@ import {
BankOutlined,
CopyOutlined,
CustomerServiceOutlined,
HistoryOutlined,
LockOutlined,
LogoutOutlined,
ReloadOutlined,
SafetyCertificateOutlined,
WalletOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
@@ -16,7 +14,6 @@ import {
Avatar,
Button,
Card,
Col,
DatePicker,
Descriptions,
Empty,
@@ -26,10 +23,8 @@ import {
InputNumber,
Modal,
Progress,
Row,
Select,
Space,
Statistic,
Table,
Tabs,
Tag,
@@ -80,16 +75,6 @@ type PasswordFormValues = {
confirmPassword?: string
}
type MetricCardItem = {
label: string
value: number
note: string
prefix?: string
suffix?: string
precision?: number
danger?: boolean
}
export default function WorkerProfilePage() {
const { message } = App.useApp()
const navigate = useNavigate()
@@ -510,23 +495,38 @@ export default function WorkerProfilePage() {
</Space>
</Card>
<Row gutter={[16, 16]}>
{buildMetricCards(worker, summary).map((item) => (
<Col key={item.label} xs={24} sm={12} xl={8}>
<Card size="small">
<Statistic
title={item.label}
value={item.value}
prefix={item.prefix}
suffix={item.suffix}
precision={item.precision}
valueStyle={item.danger ? { color: '#cf1322' } : undefined}
/>
<Typography.Text type="secondary">{item.note}</Typography.Text>
</Card>
</Col>
))}
</Row>
<Card size="small" className="worker-profile-wallet-card">
<div className="worker-profile-wallet-strip">
<div className="worker-profile-wallet-item">
<span className="worker-profile-wallet-label"></span>
<strong className="worker-profile-wallet-value">
{formatMoney(worker.wallet.availableAmount)}
</strong>
<small className="worker-profile-wallet-note"></small>
</div>
<div className="worker-profile-wallet-item">
<span className="worker-profile-wallet-label"></span>
<strong className="worker-profile-wallet-value">
{formatMoney(worker.wallet.frozenDepositAmount)}
</strong>
<small className="worker-profile-wallet-note"></small>
</div>
<div className="worker-profile-wallet-item">
<span className="worker-profile-wallet-label"></span>
<strong className="worker-profile-wallet-value">
{formatMoney(worker.wallet.pendingUnfreezeAmount)}
</strong>
<small className="worker-profile-wallet-note"></small>
</div>
<div className="worker-profile-wallet-item">
<span className="worker-profile-wallet-label"></span>
<strong className="worker-profile-wallet-value">
{formatMoney(worker.wallet.totalSettledAmount)}
</strong>
<small className="worker-profile-wallet-note"></small>
</div>
</div>
</Card>
<Card title="快捷操作" size="small">
<Space wrap size={[12, 12]}>
@@ -546,18 +546,6 @@ export default function WorkerProfilePage() {
>
</Button>
<Button
icon={<HistoryOutlined />}
onClick={() => jumpToLedgerSection()}
>
</Button>
<Button
icon={<SafetyCertificateOutlined />}
onClick={() => jumpToRequestSection('withdraw')}
>
</Button>
<Button icon={<LockOutlined />} onClick={() => setPasswordOpen(true)}>
</Button>
@@ -617,6 +605,15 @@ export default function WorkerProfilePage() {
<Descriptions.Item label="已累计完成">
{summary?.acceptedOrderCount || 0}
</Descriptions.Item>
<Descriptions.Item label="超时订单">
{summary?.timeoutOrderCount || 0}
</Descriptions.Item>
<Descriptions.Item label="累计充值">
{formatMoney(worker.wallet.totalCreditedAmount)}
</Descriptions.Item>
<Descriptions.Item label="提现中金额">
{formatMoney(summary?.pendingWithdrawAmount)}
</Descriptions.Item>
<Descriptions.Item label="免押额度">
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
</Descriptions.Item>
@@ -1055,66 +1052,6 @@ export default function WorkerProfilePage() {
)
}
function buildMetricCards(worker: WorkerUser, summary?: WorkerProfileSummary): MetricCardItem[] {
return [
{
label: '账户余额',
value: toYuan(worker.wallet.availableAmount),
note: '可用于抢单冻结或提现申请',
prefix: '¥',
precision: 2,
},
{
label: '冻结押金',
value: toYuan(worker.wallet.frozenDepositAmount),
note: '抢单后冻结,验收通过后 3 天自动解冻',
prefix: '¥',
precision: 2,
},
{
label: '待解冻押金',
value: toYuan(worker.wallet.pendingUnfreezeAmount),
note: '验收通过后待 3 天解冻到账的押金',
prefix: '¥',
precision: 2,
},
{
label: '累计充值',
value: toYuan(worker.wallet.totalCreditedAmount),
note: '管理员已处理的累计充值金额',
prefix: '¥',
precision: 2,
},
{
label: '累计结算',
value: toYuan(worker.wallet.totalSettledAmount),
note: '已经结算入账的报酬金额',
prefix: '¥',
precision: 2,
},
{
label: '提现中金额',
value: toYuan(summary?.pendingWithdrawAmount),
note: '待管理员处理的提现申请金额',
prefix: '¥',
precision: 2,
},
{
label: '累计成单',
value: Number(summary?.acceptedOrderCount || 0),
note: '已完成并验收通过的工单数量',
suffix: '单',
},
{
label: '超时订单',
value: Number(summary?.timeoutOrderCount || 0),
note: '因任务超时被系统判定失败的工单数量',
suffix: '单',
danger: Number(summary?.timeoutOrderCount || 0) > 0,
},
]
}
function resolveAvailableForWithdraw(
worker: WorkerUser | undefined,
summary: WorkerProfileSummary | undefined,
@@ -1137,10 +1074,6 @@ function formatMoney(value: number | undefined) {
return `${((Number(value || 0) || 0) / 100).toFixed(2)}`
}
function toYuan(value: number | undefined) {
return Number(((Number(value || 0) || 0) / 100).toFixed(2))
}
function formatWorkerStatus(status: string) {
if (status === 'active') return '已审核'
if (status === 'pending_review') return '待审核'
+85
View File
@@ -1685,6 +1685,91 @@ select {
margin: 0;
}
.worker-order-summary-card {
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.worker-order-summary-card.active {
border-color: #1677ff;
box-shadow: 0 0 0 1px #1677ff inset;
}
.worker-order-summary-card .ant-statistic-title {
font-size: 12px;
}
.worker-order-tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
margin-left: 6px;
border-radius: 9px;
background: #f0f0f0;
color: #595959;
font-size: 12px;
line-height: 18px;
}
.worker-order-tabs .ant-tabs-nav {
margin-bottom: 12px;
}
.worker-profile-wallet-card .ant-card-body {
padding: 0;
}
.worker-profile-wallet-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0;
overflow: hidden;
}
.worker-profile-wallet-item {
min-width: 0;
padding: 14px 16px;
display: grid;
gap: 4px;
}
.worker-profile-wallet-item + .worker-profile-wallet-item {
border-left: 1px solid #f0f0f0;
}
.worker-profile-wallet-label {
color: #64748b;
font-size: 12px;
}
.worker-profile-wallet-value {
color: #0f172a;
font-size: 20px;
line-height: 1.2;
}
.worker-profile-wallet-note {
color: #94a3b8;
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 768px) {
.worker-profile-wallet-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.worker-profile-wallet-item:nth-child(3) {
border-left: 0;
border-top: 1px solid #f0f0f0;
}
.worker-profile-wallet-item:nth-child(4) {
border-top: 1px solid #f0f0f0;
}
}
.worker-hall-head-copy {
display: grid;
gap: 4px;