feat(worker-platform): 接单平台全平台UI/UX视觉重构与体验升级

1. 全局导航侧边栏 (WorkerLayout):
   - 打造专属品牌Logo标识与实时在线脉冲呼吸灯
   - 打手身份卡片化:展示打手名称、在线状态徽章、会员等级及可用余额药丸徽标
   - 导航菜单选中项增加橙色微光背景与指示条,售后问题单菜单支持待处理红点角标

2. 我的订单管理 (WorkerOrderDesktopList):
   - 桌面端整合头部:标题 + 实时在办监控徽标 + 居中搜索框 + 刷新按钮单行整合
   - 新增4栏工单态势KPI看板:代练进行中、等待验收、异常与问题单、已验收完结,支持点击卡片即时联动筛选
   - 胶囊化状态过滤与结算收益金额说明

3. 售后问题单 (WorkerAfterSalesPage):
   - 桌面端整合头部:待说明紧急红点药丸徽标、分段筛选与刷新按钮

4. 个人中心 (WorkerProfileDesktopSections):
   - 打造全新打手Hero身份卡:大号头像环、等级徽标、UID、手机、邀请码一键复制芯片、会员权益微标签与晋级进度条
   - 4栏立体资产看板:可用余额、冻结押金、待解冻金额、累计结算收益
   - 快捷操作按资金管理、安全与设置、其他服务结构化分组
This commit is contained in:
yml2213
2026-09-12 19:08:10 +08:00
parent 9979011ac2
commit 402c10e9b6
5 changed files with 1361 additions and 204 deletions
+88 -16
View File
@@ -6,12 +6,12 @@ import {
UserOutlined,
} from '@ant-design/icons'
import { App, Button, Layout, Menu, Typography } from 'antd'
import { useQueryClient } from '@tanstack/react-query'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import type { MenuProps } from 'antd'
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router'
import { logoutWorker } from '@/services/worker'
import { fetchWorkerAfterSalesCases, fetchWorkerProfile, logoutWorker } from '@/services/worker'
import { useIsMobile } from '@/utils/use-is-mobile'
import {
clearWorkerSession,
@@ -50,6 +50,45 @@ export default function WorkerLayout() {
const status = getWorkerStatus()
const currentKey = resolveSelectedKey(location.pathname)
const profileQuery = useQuery({
queryKey: ['worker-profile'],
queryFn: () => fetchWorkerProfile(),
retry: false,
staleTime: 10_000,
})
const worker = profileQuery.data?.data.worker
const afterSalesQuery = useQuery({
queryKey: ['worker-after-sales-cases'],
queryFn: fetchWorkerAfterSalesCases,
retry: false,
staleTime: 10_000,
})
const afterSalesPendingCount = (afterSalesQuery.data?.data.items || []).filter(
(item) => item.status === 'awaiting_worker_response',
).length
const dynamicMenuItems: MenuProps['items'] = useMemo(
() => [
{ key: '/hall', icon: <ShoppingOutlined />, label: '抢单大厅' },
{ key: '/orders', icon: <ContainerOutlined />, label: '我的订单' },
{
key: '/orders/after-sales',
icon: <SafetyCertificateOutlined />,
label: (
<span className="worker-menu-label-with-badge">
<span>售后问题单</span>
{afterSalesPendingCount > 0 ? (
<span className="worker-menu-badge">{afterSalesPendingCount}</span>
) : null}
</span>
),
},
{ key: '/profile', icon: <UserOutlined />, label: '个人中心' },
],
[afterSalesPendingCount],
)
useEffect(() => {
const navigateToLogin = () => {
if (window.location.pathname !== '/') {
@@ -170,29 +209,62 @@ export default function WorkerLayout() {
return (
<Layout className="admin-shell worker-shell">
<Layout.Sider width={220} className="admin-sider" trigger={null}>
<Layout.Sider width={230} className="admin-sider worker-sider" trigger={null}>
<div className="admin-sider-inner">
<div className="admin-brand">
<div className="admin-brand-copy">
<strong>王大锤</strong>
<span>接单平台</span>
<div className="worker-sider-brand">
<div className="worker-sider-brand-logo">
<span>锤</span>
</div>
<div className="worker-sider-brand-text">
<strong>王大锤电竞</strong>
<span className="worker-sider-brand-sub">
<span className="worker-sider-pulse-dot" />
打手工作台
</span>
</div>
</div>
<div className="admin-sidebar-profile">
<Typography.Text strong>{username}</Typography.Text>
<Typography.Text type="secondary">{formatWorkerStatus(status)}</Typography.Text>
<div className="worker-sider-profile-card">
<div className="worker-sider-profile-top">
<div className="worker-sider-avatar">
{(worker?.displayName || username).slice(0, 1)}
</div>
<div className="worker-sider-user-meta">
<strong className="worker-sider-username" title={worker?.displayName || username}>
{worker?.displayName || username}
</strong>
<span className="worker-sider-status-badge">● {formatWorkerStatus(status)}</span>
</div>
</div>
{worker?.level?.name ? (
<div className="worker-sider-level-pill">
<span className="worker-sider-level-text">{worker.level.name}</span>
{worker.wallet ? (
<span className="worker-sider-balance-text">
¥{(Number(worker.wallet.availableAmount || 0) / 100).toFixed(2)}
</span>
) : null}
</div>
) : null}
</div>
<Menu
mode="inline"
inlineIndent={16}
inlineIndent={14}
selectedKeys={[currentKey]}
items={menuItems}
className="admin-menu"
items={dynamicMenuItems}
className="worker-menu"
onClick={({ key }) => navigate(String(key))}
/>
<div className="admin-sider-footer">
<div className="worker-sider-footer">
<WorkerNewOrderSpeech />
<Button block icon={<LogoutOutlined />} onClick={submitLogout}>
<Button
block
icon={<LogoutOutlined />}
className="worker-sider-logout-btn"
onClick={confirmLogout}
>
退出登录
</Button>
</div>
@@ -148,32 +148,52 @@ export default function WorkerAfterSalesPage() {
message={`你有 ${pendingCount} 条售后单待提交说明,请尽快处理,逾期可能影响结算与等级`}
/>
) : null}
<div className="worker-after-sales-toolbar">
<Segmented
options={STATUS_FILTERS.map((filter) => ({
value: filter.value,
label: (
<Space size={4}>
{filter.label}
{countByFilter[filter.value] > 0 ? (
<span className="worker-after-sales-filter-count">
{countByFilter[filter.value]}
</span>
) : null}
</Space>
),
}))}
value={statusFilter}
onChange={(value) => setStatusFilter(value as StatusFilter)}
block={isMobile}
/>
<Button
icon={<ReloadOutlined />}
loading={afterSalesCasesQuery.isFetching}
onClick={() => afterSalesCasesQuery.refetch()}
>
刷新
</Button>
<div className="worker-after-sales-desktop-header">
<div className="worker-after-sales-header-left">
<Typography.Title level={3} className="worker-after-sales-heading">
售后问题单
</Typography.Title>
{pendingCount > 0 ? (
<span className="worker-after-sales-alert-pill">
<span className="worker-after-sales-pulse-dot" />
{pendingCount} 条待提交说明
</span>
) : (
<span className="worker-after-sales-normal-pill">无待办售后单</span>
)}
</div>
<div className="worker-after-sales-header-center">
<Segmented
className="worker-after-sales-segmented"
options={STATUS_FILTERS.map((filter) => ({
value: filter.value,
label: (
<Space size={4}>
<span>{filter.label}</span>
{countByFilter[filter.value] > 0 ? (
<span className="worker-after-sales-filter-count">
{countByFilter[filter.value]}
</span>
) : null}
</Space>
),
}))}
value={statusFilter}
onChange={(value) => setStatusFilter(value as StatusFilter)}
block={isMobile}
/>
</div>
<div className="worker-after-sales-header-right">
<Button
icon={<ReloadOutlined />}
loading={afterSalesCasesQuery.isFetching}
onClick={() => afterSalesCasesQuery.refetch()}
>
刷新
</Button>
</div>
</div>
{afterSalesCasesQuery.isLoading ? (
@@ -1,4 +1,11 @@
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import {
CheckCircleOutlined,
ClockCircleOutlined,
ReloadOutlined,
SearchOutlined,
ThunderboltOutlined,
WarningOutlined,
} from '@ant-design/icons'
import { Button, Card, Empty, Input, Space, Table, Tabs, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
@@ -52,16 +59,167 @@ export function WorkerOrderDesktopList({
onKeywordSearch: () => void
onPageChange: (page: number, pageSize: number) => void
}) {
const inProgressCount = Number(overviewByStatus.get('in_progress')?.orderCount || 0)
const inProgressSettlement = Number(overviewByStatus.get('in_progress')?.settlementAmount || 0)
const pendingCount = Number(overviewByStatus.get('pending_acceptance')?.orderCount || 0)
const pendingSettlement = Number(
overviewByStatus.get('pending_acceptance')?.settlementAmount || 0,
)
const problemCount = Number(overviewByStatus.get('problem')?.orderCount || 0)
const openCount = Number(overviewByStatus.get('open')?.orderCount || 0)
const evidenceCount = Number(overviewByStatus.get('vip_evidence_pending')?.orderCount || 0)
const abnormalTotal = problemCount + openCount + evidenceCount
const acceptedCount = Number(overviewByStatus.get('accepted')?.orderCount || 0)
const acceptedSettlement = Number(overviewByStatus.get('accepted')?.settlementAmount || 0)
return (
<section className="page-stack">
<div className="worker-page-head">
<Typography.Title level={3}>我的订单</Typography.Title>
<Button icon={<ReloadOutlined />} loading={refreshing} onClick={onRefresh}>
刷新
</Button>
{/* 桌面端全新整合头部 */}
<div className="worker-orders-desktop-header">
<div className="worker-orders-header-left">
<Typography.Title level={3} className="worker-orders-heading">
我的订单
</Typography.Title>
<span className="worker-orders-live-badge">
<span className="worker-orders-pulse-dot" />
在办工单实时监控
</span>
</div>
<div className="worker-orders-header-center">
<Space.Compact className="worker-orders-desktop-search">
<Input
allowClear
value={keywordInput}
placeholder="搜索订单号 / 商品名 / 游戏ID / 游戏昵称"
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
onChange={(event) => onKeywordInputChange(event.target.value)}
onPressEnter={onKeywordSearch}
/>
<Button
type="primary"
icon={<SearchOutlined />}
loading={searching}
onClick={onKeywordSearch}
>
搜索
</Button>
</Space.Compact>
</div>
<div className="worker-orders-header-right">
<Button icon={<ReloadOutlined />} loading={refreshing} onClick={onRefresh}>
刷新
</Button>
</div>
</div>
<Card bordered={false}>
{/* 4 栏工单态势 KPI 仪表盘 */}
<div className="worker-orders-kpi-grid">
{/* 卡片 1: 代练中 */}
<div
className={`worker-orders-kpi-card ${status === 'in_progress' ? 'active' : ''}`}
onClick={() => onStatusChange(status === 'in_progress' ? '' : 'in_progress')}
>
<div className="worker-orders-kpi-top">
<div className="worker-orders-kpi-icon-wrap in-progress">
<ThunderboltOutlined />
</div>
<div className="worker-orders-kpi-info">
<span className="worker-orders-kpi-title">代练进行中</span>
<strong className="worker-orders-kpi-val">
{inProgressCount}
<span className="worker-orders-kpi-unit"> 单</span>
</strong>
</div>
</div>
<div className="worker-orders-kpi-bottom">
<span className="worker-orders-kpi-sub-label">待结算预估:</span>
<span className="worker-orders-kpi-sub-val">{formatMoney(inProgressSettlement)}</span>
</div>
</div>
{/* 卡片 2: 待验收 */}
<div
className={`worker-orders-kpi-card ${status === 'pending_acceptance' ? 'active' : ''}`}
onClick={() =>
onStatusChange(status === 'pending_acceptance' ? '' : 'pending_acceptance')
}
>
<div className="worker-orders-kpi-top">
<div className="worker-orders-kpi-icon-wrap pending">
<ClockCircleOutlined />
</div>
<div className="worker-orders-kpi-info">
<span className="worker-orders-kpi-title">已提交待验收</span>
<strong className="worker-orders-kpi-val">
{pendingCount}
<span className="worker-orders-kpi-unit"> 单</span>
</strong>
</div>
</div>
<div className="worker-orders-kpi-bottom">
<span className="worker-orders-kpi-sub-label">待审核报酬:</span>
<span className="worker-orders-kpi-sub-val">{formatMoney(pendingSettlement)}</span>
</div>
</div>
{/* 卡片 3: 异常单 */}
<div
className={`worker-orders-kpi-card ${
status === 'problem' || status === 'open' || status === 'vip_evidence_pending'
? 'active'
: ''
}`}
onClick={() => onStatusChange(status === 'problem' ? '' : 'problem')}
>
<div className="worker-orders-kpi-top">
<div className="worker-orders-kpi-icon-wrap problem">
<WarningOutlined />
</div>
<div className="worker-orders-kpi-info">
<span className="worker-orders-kpi-title">异常与问题工单</span>
<strong className="worker-orders-kpi-val">
{abnormalTotal}
<span className="worker-orders-kpi-unit"> 单</span>
</strong>
</div>
</div>
<div className="worker-orders-kpi-bottom">
<span className="worker-orders-kpi-sub-label">
超时 {openCount} · 补图 {evidenceCount} · 问题 {problemCount}
</span>
</div>
</div>
{/* 卡片 4: 已验收 */}
<div
className={`worker-orders-kpi-card ${status === 'accepted' ? 'active' : ''}`}
onClick={() => onStatusChange(status === 'accepted' ? '' : 'accepted')}
>
<div className="worker-orders-kpi-top">
<div className="worker-orders-kpi-icon-wrap accepted">
<CheckCircleOutlined />
</div>
<div className="worker-orders-kpi-info">
<span className="worker-orders-kpi-title">已验收完结</span>
<strong className="worker-orders-kpi-val">
{acceptedCount}
<span className="worker-orders-kpi-unit"> 单</span>
</strong>
</div>
</div>
<div className="worker-orders-kpi-bottom">
<span className="worker-orders-kpi-sub-label">已结算到账:</span>
<span className="worker-orders-kpi-sub-val">{formatMoney(acceptedSettlement)}</span>
</div>
</div>
</div>
<Card bordered={false} className="worker-orders-card">
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Tabs
className="worker-order-tabs"
@@ -88,26 +246,13 @@ export function WorkerOrderDesktopList({
onChange={(nextKey) => onStatusChange(nextKey === 'all' ? '' : nextKey)}
/>
<Space.Compact style={{ width: '100%', maxWidth: 460 }}>
<Input
allowClear
value={keywordInput}
placeholder="搜索订单号 / 商品名 / 游戏ID / 游戏昵称"
prefix={<SearchOutlined />}
onChange={(event) => onKeywordInputChange(event.target.value)}
onPressEnter={onKeywordSearch}
/>
<Button icon={<SearchOutlined />} loading={searching} onClick={onKeywordSearch}>
搜索
</Button>
</Space.Compact>
<Table<WorkOrder>
rowKey="workOrderId"
size="small"
size="middle"
loading={loading}
dataSource={orders}
columns={columns}
className="worker-orders-table"
locale={{
emptyText: error ? (
<Empty description={error instanceof Error ? error.message : '读取我的订单失败'} />
@@ -125,7 +270,7 @@ export function WorkerOrderDesktopList({
total: pagination?.total || 0,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (total) => `共 ${total} 条`,
showTotal: (total) => `共 ${total} 条工单`,
onChange: onPageChange,
}}
scroll={{ x: 1440 }}
@@ -1,14 +1,17 @@
import {
BankOutlined,
ClockCircleOutlined,
CopyOutlined,
CustomerServiceOutlined,
GlobalOutlined,
LockOutlined,
LogoutOutlined,
SafetyCertificateOutlined,
TrophyOutlined,
UserOutlined,
WalletOutlined,
} from '@ant-design/icons'
import { Avatar, Button, Card, Descriptions, Progress, Space, Tag, Tooltip, Typography } from 'antd'
import { Button, Card, Descriptions, Progress, Space, Tag, Tooltip, Typography } from 'antd'
import type {
WorkerLevelProgress,
@@ -17,12 +20,10 @@ import type {
WorkerWithdrawalAccount,
} from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import {
formatMoney,
formatWorkerStatus,
formatWithdrawChannel,
resolveWorkerInitial,
formatWorkerStatus,
resolveWorkerStatusColor,
} from './worker-profile-view-utils'
@@ -35,88 +36,155 @@ export function WorkerProfileDesktopIdentity({
worker: WorkerUser
summary: WorkerProfileSummary | undefined
levelProgress: WorkerLevelProgress | null | undefined
onCopyInviteCode: () => void
onCopyInviteCode?: () => void
}) {
const nextRequired =
levelProgress && levelProgress.nextThreshold
? Math.max(0, levelProgress.nextThreshold - (summary?.acceptedOrderCount || 0))
: 0
return (
<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>
<Tag color={worker.workerType === 'internal' ? 'blue' : 'default'}>
{worker.workerType === 'internal' ? '内部打手' : '外部打手'}
</Tag>
<Tag>{worker.level?.name || '未分级'}</Tag>
</Space>
</Space>
<Space direction="vertical" size={4} style={{ marginTop: 12, width: '100%' }}>
<Space wrap size={[8, 8]}>
<Typography.Text type="secondary">我的邀请码:</Typography.Text>
<Typography.Text strong>{worker.inviteCode || '-'}</Typography.Text>
<Button
size="small"
type="link"
icon={<CopyOutlined />}
disabled={!worker.inviteCode}
onClick={onCopyInviteCode}
>
复制
</Button>
</Space>
{levelProgress && levelProgress.nextThreshold !== null ? (
<Space direction="vertical" size={0} style={{ width: '100%' }}>
<Typography.Text type="secondary">
已累计完成 {summary?.acceptedOrderCount || 0} 单,距下一等级还需{' '}
{Math.max(0, levelProgress.nextThreshold - (summary?.acceptedOrderCount || 0))} 单
</Typography.Text>
<Progress
percent={levelProgress.progressPercent}
size="small"
format={(percent) => `${percent}%`}
/>
</Space>
) : (
<Typography.Text type="secondary">
已累计完成 {summary?.acceptedOrderCount || 0} 单,当前已是最高等级
</Typography.Text>
)}
</Space>
</Card>
<div className="worker-profile-hero-card">
<div className="worker-profile-hero-top">
<div className="worker-profile-hero-user">
<div className="worker-profile-avatar">
{(worker.displayName || worker.username).slice(0, 1)}
</div>
<div className="worker-profile-user-info">
<div className="worker-profile-name-row">
<strong className="worker-profile-name">
{worker.displayName || worker.username}
</strong>
<Tag
color={resolveWorkerStatusColor(worker.status)}
className="worker-profile-status-tag"
>
{formatWorkerStatus(worker.status)}
</Tag>
{worker.level?.name ? (
<span className="worker-profile-level-badge">{worker.level.name}</span>
) : null}
</div>
<div className="worker-profile-meta-row">
<span className="worker-profile-meta-item">账号: {worker.username}</span>
<span className="worker-profile-meta-divider">·</span>
<span className="worker-profile-meta-item">UID: {worker.workerId}</span>
{worker.phone ? (
<>
<span className="worker-profile-meta-divider">·</span>
<span className="worker-profile-meta-item">手机: {worker.phone}</span>
</>
) : null}
{worker.inviteCode ? (
<>
<span className="worker-profile-meta-divider">·</span>
<span className="worker-profile-meta-item">
邀请码: <strong>{worker.inviteCode}</strong>
{onCopyInviteCode ? (
<Button
size="small"
type="link"
icon={<CopyOutlined />}
style={{ padding: '0 4px', height: 'auto' }}
onClick={onCopyInviteCode}
/>
) : null}
</span>
</>
) : null}
</div>
</div>
</div>
<div className="worker-profile-privileges">
<div className="worker-profile-priv-chip">
<span className="priv-label">免押额度</span>
<strong className="priv-val">{formatMoney(worker.effectiveDepositFreeAmount)}</strong>
</div>
<div className="worker-profile-priv-chip">
<span className="priv-label">最大接单</span>
<strong className="priv-val">
{worker.level?.permissions.maxActiveOrders || 0} 单
</strong>
</div>
</div>
</div>
<div className="worker-profile-progress-bar-wrap">
<div className="worker-profile-progress-header">
<span className="worker-profile-progress-title">
已完成 <strong>{summary?.acceptedOrderCount || 0}</strong> 单
</span>
{levelProgress && levelProgress.nextThreshold ? (
<span className="worker-profile-progress-sub">距下一级还需 {nextRequired} 单</span>
) : (
<span className="worker-profile-progress-sub">当前已是最高等级</span>
)}
</div>
<Progress
percent={levelProgress ? levelProgress.progressPercent : 100}
strokeColor={{ '0%': '#ff6a00', '100%': '#ff944d' }}
size={['100%', 8]}
showInfo={false}
/>
</div>
</div>
)
}
export function WorkerProfileDesktopWallet({ worker }: { worker: WorkerUser }) {
const items = [
['可用余额', worker.wallet.availableAmount, '可用于抢单冻结或提现申请'],
['冻结押金', worker.wallet.frozenDepositAmount, '抢单后冻结,验收后进入待解冻'],
[
'待解冻金额',
worker.wallet.pendingUnfreezeAmount,
'验收报酬与押金按配置天数解冻到账,期间不可提现',
],
['累计结算', worker.wallet.totalSettledAmount, '已结算入账的报酬合计'],
] as const
{
key: 'available',
label: '可用余额',
amount: worker.wallet.availableAmount,
note: '可用于抢单冻结与提现',
icon: <WalletOutlined />,
cls: 'kpi-wallet',
},
{
key: 'frozen',
label: '冻结押金',
amount: worker.wallet.frozenDepositAmount,
note: '抢单后暂扣,验收后解冻',
icon: <SafetyCertificateOutlined />,
cls: 'kpi-quota',
},
{
key: 'pending',
label: '待解冻金额',
amount: worker.wallet.pendingUnfreezeAmount,
note: '按配置天数释放到可用余额',
icon: <ClockCircleOutlined />,
cls: 'kpi-tasks',
},
{
key: 'settled',
label: '累计结算收益',
amount: worker.wallet.totalSettledAmount,
note: '已结算入账的报酬合计',
icon: <TrophyOutlined />,
cls: 'kpi-pool',
},
]
return (
<Card size="small" className="worker-profile-wallet-card">
<div className="worker-profile-wallet-strip">
{items.map(([label, amount, note]) => (
<div key={label} className="worker-profile-wallet-item">
<span className="worker-profile-wallet-label">{label}</span>
<strong className="worker-profile-wallet-value">{formatMoney(amount)}</strong>
<small className="worker-profile-wallet-note">{note}</small>
<div className="worker-profile-wallet-grid">
{items.map((item) => (
<div key={item.key} className="worker-profile-wallet-kpi-card">
<div className="worker-profile-kpi-top">
<div className={`worker-profile-kpi-icon ${item.cls}`}>{item.icon}</div>
<div className="worker-profile-kpi-info">
<span className="worker-profile-kpi-label">{item.label}</span>
<strong className="worker-profile-kpi-val">{formatMoney(item.amount)}</strong>
</div>
</div>
))}
</div>
</Card>
<div className="worker-profile-kpi-bottom">
<span className="worker-profile-kpi-note">{item.note}</span>
</div>
</div>
))}
</div>
)
}
@@ -152,60 +220,77 @@ export function WorkerProfileDesktopActions({
onLogout: () => void
}) {
return (
<Card title="快捷操作" size="small">
<Space wrap size={[12, 12]}>
<Button
type="primary"
icon={<WalletOutlined />}
disabled={!canUseFinanceActions}
onClick={onRecharge}
>
充值申请
</Button>
<Button
type="primary"
icon={<BankOutlined />}
disabled={!canUseFinanceActions}
onClick={onWithdraw}
>
提现申请
</Button>
{canAddWithdrawalAccount ? (
<Button
icon={<BankOutlined />}
disabled={!canUseFinanceActions}
onClick={onAddWithdrawalAccount}
>
添加提现信息
</Button>
) : null}
{needsAlipayQrCode ? (
<Button
icon={<BankOutlined />}
disabled={!canUseFinanceActions}
onClick={onAddAlipayQrCode}
>
补充支付宝收款码
</Button>
) : null}
<Tooltip title={renameHint}>
<Button icon={<UserOutlined />} disabled={renameDisabled} onClick={onRename}>
修改昵称
</Button>
</Tooltip>
<Button icon={<LockOutlined />} onClick={onChangePassword}>
修改密码
</Button>
<Button icon={<GlobalOutlined />} onClick={onManageDevices}>
登录设备
</Button>
<Button icon={<CustomerServiceOutlined />} onClick={onContact}>
联系管理员
</Button>
<Button danger icon={<LogoutOutlined />} onClick={onLogout}>
退出登录
</Button>
</Space>
<Card title="快捷操作与服务" size="small" className="worker-profile-actions-card">
<div className="worker-profile-actions-grouped">
<div className="worker-profile-actions-group">
<span className="worker-profile-group-title">资金管理</span>
<Space wrap size={[10, 10]}>
<Button
type="primary"
icon={<WalletOutlined />}
disabled={!canUseFinanceActions}
onClick={onRecharge}
>
充值申请
</Button>
<Button
type="primary"
icon={<BankOutlined />}
disabled={!canUseFinanceActions}
onClick={onWithdraw}
>
提现申请
</Button>
{canAddWithdrawalAccount ? (
<Button
icon={<BankOutlined />}
disabled={!canUseFinanceActions}
onClick={onAddWithdrawalAccount}
>
添加提现信息
</Button>
) : null}
{needsAlipayQrCode ? (
<Button
icon={<BankOutlined />}
disabled={!canUseFinanceActions}
onClick={onAddAlipayQrCode}
>
补充支付宝收款码
</Button>
) : null}
</Space>
</div>
<div className="worker-profile-actions-group">
<span className="worker-profile-group-title">安全与设置</span>
<Space wrap size={[10, 10]}>
<Tooltip title={renameHint}>
<Button icon={<UserOutlined />} disabled={renameDisabled} onClick={onRename}>
修改昵称
</Button>
</Tooltip>
<Button icon={<LockOutlined />} onClick={onChangePassword}>
修改密码
</Button>
<Button icon={<GlobalOutlined />} onClick={onManageDevices}>
登录设备
</Button>
</Space>
</div>
<div className="worker-profile-actions-group">
<span className="worker-profile-group-title">其他服务</span>
<Space wrap size={[10, 10]}>
<Button icon={<CustomerServiceOutlined />} onClick={onContact}>
联系客服
</Button>
<Button danger icon={<LogoutOutlined />} onClick={onLogout}>
退出登录
</Button>
</Space>
</div>
</div>
</Card>
)
}
@@ -226,7 +311,7 @@ export function WorkerProfileDesktopAccountDetails({
onAddWithdrawalAccount: () => void
}) {
return (
<Card size="small">
<Card size="small" className="worker-profile-details-card">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="账号">{worker.username}</Descriptions.Item>
<Descriptions.Item label="昵称">{worker.displayName || '-'}</Descriptions.Item>
@@ -260,17 +345,16 @@ export function WorkerProfileDesktopAccountDetails({
</Descriptions.Item>
<Descriptions.Item label="提现信息" span={2}>
{withdrawalAccounts.length > 0 ? (
<Space direction="vertical" size={2}>
<Space direction="vertical" size={4}>
{withdrawalAccounts.map((account) => (
<Space key={account.accountChannel} wrap size={4}>
<Typography.Text>{formatWithdrawChannel(account.accountChannel)}</Typography.Text>
<Typography.Text>{account.accountName}</Typography.Text>
<Space key={account.accountChannel} wrap size={6}>
<Tag color="blue">{formatWithdrawChannel(account.accountChannel)}</Tag>
<Typography.Text strong>{account.accountName}</Typography.Text>
<Typography.Text type="secondary">
{account.accountChannel === 'wechat'
? '已上传收款二维码'
: `${account.accountNoMasked} / ${account.alipayQrCodeImage ? '已上传收款二维码' : '未上传收款二维码'}`}
? '已上传收款码'
: `${account.accountNoMasked} / ${account.alipayQrCodeImage ? '已上传收款码' : '未上传收款码'}`}
</Typography.Text>
<Typography.Text type="secondary">收款人姓名和账号已锁定</Typography.Text>
</Space>
))}
</Space>
+836
View File
@@ -2890,3 +2890,839 @@ body {
.worker-after-sales-mobile .worker-after-sales-toolbar .ant-segmented {
overflow-x: auto;
}
/* ==========================================================================
接单平台全局视觉升级 (Global Worker Platform UI Upgrade)
========================================================================== */
/* ===== 打手工作台专属侧边栏 ===== */
.worker-shell.admin-shell {
background: #f8fafc;
}
.worker-sider {
background: #ffffff !important;
border-right: 1px solid #e2e8f0 !important;
box-shadow: 2px 0 12px rgba(15, 23, 42, 0.03) !important;
}
.worker-sider-brand {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 16px 12px 16px;
border-bottom: 1px solid #f1f5f9;
}
.worker-sider-brand-logo {
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, #ff6a00 0%, #ff8e3c 100%);
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-weight: 800;
font-size: 18px;
box-shadow: 0 4px 10px rgba(255, 106, 0, 0.28);
flex-shrink: 0;
}
.worker-sider-brand-text {
display: flex;
flex-direction: column;
min-width: 0;
}
.worker-sider-brand-text strong {
font-size: 15px;
font-weight: 700;
color: #0f172a;
line-height: 1.2;
letter-spacing: -0.01em;
}
.worker-sider-brand-sub {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: #059669;
font-weight: 500;
margin-top: 2px;
}
.worker-sider-pulse-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #10b981;
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
animation: worker-hall-pulse 2s infinite cubic-bezier(0.4, 0, 0.6, 1);
}
.worker-sider-profile-card {
margin: 12px 12px 8px 12px;
padding: 10px 12px;
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
border: 1px solid #e2e8f0;
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.worker-sider-profile-top {
display: flex;
align-items: center;
gap: 8px;
}
.worker-sider-avatar {
width: 32px;
height: 32px;
border-radius: 8px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: #ffffff;
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text-transform: uppercase;
}
.worker-sider-user-meta {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.worker-sider-username {
font-size: 13px;
font-weight: 600;
color: #0f172a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.worker-sider-status-badge {
font-size: 10px;
color: #059669;
display: inline-block;
line-height: 1.2;
margin-top: 1px;
}
.worker-sider-level-pill {
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px 8px;
background: #ffffff;
border: 1px solid #fed7aa;
border-radius: 6px;
font-size: 11px;
}
.worker-sider-level-text {
color: #ea580c;
font-weight: 600;
}
.worker-sider-balance-text {
color: #059669;
font-weight: 600;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
/* 侧边栏菜单定制 */
.worker-sider .ant-menu {
background: transparent !important;
border-inline-end: none !important;
padding: 4px 8px;
}
.worker-sider .ant-menu-item {
border-radius: 8px !important;
margin-block: 4px !important;
font-size: 13px !important;
font-weight: 500 !important;
color: #475569 !important;
height: 40px !important;
line-height: 40px !important;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.worker-sider .ant-menu-item:hover {
background: #f1f5f9 !important;
color: #0f172a !important;
}
.worker-sider .ant-menu-item-selected {
background: #fff7ed !important;
color: var(--theme-primary) !important;
font-weight: 600 !important;
box-shadow: inset 3px 0 0 var(--theme-primary);
}
.worker-sider .ant-menu-item-selected .anticon {
color: var(--theme-primary) !important;
}
.worker-menu-label-with-badge {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.worker-menu-badge {
background: #ef4444;
color: #ffffff;
font-size: 11px;
font-weight: 600;
padding: 0 6px;
border-radius: 9999px;
line-height: 16px;
height: 16px;
box-shadow: 0 1px 3px rgba(239, 68, 68, 0.35);
}
.worker-sider-footer {
margin-top: auto;
padding: 12px;
border-top: 1px solid #f1f5f9;
display: flex;
flex-direction: column;
gap: 8px;
}
.worker-sider-logout-btn {
border-color: #fee2e2 !important;
color: #dc2626 !important;
background: #fff5f5 !important;
border-radius: 8px !important;
font-size: 12px !important;
font-weight: 500 !important;
transition: all 0.2s ease !important;
}
.worker-sider-logout-btn:hover {
background: #fee2e2 !important;
border-color: #fca5a5 !important;
}
/* ===== 我的订单 桌面端头部与 4 栏 KPI 看板 ===== */
.worker-orders-desktop-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
background: #ffffff;
padding: 14px 20px;
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
}
.worker-orders-header-left {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.worker-orders-heading.ant-typography {
margin: 0 !important;
font-size: 20px !important;
font-weight: 700 !important;
color: #0f172a !important;
letter-spacing: -0.02em;
}
.worker-orders-live-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: #2563eb;
background: #eff6ff;
border: 1px solid #bfdbfe;
padding: 2px 10px;
border-radius: 9999px;
}
.worker-orders-pulse-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #3b82f6;
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
animation: worker-order-pulse 2s infinite cubic-bezier(0.4, 0, 0.6, 1);
}
@keyframes worker-order-pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 6px rgba(59, 130, 246, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
.worker-orders-header-center {
flex: 1;
max-width: 520px;
min-width: 260px;
}
.worker-orders-desktop-search {
width: 100%;
}
.worker-orders-desktop-search .ant-input {
border-radius: 8px 0 0 8px;
padding: 6px 14px;
border-color: #cbd5e1;
}
.worker-orders-desktop-search .ant-input:hover,
.worker-orders-desktop-search .ant-input:focus {
border-color: var(--theme-primary);
}
.worker-orders-desktop-search .ant-btn {
border-radius: 0 8px 8px 0;
padding: 6px 18px;
height: auto;
font-weight: 500;
}
.worker-orders-header-right {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
/* 订单 4 栏 KPI 网格 */
.worker-orders-kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 16px;
}
@media (max-width: 1200px) {
.worker-orders-kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.worker-orders-kpi-grid {
grid-template-columns: 1fr;
}
}
.worker-orders-kpi-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 14px 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 10px;
cursor: pointer;
user-select: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.03);
}
.worker-orders-kpi-card:hover {
border-color: #cbd5e1;
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
transform: translateY(-2px);
}
.worker-orders-kpi-card.active {
border-color: var(--theme-primary);
background: linear-gradient(180deg, #fffbf7 0%, #ffffff 100%);
box-shadow: 0 4px 14px rgba(255, 106, 0, 0.16);
transform: translateY(-2px);
}
.worker-orders-kpi-top {
display: flex;
align-items: center;
gap: 12px;
}
.worker-orders-kpi-icon-wrap {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.worker-orders-kpi-icon-wrap.in-progress {
background: #eff6ff;
color: #2563eb;
}
.worker-orders-kpi-icon-wrap.pending {
background: #faf5ff;
color: #9333ea;
}
.worker-orders-kpi-icon-wrap.problem {
background: #fef2f2;
color: #dc2626;
}
.worker-orders-kpi-icon-wrap.accepted {
background: #ecfdf5;
color: #059669;
}
.worker-orders-kpi-info {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.worker-orders-kpi-title {
font-size: 12px;
color: #64748b;
font-weight: 500;
}
.worker-orders-kpi-val {
font-size: 20px;
font-weight: 700;
color: #0f172a;
line-height: 1.2;
}
.worker-orders-kpi-unit {
font-size: 12px;
font-weight: 400;
color: #64748b;
margin-left: 2px;
}
.worker-orders-kpi-bottom {
display: flex;
align-items: center;
gap: 6px;
padding-top: 8px;
border-top: 1px solid #f1f5f9;
font-size: 12px;
}
.worker-orders-kpi-sub-label {
color: #64748b;
}
.worker-orders-kpi-sub-val {
color: #0f172a;
font-weight: 600;
}
.worker-orders-card .ant-card-body {
padding: 16px;
}
.worker-orders-table .ant-table-thead > tr > th {
background: #f8fafc;
color: #475569;
font-weight: 600;
font-size: 13px;
}
/* ===== 售后问题单 桌面端头部 ===== */
.worker-after-sales-desktop-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
background: #ffffff;
padding: 14px 20px;
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
}
.worker-after-sales-header-left {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.worker-after-sales-heading.ant-typography {
margin: 0 !important;
font-size: 20px !important;
font-weight: 700 !important;
color: #0f172a !important;
letter-spacing: -0.02em;
}
.worker-after-sales-alert-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
color: #dc2626;
background: #fef2f2;
border: 1px solid #fecaca;
padding: 2px 10px;
border-radius: 9999px;
}
.worker-after-sales-pulse-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #ef4444;
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
animation: worker-after-sales-pulse 2s infinite cubic-bezier(0.4, 0, 0.6, 1);
}
@keyframes worker-after-sales-pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
}
}
.worker-after-sales-normal-pill {
display: inline-flex;
align-items: center;
font-size: 12px;
font-weight: 500;
color: #059669;
background: #ecfdf5;
border: 1px solid #a7f3d0;
padding: 2px 10px;
border-radius: 9999px;
}
.worker-after-sales-header-center {
flex: 1;
max-width: 480px;
}
.worker-after-sales-segmented {
background: #f1f5f9;
padding: 3px;
border-radius: 8px;
}
.worker-after-sales-header-right {
flex-shrink: 0;
}
/* ===== 个人中心 桌面端 Hero 与 钱包网格 ===== */
.worker-profile-hero-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 18px 20px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
display: flex;
flex-direction: column;
gap: 16px;
}
.worker-profile-hero-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
}
.worker-profile-hero-user {
display: flex;
align-items: center;
gap: 14px;
}
.worker-profile-avatar {
width: 52px;
height: 52px;
border-radius: 14px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: #ffffff;
font-size: 22px;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
flex-shrink: 0;
text-transform: uppercase;
}
.worker-profile-user-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.worker-profile-name-row {
display: flex;
align-items: center;
gap: 8px;
}
.worker-profile-name {
font-size: 18px;
font-weight: 700;
color: #0f172a;
}
.worker-profile-status-tag {
margin: 0 !important;
font-size: 11px;
}
.worker-profile-level-badge {
font-size: 11px;
padding: 1px 8px;
border-radius: 9999px;
background: #fff7ed;
color: #c2410c;
border: 1px solid #fed7aa;
font-weight: 600;
}
.worker-profile-meta-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #64748b;
}
.worker-profile-meta-divider {
color: #cbd5e1;
}
.worker-profile-privileges {
display: flex;
align-items: center;
gap: 10px;
}
.worker-profile-priv-chip {
display: flex;
flex-direction: column;
padding: 6px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
.worker-profile-priv-chip .priv-label {
font-size: 11px;
color: #64748b;
}
.worker-profile-priv-chip .priv-val {
font-size: 14px;
font-weight: 700;
color: #0f172a;
}
.worker-profile-progress-bar-wrap {
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 12px;
border-top: 1px solid #f1f5f9;
}
.worker-profile-progress-header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
}
.worker-profile-progress-title {
color: #334155;
}
.worker-profile-progress-sub {
color: #94a3b8;
}
/* 个人中心 4 栏资产网格 */
.worker-profile-wallet-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 16px;
}
@media (max-width: 1200px) {
.worker-profile-wallet-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.worker-profile-wallet-grid {
grid-template-columns: 1fr;
}
}
.worker-profile-wallet-kpi-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 14px 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 10px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.03);
}
.worker-profile-wallet-kpi-card:hover {
border-color: #cbd5e1;
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
transform: translateY(-2px);
}
.worker-profile-kpi-top {
display: flex;
align-items: center;
gap: 12px;
}
.worker-profile-kpi-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.worker-profile-kpi-icon.kpi-wallet {
background: #fff7ed;
color: #ea580c;
}
.worker-profile-kpi-icon.kpi-quota {
background: #eff6ff;
color: #2563eb;
}
.worker-profile-kpi-icon.kpi-tasks {
background: #faf5ff;
color: #9333ea;
}
.worker-profile-kpi-icon.kpi-pool {
background: #ecfdf5;
color: #059669;
}
.worker-profile-kpi-info {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.worker-profile-kpi-label {
font-size: 12px;
color: #64748b;
font-weight: 500;
}
.worker-profile-kpi-val {
font-size: 20px;
font-weight: 700;
color: #0f172a;
line-height: 1.2;
}
.worker-profile-kpi-bottom {
padding-top: 8px;
border-top: 1px solid #f1f5f9;
font-size: 11px;
color: #94a3b8;
}
/* 分组操作卡片 */
.worker-profile-actions-card {
margin-bottom: 16px;
border-radius: 12px;
}
.worker-profile-actions-grouped {
display: flex;
flex-direction: column;
gap: 14px;
}
.worker-profile-actions-group {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 12px;
background: #f8fafc;
border: 1px solid #f1f5f9;
border-radius: 8px;
flex-wrap: wrap;
}
.worker-profile-group-title {
font-size: 12px;
font-weight: 600;
color: #475569;
min-width: 70px;
}
.worker-profile-details-card {
border-radius: 12px;
}