精简接单端页面样式

This commit is contained in:
yml2213
2026-07-28 11:52:41 +08:00
parent 55ffb1e88b
commit b505b6b85e
4 changed files with 519 additions and 965 deletions
@@ -140,7 +140,7 @@ export default function WorkerHallPage() {
{
label: '当前余额',
value: worker ? formatMoney(worker.wallet.availableAmount) : '-',
note: '可用于冻结押金',
note: worker ? `冻结押金 ${formatMoney(worker.wallet.frozenDepositAmount)}` : '可用于抢单冻结',
},
{
label: '接单上限',
@@ -151,17 +151,7 @@ export default function WorkerHallPage() {
note:
remainingSlots === null
? '当前等级待配置'
: `还能再接 ${remainingSlots}`,
},
{
label: '冻结押金',
value: worker ? formatMoney(worker.wallet.frozenDepositAmount) : '-',
note: '当前已占用金额',
},
{
label: '问题单',
value: `${problemCount}`,
note: problemCount ? '建议优先处理' : '当前暂无问题单',
: `待验收 ${pendingAcceptanceCount} 单 / 问题单 ${problemCount}`,
},
]
@@ -257,72 +247,50 @@ export default function WorkerHallPage() {
<>
<div className="worker-hall-card-grid">
{orders.map((order) => {
const requirementFields = getRequirementFields(order)
const disabledReason = resolveGrabDisabledReason(
order,
worker,
remainingSlots,
)
const zeroDeposit = Number(order.freezeDepositAmount || 0) <= 0
return (
<article key={order.workOrderId} className="worker-hall-card">
<div className="worker-hall-card-head">
<div className="worker-hall-card-title">
<Typography.Link onClick={() => setDetailOrder(order)}>
{order.productName || order.workOrderNo}
</Typography.Link>
<Typography.Text type="secondary">
{order.categoryName || '默认分类'}
</Typography.Text>
</div>
<div className="worker-hall-card-badges">
<Tag color={zeroDeposit ? 'green' : 'gold'}>
{zeroDeposit ? '免押金' : '需押金'}
</Tag>
<Tag color="blue">
{formatRequirementShortLabel(requirementFields)}
</Tag>
</div>
</div>
<div className="worker-hall-card-order">
<div className="worker-hall-card-title">
<Typography.Link
className="worker-hall-card-link"
title={order.productName || order.workOrderNo}
onClick={() => setDetailOrder(order)}
>
{order.productName || order.workOrderNo}
</Typography.Link>
<Typography.Text type="secondary">
{order.workOrderNo}
{order.categoryName || '默认分类'}
</Typography.Text>
<Typography.Text type="secondary">
{order.platformOrderId || '-'}
{order.platformOrderId || order.workOrderNo}
</Typography.Text>
</div>
<div className="worker-hall-card-main">
<div className="worker-hall-card-amount-row">
<div className="worker-hall-card-amount">
{formatMoney(order.rewardAmount)}
</div>
<div className="worker-hall-card-side">
<span>{formatMoney(order.freezeDepositAmount)}</span>
<span>{formatDateTime(resolvePublishedTime(order))}</span>
</div>
<Typography.Text
type="secondary"
className="worker-hall-card-deposit"
>
{formatMoney(order.freezeDepositAmount)}
</Typography.Text>
</div>
<div className="worker-hall-card-meta">
<div className="worker-hall-card-meta-row">
<span></span>
<strong>{formatRequirementPreview(requirementFields)}</strong>
</div>
<div className="worker-hall-card-meta-row">
<span></span>
<strong
className={
disabledReason
? 'worker-hall-card-danger'
: 'worker-hall-card-safe'
}
>
{disabledReason || buildGrabReadyHint(order)}
</strong>
</div>
</div>
{disabledReason ? (
<Typography.Text
type="danger"
className="worker-hall-card-hint"
>
{disabledReason}
</Typography.Text>
) : null}
<div className="worker-hall-card-actions">
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(order)}>
@@ -517,24 +485,6 @@ function formatRequirementShortLabel(fields: CollectField[]) {
return `${requiredCount} 项必填 / 共 ${fields.length}`
}
function formatRequirementPreview(fields: CollectField[]) {
if (fields.length === 0) {
return '当前工单未配置额外资料字段'
}
const preview = fields
.slice(0, 3)
.map((field) => field.label)
.filter(Boolean)
.join('、')
if (fields.length <= 3) {
return preview
}
return `${preview}${fields.length}`
}
function resolvePublishedTime(order: WorkOrder) {
return order.publishedAt || order.createdAt || order.updatedAt
}
@@ -9,13 +9,17 @@ import {
App,
Button,
Card,
Col,
Descriptions,
Drawer,
Empty,
Form,
Input,
Modal,
Row,
Segmented,
Space,
Statistic,
Table,
Tag,
Typography,
@@ -38,37 +42,13 @@ const STATUS_OPTIONS = [
{ value: 'cancelled', label: '已取消' },
] as const
const STATUS_OVERVIEW = [
{
key: 'in_progress',
label: '代练中',
note: '正在执行中的订单',
tone: 'blue',
},
{
key: 'pending_acceptance',
label: '待验收',
note: '已经提交,等待审核',
tone: 'gold',
},
{
key: 'problem',
label: '问题单',
note: '需要补充或重新处理',
tone: 'red',
},
{
key: 'accepted',
label: '已验收',
note: '已完成并结算的订单',
tone: 'green',
},
{
key: 'cancelled',
label: '已取消',
note: '已结束且不再继续',
tone: 'slate',
},
const STATUS_SUMMARY_ITEMS = [
{ key: 'all', label: '全部订单', note: '当前账号历史接单总数' },
{ key: 'in_progress', label: '代练中', note: '正在执行中的订单' },
{ key: 'pending_acceptance', label: '待验收', note: '已提交,等待审核' },
{ key: 'problem', label: '问题单', note: '需要补充或重新处理' },
{ key: 'accepted', label: '已验收', note: '已完成并结算的订单' },
{ key: 'cancelled', label: '已取消', note: '已结束且不再继续' },
] as const
type AcceptanceFormValues = {
@@ -248,7 +228,11 @@ export default function WorkerOrdersPage() {
</Button>
{canSubmitAcceptance(row) ? (
<Button icon={<CheckOutlined />} type="primary" onClick={() => openAcceptanceModal(row)}>
<Button
icon={<CheckOutlined />}
type="primary"
onClick={() => openAcceptanceModal(row)}
>
</Button>
) : null}
@@ -270,119 +254,114 @@ export default function WorkerOrdersPage() {
</Button>
</div>
<div className="worker-orders-overview-grid">
<Card className="worker-orders-overview-card total" bordered={false}>
<span className="worker-orders-overview-label"></span>
<strong className="worker-orders-overview-value">
{Number(overviewQuery.data?.all || 0)}
</strong>
<span className="worker-orders-overview-note"></span>
</Card>
{STATUS_OVERVIEW.map((item) => (
<Card
key={item.key}
className={`worker-orders-overview-card tone-${item.tone}`}
bordered={false}
>
<span className="worker-orders-overview-label">{item.label}</span>
<strong className="worker-orders-overview-value">
{Number(overviewQuery.data?.[item.key] || 0)}
</strong>
<span className="worker-orders-overview-note">{item.note}</span>
</Card>
<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>
))}
</div>
</Row>
<Card bordered={false} className="worker-orders-table-card">
<div className="worker-orders-toolbar">
<Space.Compact className="worker-orders-search">
<Input
allowClear
value={keywordInput}
placeholder="搜索工单号 / 订单号 / 商品名"
prefix={<SearchOutlined />}
onChange={(event) => {
const nextValue = event.target.value
setKeywordInput(nextValue)
if (!nextValue.trim()) {
setKeyword('')
setPage(1)
<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>
<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)})`,
}
})}
onChange={(value) => {
const nextStatus = String(value) === 'all' ? '' : String(value)
setStatus(nextStatus)
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>
</div>
</Space>
<div className="worker-orders-status-tabs">
{STATUS_OPTIONS.map((item) => {
const active = status === item.value
const countKey = item.value || 'all'
return (
<button
key={item.value || 'all'}
type="button"
className={`worker-orders-status-tab${active ? ' is-active' : ''}`}
onClick={() => {
setStatus(item.value)
setPage(1)
}}
>
<span>{item.label}</span>
<strong>{Number(overviewQuery.data?.[countKey] || 0)}</strong>
</button>
)
})}
</div>
<Table<WorkOrder>
rowKey="workOrderId"
loading={ordersQuery.isLoading}
dataSource={orders}
columns={columns}
locale={{
emptyText: ordersQuery.error ? (
<Empty
description={
ordersQuery.error instanceof Error
? ordersQuery.error.message
: '读取我的订单失败'
}
/>
) : (
<Empty
description={
keyword
? `没有找到与 “${keyword}” 相关的订单`
: '当前筛选下暂无订单'
}
/>
),
}}
pagination={{
current: pagination?.page || page,
pageSize: pagination?.pageSize || pageSize,
total: pagination?.total || 0,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (total) => `${total}`,
onChange: (nextPage, nextPageSize) => {
setPage(nextPage)
setPageSize(nextPageSize)
},
}}
scroll={{ x: 1160 }}
/>
<Table<WorkOrder>
rowKey="workOrderId"
size="small"
loading={ordersQuery.isLoading}
dataSource={orders}
columns={columns}
locale={{
emptyText: ordersQuery.error ? (
<Empty
description={
ordersQuery.error instanceof Error
? ordersQuery.error.message
: '读取我的订单失败'
}
/>
) : (
<Empty
description={
keyword
? `没有找到与 “${keyword}” 相关的订单`
: '当前筛选下暂无订单'
}
/>
),
}}
pagination={{
current: pagination?.page || page,
pageSize: pagination?.pageSize || pageSize,
total: pagination?.total || 0,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (total) => `${total}`,
onChange: (nextPage, nextPageSize) => {
setPage(nextPage)
setPageSize(nextPageSize)
},
}}
scroll={{ x: 1160 }}
/>
</Space>
</Card>
<Drawer
@@ -402,7 +381,11 @@ export default function WorkerOrdersPage() {
{formatStatus(detailOrder.status)}
</Tag>
{canSubmitAcceptance(detailOrder) ? (
<Button type="primary" icon={<CheckOutlined />} onClick={() => openAcceptanceModal(detailOrder)}>
<Button
type="primary"
icon={<CheckOutlined />}
onClick={() => openAcceptanceModal(detailOrder)}
>
</Button>
) : null}
@@ -12,22 +12,27 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
App,
Avatar,
Button,
Card,
Col,
Descriptions,
Empty,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
Space,
Statistic,
Table,
Tabs,
Tag,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useRef, useState, type ReactNode, type RefObject } from 'react'
import { useState } from 'react'
import { useNavigate } from 'react-router'
import {
@@ -39,13 +44,14 @@ import {
fetchWorkerWalletLedgers,
logoutWorker,
} from '@/services/worker'
import { clearWorkerSession } from '@/utils/worker-auth'
import type {
WorkerFinanceRequest,
WorkerProfileSummary,
WorkerUser,
WorkerWalletLedger,
} from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import { clearWorkerSession } from '@/utils/worker-auth'
type RechargeFormValues = {
amount?: number
@@ -66,13 +72,21 @@ type PasswordFormValues = {
confirmPassword?: string
}
type MetricCardItem = {
label: string
value: number
note: string
prefix?: string
suffix?: string
precision?: number
}
export default function WorkerProfilePage() {
const { message } = App.useApp()
const navigate = useNavigate()
const queryClient = useQueryClient()
const ledgerSectionRef = useRef<HTMLDivElement | null>(null)
const requestSectionRef = useRef<HTMLDivElement | null>(null)
const [activeTab, setActiveTab] = useState('account')
const [ledgerPage, setLedgerPage] = useState(1)
const [ledgerPageSize, setLedgerPageSize] = useState(10)
const [ledgerType, setLedgerType] = useState('')
@@ -152,21 +166,17 @@ export default function WorkerProfilePage() {
}
}
function scrollToSection(ref: RefObject<HTMLDivElement | null>) {
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
function jumpToLedgerSection(nextLedgerType = '') {
setLedgerType(nextLedgerType)
setLedgerPage(1)
scrollToSection(ledgerSectionRef)
setActiveTab('ledger')
}
function jumpToRequestSection(nextRequestType = '', nextStatus = '') {
setRequestType(nextRequestType)
setRequestStatus(nextStatus)
setRequestPage(1)
scrollToSection(requestSectionRef)
setActiveTab('requests')
}
async function submitRecharge(values: RechargeFormValues) {
@@ -232,7 +242,7 @@ export default function WorkerProfilePage() {
title: '时间',
dataIndex: 'createdAt',
width: 180,
render: (value) => String(value || '-'),
render: (value) => formatDateTime(String(value || '')),
},
{
title: '类型',
@@ -287,7 +297,7 @@ export default function WorkerProfilePage() {
title: '申请时间',
dataIndex: 'createdAt',
width: 180,
render: (value) => String(value || '-'),
render: (value) => formatDateTime(String(value || '')),
},
{
title: '类型',
@@ -385,7 +395,7 @@ export default function WorkerProfilePage() {
{!worker && !profileQuery.isLoading ? <Empty description="暂无打手信息" /> : null}
{worker ? (
<>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
{!canUseFinanceActions ? (
<Alert
showIcon
@@ -394,285 +404,280 @@ export default function WorkerProfilePage() {
/>
) : null}
<Card className="worker-profile-hero" bordered={false}>
<div className="worker-profile-hero-head">
<div className="worker-profile-identity">
<div className="worker-profile-avatar">
{String(worker.displayName || worker.username || '打')
.trim()
.slice(0, 1)
.toUpperCase()}
</div>
<div className="worker-profile-identity-copy">
<div className="worker-profile-identity-title">
<Typography.Title level={4}>
{worker.displayName || worker.username}
</Typography.Title>
<Space size={[6, 6]} wrap>
<Tag className="worker-profile-tag">
{worker.level?.name || '未分级'}
</Tag>
<Tag color={resolveWorkerStatusColor(worker.status)}>
{formatWorkerStatus(worker.status)}
</Tag>
</Space>
</div>
<Typography.Text>
{worker.username}
</Typography.Text>
<Typography.Text>
{worker.phone || '-'}
</Typography.Text>
</div>
</div>
<div className="worker-profile-hero-side">
<Space direction="vertical" size={4}>
<Typography.Text></Typography.Text>
<Typography.Text strong>
{formatMoney(worker.wallet.totalCreditedAmount)}
</Typography.Text>
</Space>
<Space direction="vertical" size={4}>
<Typography.Text></Typography.Text>
<Typography.Text strong>
{formatMoney(worker.wallet.totalSettledAmount)}
</Typography.Text>
</Space>
</div>
</div>
<div className="worker-profile-metrics">
{buildMetricCards(worker, summary).map((item) => (
<div key={item.label} className="worker-profile-metric">
<span className="worker-profile-metric-label">{item.label}</span>
<strong className="worker-profile-metric-value">{item.value}</strong>
<span className="worker-profile-metric-note">{item.note}</span>
</div>
))}
</div>
</Card>
<div className="worker-profile-actions">
<ProfileActionCard
icon={<WalletOutlined />}
title="充值申请"
description="线下充值后提交金额与备注,等待管理员入账。"
tone="blue"
disabled={!canUseFinanceActions}
onClick={() => setRechargeOpen(true)}
/>
<ProfileActionCard
icon={<BankOutlined />}
title="提现申请"
description="填写收款账号并发起提现,系统会保留申请记录。"
tone="green"
disabled={!canUseFinanceActions}
onClick={() => setWithdrawOpen(true)}
/>
<ProfileActionCard
icon={<HistoryOutlined />}
title="资金流水"
description="查看充值、押金冻结、结算奖励等钱包变动明细。"
tone="slate"
onClick={() => jumpToLedgerSection()}
/>
<ProfileActionCard
icon={<SafetyCertificateOutlined />}
title="提现记录"
description="快速查看提现申请状态、审核备注和收款信息。"
tone="orange"
onClick={() => jumpToRequestSection('withdraw')}
/>
<ProfileActionCard
icon={<LockOutlined />}
title="修改密码"
description="更新接单账号密码,修改后会要求重新登录。"
tone="violet"
onClick={() => setPasswordOpen(true)}
/>
<ProfileActionCard
icon={<CustomerServiceOutlined />}
title="联系管理员"
description="查看处理说明和当前账号资料,沟通充值、提现或审核问题。"
tone="amber"
onClick={() => setContactOpen(true)}
/>
<ProfileActionCard
icon={<LogoutOutlined />}
title="退出登录"
description="当前设备退出接单端登录,会清理本地登录态。"
tone="rose"
onClick={submitLogout}
/>
</div>
<Card title="账号信息" bordered={false}>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="账号">{worker.username}</Descriptions.Item>
<Descriptions.Item label="昵称">{worker.displayName}</Descriptions.Item>
<Descriptions.Item label="手机号">{worker.phone || '-'}</Descriptions.Item>
<Descriptions.Item label="审核状态">
<Card size="small">
<Space size={16} wrap align="start">
<Avatar size={64}>
{resolveWorkerInitial(worker)}
</Avatar>
<Space direction="vertical" size={2}>
<Typography.Title level={4} style={{ margin: 0 }}>
{worker.displayName || worker.username}
</Typography.Title>
<Typography.Text type="secondary">
{worker.username}
</Typography.Text>
<Typography.Text type="secondary">
{worker.phone || '-'}
</Typography.Text>
</Space>
<Space size={[8, 8]} wrap>
<Tag color={resolveWorkerStatusColor(worker.status)}>
{formatWorkerStatus(worker.status)}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="当前等级">
{worker.level?.name || '-'}
</Descriptions.Item>
<Descriptions.Item label="免押额度">
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
</Descriptions.Item>
<Descriptions.Item label="最大同时接单">
{worker.level?.permissions.maxActiveOrders || 0}
</Descriptions.Item>
<Descriptions.Item label="审核备注">
{worker.reviewNote || '-'}
</Descriptions.Item>
<Descriptions.Item label="注册时间">
{worker.createdAt || '-'}
</Descriptions.Item>
<Descriptions.Item label="最近更新时间">
{worker.updatedAt || '-'}
</Descriptions.Item>
</Descriptions>
<Tag>{worker.level?.name || '未分级'}</Tag>
</Space>
</Space>
</Card>
<div ref={ledgerSectionRef}>
<Card
title="资金流水"
bordered={false}
extra={
<Space>
<Select
value={ledgerType}
style={{ width: 170 }}
options={[
{ value: '', label: '全部流水' },
{ value: 'manual_credit', label: '人工充值' },
{ value: 'deposit_freeze', label: '冻结押金' },
{ value: 'deposit_release', label: '释放押金' },
{ value: 'deposit_deduction', label: '扣除押金' },
{ value: 'reward_settlement', label: '结算报酬' },
]}
onChange={(nextType) => {
setLedgerType(nextType)
setLedgerPage(1)
}}
<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}
/>
<Button
icon={<ReloadOutlined />}
loading={ledgersQuery.isFetching}
onClick={() => ledgersQuery.refetch()}
>
</Button>
</Space>
}
>
<Table<WorkerWalletLedger>
rowKey="ledgerId"
size="small"
loading={ledgersQuery.isLoading}
dataSource={ledgersQuery.data?.data.items || []}
columns={ledgerColumns}
locale={{
emptyText: ledgersQuery.error
? ledgersQuery.error instanceof Error
? ledgersQuery.error.message
: '读取钱包流水失败'
: '暂无钱包流水',
}}
pagination={{
current: ledgersQuery.data?.data.pagination.page || ledgerPage,
pageSize: ledgersQuery.data?.data.pagination.pageSize || ledgerPageSize,
total: ledgersQuery.data?.data.pagination.total || 0,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50],
showTotal: (total) => `${total}`,
onChange: (nextPage, nextPageSize) => {
setLedgerPage(nextPage)
setLedgerPageSize(nextPageSize)
},
}}
scroll={{ x: 920 }}
/>
</Card>
</div>
<Typography.Text type="secondary">{item.note}</Typography.Text>
</Card>
</Col>
))}
</Row>
<div ref={requestSectionRef}>
<Card
title="申请记录"
bordered={false}
extra={
<Space>
<Select
value={requestType}
style={{ width: 140 }}
options={[
{ value: '', label: '全部类型' },
{ value: 'recharge', label: '充值申请' },
{ value: 'withdraw', label: '提现申请' },
]}
onChange={(nextType) => {
setRequestType(nextType)
setRequestPage(1)
}}
/>
<Select
value={requestStatus}
style={{ width: 140 }}
options={[
{ value: '', label: '全部状态' },
{ value: 'pending', label: '待处理' },
{ value: 'approved', label: '已通过' },
{ value: 'rejected', label: '已拒绝' },
{ value: 'cancelled', label: '已取消' },
]}
onChange={(nextStatus) => {
setRequestStatus(nextStatus)
setRequestPage(1)
}}
/>
<Button
icon={<ReloadOutlined />}
loading={requestsQuery.isFetching}
onClick={() => requestsQuery.refetch()}
<Card title="快捷操作" size="small">
<Space wrap size={[12, 12]}>
<Button
type="primary"
icon={<WalletOutlined />}
disabled={!canUseFinanceActions}
onClick={() => setRechargeOpen(true)}
>
</Button>
<Button
type="primary"
icon={<BankOutlined />}
disabled={!canUseFinanceActions}
onClick={() => setWithdrawOpen(true)}
>
</Button>
<Button
icon={<HistoryOutlined />}
onClick={() => jumpToLedgerSection()}
>
</Button>
<Button
icon={<SafetyCertificateOutlined />}
onClick={() => jumpToRequestSection('withdraw')}
>
</Button>
<Button icon={<LockOutlined />} onClick={() => setPasswordOpen(true)}>
</Button>
<Button
icon={<CustomerServiceOutlined />}
onClick={() => setContactOpen(true)}
>
</Button>
<Button danger icon={<LogoutOutlined />} onClick={submitLogout}>
退
</Button>
</Space>
</Card>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: 'account',
label: '账号信息',
children: (
<Card size="small">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="账号">{worker.username}</Descriptions.Item>
<Descriptions.Item label="昵称">
{worker.displayName || '-'}
</Descriptions.Item>
<Descriptions.Item label="手机号">
{worker.phone || '-'}
</Descriptions.Item>
<Descriptions.Item label="审核状态">
<Tag color={resolveWorkerStatusColor(worker.status)}>
{formatWorkerStatus(worker.status)}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="当前等级">
{worker.level?.name || '-'}
</Descriptions.Item>
<Descriptions.Item label="免押额度">
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
</Descriptions.Item>
<Descriptions.Item label="最大同时接单">
{worker.level?.permissions.maxActiveOrders || 0}
</Descriptions.Item>
<Descriptions.Item label="审核备注">
{worker.reviewNote || '-'}
</Descriptions.Item>
<Descriptions.Item label="注册时间">
{formatDateTime(worker.createdAt)}
</Descriptions.Item>
<Descriptions.Item label="最近更新时间">
{formatDateTime(worker.updatedAt)}
</Descriptions.Item>
</Descriptions>
</Card>
),
},
{
key: 'ledger',
label: '资金流水',
children: (
<Card
size="small"
extra={
<Space wrap>
<Select
value={ledgerType}
style={{ width: 170 }}
options={[
{ value: '', label: '全部流水' },
{ value: 'manual_credit', label: '人工充值' },
{ value: 'deposit_freeze', label: '冻结押金' },
{ value: 'deposit_release', label: '释放押金' },
{ value: 'deposit_deduction', label: '扣除押金' },
{ value: 'reward_settlement', label: '结算报酬' },
]}
onChange={(nextType) => {
setLedgerType(nextType)
setLedgerPage(1)
}}
/>
<Button
icon={<ReloadOutlined />}
loading={ledgersQuery.isFetching}
onClick={() => ledgersQuery.refetch()}
>
</Button>
</Space>
}
>
</Button>
</Space>
}
>
<Table<WorkerFinanceRequest>
rowKey="requestId"
size="small"
loading={requestsQuery.isLoading}
dataSource={requestsQuery.data?.data.items || []}
columns={requestColumns}
locale={{
emptyText: requestsQuery.error
? requestsQuery.error instanceof Error
? requestsQuery.error.message
: '读取申请记录失败'
: '暂无申请记录',
}}
pagination={{
current: requestsQuery.data?.data.pagination.page || requestPage,
pageSize: requestsQuery.data?.data.pagination.pageSize || requestPageSize,
total: requestsQuery.data?.data.pagination.total || 0,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50],
showTotal: (total) => `${total}`,
onChange: (nextPage, nextPageSize) => {
setRequestPage(nextPage)
setRequestPageSize(nextPageSize)
},
}}
scroll={{ x: 980 }}
/>
</Card>
</div>
</>
<Table<WorkerWalletLedger>
rowKey="ledgerId"
size="small"
loading={ledgersQuery.isLoading}
dataSource={ledgersQuery.data?.data.items || []}
columns={ledgerColumns}
locale={{
emptyText: ledgersQuery.error
? ledgersQuery.error instanceof Error
? ledgersQuery.error.message
: '读取钱包流水失败'
: '暂无钱包流水',
}}
pagination={{
current: ledgersQuery.data?.data.pagination.page || ledgerPage,
pageSize: ledgersQuery.data?.data.pagination.pageSize || ledgerPageSize,
total: ledgersQuery.data?.data.pagination.total || 0,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50],
showTotal: (total) => `${total}`,
onChange: (nextPage, nextPageSize) => {
setLedgerPage(nextPage)
setLedgerPageSize(nextPageSize)
},
}}
scroll={{ x: 920 }}
/>
</Card>
),
},
{
key: 'requests',
label: '申请记录',
children: (
<Card
size="small"
extra={
<Space wrap>
<Select
value={requestType}
style={{ width: 140 }}
options={[
{ value: '', label: '全部类型' },
{ value: 'recharge', label: '充值申请' },
{ value: 'withdraw', label: '提现申请' },
]}
onChange={(nextType) => {
setRequestType(nextType)
setRequestPage(1)
}}
/>
<Select
value={requestStatus}
style={{ width: 140 }}
options={[
{ value: '', label: '全部状态' },
{ value: 'pending', label: '待处理' },
{ value: 'approved', label: '已通过' },
{ value: 'rejected', label: '已拒绝' },
{ value: 'cancelled', label: '已取消' },
]}
onChange={(nextStatus) => {
setRequestStatus(nextStatus)
setRequestPage(1)
}}
/>
<Button
icon={<ReloadOutlined />}
loading={requestsQuery.isFetching}
onClick={() => requestsQuery.refetch()}
>
</Button>
</Space>
}
>
<Table<WorkerFinanceRequest>
rowKey="requestId"
size="small"
loading={requestsQuery.isLoading}
dataSource={requestsQuery.data?.data.items || []}
columns={requestColumns}
locale={{
emptyText: requestsQuery.error
? requestsQuery.error instanceof Error
? requestsQuery.error.message
: '读取申请记录失败'
: '暂无申请记录',
}}
pagination={{
current: requestsQuery.data?.data.pagination.page || requestPage,
pageSize: requestsQuery.data?.data.pagination.pageSize || requestPageSize,
total: requestsQuery.data?.data.pagination.total || 0,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50],
showTotal: (total) => `${total}`,
onChange: (nextPage, nextPageSize) => {
setRequestPage(nextPage)
setRequestPageSize(nextPageSize)
},
}}
scroll={{ x: 980 }}
/>
</Card>
),
},
]}
/>
</Space>
) : null}
<Modal
@@ -697,12 +702,7 @@ export default function WorkerProfilePage() {
name="amount"
rules={[{ required: true, message: '请输入充值金额' }]}
>
<InputNumber
min={0.01}
step={1}
addonAfter="元"
className="full-width"
/>
<InputNumber min={0.01} step={1} addonAfter="元" className="full-width" />
</Form.Item>
<Form.Item label="备注说明" name="note">
<Input.TextArea
@@ -740,12 +740,7 @@ export default function WorkerProfilePage() {
name="amount"
rules={[{ required: true, message: '请输入提现金额' }]}
>
<InputNumber
min={0.01}
step={1}
addonAfter="元"
className="full-width"
/>
<InputNumber min={0.01} step={1} addonAfter="元" className="full-width" />
</Form.Item>
<Form.Item
label="提现方式"
@@ -794,11 +789,7 @@ export default function WorkerProfilePage() {
}}
onOk={() => passwordForm.submit()}
>
<Form
form={passwordForm}
layout="vertical"
onFinish={submitPasswordChange}
>
<Form form={passwordForm} layout="vertical" onFinish={submitPasswordChange}>
<Form.Item
label="当前密码"
name="currentPassword"
@@ -880,56 +871,48 @@ export default function WorkerProfilePage() {
)
}
function ProfileActionCard({
icon,
title,
description,
tone,
disabled = false,
onClick,
}: {
icon: ReactNode
title: string
description: string
tone: 'blue' | 'green' | 'slate' | 'orange' | 'violet' | 'amber' | 'rose'
disabled?: boolean
onClick: () => void
}) {
return (
<button
type="button"
disabled={disabled}
className={`worker-profile-action-card is-${tone}`}
onClick={onClick}
>
<span className="worker-profile-action-icon">{icon}</span>
<span className="worker-profile-action-title">{title}</span>
<span className="worker-profile-action-description">{description}</span>
</button>
)
}
function buildMetricCards(worker: WorkerUser, summary?: WorkerProfileSummary) {
function buildMetricCards(worker: WorkerUser, summary?: WorkerProfileSummary): MetricCardItem[] {
return [
{
label: '账户余额',
value: formatMoney(worker.wallet.availableAmount),
value: toYuan(worker.wallet.availableAmount),
note: '可用于抢单冻结或提现申请',
prefix: '¥',
precision: 2,
},
{
label: '冻结押金',
value: formatMoney(worker.wallet.frozenDepositAmount),
value: toYuan(worker.wallet.frozenDepositAmount),
note: '抢单后冻结,验收通过会自动释放',
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: formatMoney(summary?.pendingWithdrawAmount),
value: toYuan(summary?.pendingWithdrawAmount),
note: '待管理员处理的提现申请金额',
prefix: '¥',
precision: 2,
},
{
label: '累计成单',
value: `${Number(summary?.acceptedOrderCount || 0)}`,
value: Number(summary?.acceptedOrderCount || 0),
note: '已完成并验收通过的工单数量',
suffix: '单',
},
]
}
@@ -945,10 +928,21 @@ function resolveAvailableForWithdraw(
)
}
function resolveWorkerInitial(worker: WorkerUser | undefined) {
return String(worker?.displayName || worker?.username || '打')
.trim()
.slice(0, 1)
.toUpperCase()
}
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 '待审核'