feat(worker-platform): 接单平台全平台UI/UX视觉重构与体验升级
1. 全局导航侧边栏 (WorkerLayout): - 打造专属品牌Logo标识与实时在线脉冲呼吸灯 - 打手身份卡片化:展示打手名称、在线状态徽章、会员等级及可用余额药丸徽标 - 导航菜单选中项增加橙色微光背景与指示条,售后问题单菜单支持待处理红点角标 2. 我的订单管理 (WorkerOrderDesktopList): - 桌面端整合头部:标题 + 实时在办监控徽标 + 居中搜索框 + 刷新按钮单行整合 - 新增4栏工单态势KPI看板:代练进行中、等待验收、异常与问题单、已验收完结,支持点击卡片即时联动筛选 - 胶囊化状态过滤与结算收益金额说明 3. 售后问题单 (WorkerAfterSalesPage): - 桌面端整合头部:待说明紧急红点药丸徽标、分段筛选与刷新按钮 4. 个人中心 (WorkerProfileDesktopSections): - 打造全新打手Hero身份卡:大号头像环、等级徽标、UID、手机、邀请码一键复制芯片、会员权益微标签与晋级进度条 - 4栏立体资产看板:可用余额、冻结押金、待解冻金额、累计结算收益 - 快捷操作按资金管理、安全与设置、其他服务结构化分组
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user