精简接单端页面样式

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 '待审核'
+40 -413
View File
@@ -1476,10 +1476,6 @@ select {
display: grid;
}
.worker-profile-hero-head {
flex-direction: column;
}
.worker-hall-toolbar {
width: 100%;
justify-content: flex-start;
@@ -1713,20 +1709,25 @@ select {
.worker-hall-summary-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
background: #ffffff;
}
.worker-hall-summary-item {
min-width: 0;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid #e2e8f0;
background: #f8fafc;
padding: 14px 16px;
display: grid;
gap: 4px;
}
.worker-hall-summary-item + .worker-hall-summary-item {
border-left: 1px solid #e5e7eb;
}
.worker-hall-summary-label {
color: #64748b;
font-size: 12px;
@@ -1762,25 +1763,18 @@ select {
.worker-hall-card {
min-width: 0;
padding: 18px 18px 16px;
border-radius: 16px;
padding: 16px;
border-radius: 14px;
background: #ffffff;
border: 1px solid #edf0f5;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
border: 1px solid #e5e7eb;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
display: grid;
gap: 14px;
}
.worker-hall-card-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.worker-hall-card-title {
display: grid;
gap: 6px;
gap: 4px;
min-width: 0;
}
@@ -1788,65 +1782,32 @@ select {
margin: 0;
}
.worker-hall-card-badges {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
.worker-hall-card-link {
display: block;
max-width: 100%;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.worker-hall-card-amount-row {
display: grid;
gap: 6px;
}
.worker-hall-card-order {
display: grid;
gap: 4px;
}
.worker-hall-card-main {
display: grid;
gap: 8px;
}
.worker-hall-card-amount {
color: #f97316;
font-size: 34px;
font-size: 32px;
line-height: 1;
font-weight: 700;
}
.worker-hall-card-side {
display: grid;
gap: 4px;
color: #94a3b8;
.worker-hall-card-deposit,
.worker-hall-card-hint {
margin: 0;
font-size: 12px;
}
.worker-hall-card-meta {
display: grid;
gap: 10px;
}
.worker-hall-card-meta-row {
display: grid;
gap: 6px;
}
.worker-hall-card-meta-row span {
color: #64748b;
font-size: 12px;
}
.worker-hall-card-meta-row strong {
color: #334155;
font-weight: 500;
line-height: 1.5;
overflow-wrap: anywhere;
}
.worker-hall-card-danger {
color: #dc2626 !important;
}
.worker-hall-card-safe {
color: #16a34a !important;
}
.worker-hall-card-actions {
@@ -1862,332 +1823,6 @@ select {
gap: 16px;
}
.worker-orders-overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.worker-orders-overview-card {
border: 1px solid #e2e8f0;
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.04);
}
.worker-orders-overview-card .ant-card-body {
display: grid;
gap: 8px;
}
.worker-orders-overview-card.total {
background:
linear-gradient(135deg, rgba(20, 50, 74, 0.96), rgba(27, 93, 115, 0.92)),
#14324a;
}
.worker-orders-overview-card.total .worker-orders-overview-label,
.worker-orders-overview-card.total .worker-orders-overview-value,
.worker-orders-overview-card.total .worker-orders-overview-note {
color: #f8fafc;
}
.worker-orders-overview-card.tone-blue {
border-top: 3px solid #2563eb;
}
.worker-orders-overview-card.tone-gold {
border-top: 3px solid #d97706;
}
.worker-orders-overview-card.tone-red {
border-top: 3px solid #dc2626;
}
.worker-orders-overview-card.tone-green {
border-top: 3px solid #059669;
}
.worker-orders-overview-card.tone-slate {
border-top: 3px solid #64748b;
}
.worker-orders-overview-label {
color: #64748b;
font-size: 13px;
}
.worker-orders-overview-value {
color: #0f172a;
font-size: 28px;
line-height: 1.1;
}
.worker-orders-overview-note {
color: #94a3b8;
font-size: 12px;
line-height: 1.5;
}
.worker-orders-table-card .ant-card-body {
display: grid;
gap: 18px;
}
.worker-orders-toolbar {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.worker-orders-search {
width: min(520px, 100%);
}
.worker-orders-status-tabs {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.worker-orders-status-tab {
padding: 10px 14px;
border-radius: 14px;
border: 1px solid #dbe4ee;
background: #f8fafc;
color: #475569;
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
transition:
border-color 0.18s ease,
background 0.18s ease,
color 0.18s ease,
transform 0.18s ease;
}
.worker-orders-status-tab:hover {
transform: translateY(-1px);
border-color: #90b7cf;
}
.worker-orders-status-tab strong {
font-size: 13px;
}
.worker-orders-status-tab.is-active {
border-color: transparent;
background: linear-gradient(135deg, #1b5d73, #237b77);
color: #f8fafc;
box-shadow: 0 10px 24px rgba(27, 93, 115, 0.24);
}
.worker-profile-hero {
overflow: hidden;
border: 1px solid rgba(25, 94, 125, 0.14);
background:
radial-gradient(circle at top right, rgba(116, 227, 214, 0.18), transparent 30%),
linear-gradient(135deg, #14324a 0%, #1b5d73 55%, #237b77 100%);
}
.worker-profile-hero .ant-card-body {
display: grid;
gap: 24px;
}
.worker-profile-hero-head {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: flex-start;
}
.worker-profile-identity {
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
}
.worker-profile-avatar {
width: 72px;
height: 72px;
border-radius: 22px;
display: grid;
place-items: center;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.24);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
color: #ffffff;
font-size: 28px;
font-weight: 700;
}
.worker-profile-identity-copy {
display: grid;
gap: 6px;
min-width: 0;
}
.worker-profile-identity-copy .ant-typography {
color: rgba(239, 248, 255, 0.88);
}
.worker-profile-identity-title {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.worker-profile-identity-title .ant-typography {
margin: 0;
color: #ffffff;
}
.worker-profile-tag {
border-color: transparent;
background: rgba(255, 255, 255, 0.14);
color: #f8fafc;
}
.worker-profile-hero-side {
display: flex;
gap: 24px;
flex-wrap: wrap;
justify-content: flex-end;
}
.worker-profile-hero-side .ant-space {
padding: 10px 14px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.1);
}
.worker-profile-hero-side .ant-typography {
color: rgba(239, 248, 255, 0.88);
}
.worker-profile-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
}
.worker-profile-metric {
display: grid;
gap: 6px;
padding: 16px 18px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.worker-profile-metric-label,
.worker-profile-metric-note {
color: rgba(239, 248, 255, 0.75);
font-size: 12px;
}
.worker-profile-metric-value {
color: #ffffff;
font-size: 26px;
line-height: 1.2;
}
.worker-profile-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.worker-profile-action-card {
width: 100%;
padding: 18px;
border-radius: 18px;
border: 1px solid #e2e8f0;
background: #ffffff;
text-align: left;
display: grid;
gap: 10px;
cursor: pointer;
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
border-color 0.18s ease;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04);
}
.worker-profile-action-card:hover {
transform: translateY(-2px);
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.08);
}
.worker-profile-action-card:disabled {
cursor: not-allowed;
opacity: 0.58;
transform: none;
box-shadow: none;
}
.worker-profile-action-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: grid;
place-items: center;
font-size: 20px;
}
.worker-profile-action-title {
color: #0f172a;
font-size: 16px;
font-weight: 600;
}
.worker-profile-action-description {
color: #64748b;
font-size: 13px;
line-height: 1.6;
}
.worker-profile-action-card.is-blue .worker-profile-action-icon {
background: #dbeafe;
color: #2563eb;
}
.worker-profile-action-card.is-green .worker-profile-action-icon {
background: #dcfce7;
color: #16a34a;
}
.worker-profile-action-card.is-slate .worker-profile-action-icon {
background: #e2e8f0;
color: #334155;
}
.worker-profile-action-card.is-orange .worker-profile-action-icon {
background: #ffedd5;
color: #ea580c;
}
.worker-profile-action-card.is-violet .worker-profile-action-icon {
background: #ede9fe;
color: #7c3aed;
}
.worker-profile-action-card.is-amber .worker-profile-action-icon {
background: #fef3c7;
color: #d97706;
}
.worker-profile-action-card.is-rose .worker-profile-action-icon {
background: #ffe4e6;
color: #e11d48;
}
.worker-amount-positive {
color: #047857;
}
@@ -2253,12 +1888,7 @@ select {
flex-direction: column;
}
.worker-hall-summary-strip {
grid-template-columns: 1fr;
}
.worker-hall-toolbar,
.worker-orders-toolbar {
.worker-hall-toolbar {
align-items: stretch;
flex-direction: column;
}
@@ -2275,23 +1905,20 @@ select {
padding: 16px;
}
.worker-hall-card-head,
.worker-hall-card-actions {
align-items: stretch;
flex-direction: column;
}
.worker-orders-search {
width: 100%;
}
@media (max-width: 480px) {
.worker-hall-summary-strip {
grid-template-columns: 1fr;
}
.worker-orders-status-tab {
flex: 1 1 calc(50% - 10px);
justify-content: space-between;
}
.worker-profile-hero-side {
width: 100%;
justify-content: flex-start;
.worker-hall-summary-item + .worker-hall-summary-item {
border-top: 1px solid #e5e7eb;
border-left: 0;
}
}