初步优化手机端ui
This commit is contained in:
@@ -4,11 +4,12 @@ import {
|
||||
ShoppingOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { App, Button, Layout, Menu, Space, Typography } from 'antd'
|
||||
import { App, Button, Layout, Menu, Space, Tag, Typography } from 'antd'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router'
|
||||
|
||||
import { logoutWorker } from '@/services/worker'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
import { clearWorkerSession, getWorkerStatus, getWorkerUsername } from '@/utils/worker-auth'
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
@@ -17,12 +18,20 @@ const menuItems: MenuProps['items'] = [
|
||||
{ key: '/worker/profile', icon: <UserOutlined />, label: '个人中心' },
|
||||
]
|
||||
|
||||
const tabItems = [
|
||||
{ key: '/worker/hall', icon: ShoppingOutlined, label: '抢单大厅' },
|
||||
{ key: '/worker/orders', icon: ContainerOutlined, label: '我的订单' },
|
||||
{ key: '/worker/profile', icon: UserOutlined, label: '个人中心' },
|
||||
]
|
||||
|
||||
export default function WorkerLayout() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { message } = App.useApp()
|
||||
const isMobile = useIsMobile()
|
||||
const { message, modal } = App.useApp()
|
||||
const username = getWorkerUsername() || '打手'
|
||||
const status = getWorkerStatus()
|
||||
const currentKey = resolveSelectedKey(location.pathname)
|
||||
|
||||
async function submitLogout() {
|
||||
try {
|
||||
@@ -36,6 +45,72 @@ export default function WorkerLayout() {
|
||||
}
|
||||
}
|
||||
|
||||
function confirmLogout() {
|
||||
modal.confirm({
|
||||
title: '确认退出登录',
|
||||
content: '退出后需要重新登录才能继续抢单和处理订单。',
|
||||
okText: '确认退出',
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: submitLogout,
|
||||
})
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="worker-mobile-shell">
|
||||
<header className="worker-mobile-header">
|
||||
<div className="worker-mobile-brand">
|
||||
<span className="worker-mobile-brand-badge">锤</span>
|
||||
<div className="worker-mobile-brand-text">
|
||||
<strong>王大锤接单</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="worker-mobile-user-area">
|
||||
<Tag color={resolveStatusTagColor(status)} className="worker-mobile-status-tag">
|
||||
{formatWorkerStatus(status)}
|
||||
</Tag>
|
||||
<span className="worker-mobile-username" title={username}>
|
||||
{username}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<LogoutOutlined />}
|
||||
className="worker-mobile-logout-btn"
|
||||
onClick={confirmLogout}
|
||||
title="退出登录"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="worker-mobile-content">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<nav className="worker-mobile-tabbar" aria-label="移动端底部导航">
|
||||
{tabItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isActive = currentKey === item.key
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={`worker-mobile-tab-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => navigate(item.key)}
|
||||
>
|
||||
<div className="worker-mobile-tab-icon">
|
||||
<Icon />
|
||||
</div>
|
||||
<span className="worker-mobile-tab-label">{item.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout className="admin-shell worker-shell">
|
||||
<Layout.Sider width={220} className="admin-sider" trigger={null}>
|
||||
@@ -53,7 +128,7 @@ export default function WorkerLayout() {
|
||||
<Menu
|
||||
mode="inline"
|
||||
inlineIndent={16}
|
||||
selectedKeys={[resolveSelectedKey(location.pathname)]}
|
||||
selectedKeys={[currentKey]}
|
||||
items={menuItems}
|
||||
className="admin-menu"
|
||||
onClick={({ key }) => navigate(String(key))}
|
||||
@@ -68,7 +143,8 @@ export default function WorkerLayout() {
|
||||
<Layout className="admin-main-shell">
|
||||
<Layout.Header className="admin-topbar">
|
||||
<Space className="admin-user" size={12}>
|
||||
<Typography.Text type="secondary">{formatWorkerStatus(status)}</Typography.Text>
|
||||
<Tag color={resolveStatusTagColor(status)}>{formatWorkerStatus(status)}</Tag>
|
||||
<Typography.Text strong>{username}</Typography.Text>
|
||||
</Space>
|
||||
</Layout.Header>
|
||||
<Layout.Content className="admin-content">
|
||||
@@ -92,3 +168,11 @@ function formatWorkerStatus(status: string) {
|
||||
if (status === 'disabled') return '已停用'
|
||||
return '未登录'
|
||||
}
|
||||
|
||||
function resolveStatusTagColor(status: string) {
|
||||
if (status === 'active') return 'success'
|
||||
if (status === 'pending_review') return 'warning'
|
||||
if (status === 'rejected') return 'error'
|
||||
if (status === 'disabled') return 'default'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClockCircleOutlined, EyeOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { ClockCircleOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Empty,
|
||||
Input,
|
||||
InputNumber,
|
||||
@@ -29,8 +28,8 @@ import {
|
||||
grabWorkerOrder,
|
||||
joinWorkerSharingOrder,
|
||||
} from '@/services/worker'
|
||||
import type { CollectField, WorkOrder, WorkerUser } from '@/types/worker-platform'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import type { WorkOrder, WorkerUser } from '@/types/worker-platform'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
|
||||
type HallSummary = {
|
||||
pendingAcceptanceCount: number
|
||||
@@ -38,6 +37,7 @@ type HallSummary = {
|
||||
}
|
||||
|
||||
export default function WorkerHallPage() {
|
||||
const isMobile = useIsMobile()
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [keywordInput, setKeywordInput] = useState('')
|
||||
@@ -46,7 +46,6 @@ export default function WorkerHallPage() {
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [grabbingId, setGrabbingId] = useState(0)
|
||||
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
||||
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
||||
const [sharingQuantity, setSharingQuantity] = useState(1)
|
||||
const [submittingSharing, setSubmittingSharing] = useState(false)
|
||||
@@ -118,9 +117,6 @@ export default function WorkerHallPage() {
|
||||
maxActiveOrders > 0 ? Math.max(0, maxActiveOrders - activeCount) : null
|
||||
const shouldWarn =
|
||||
(remainingSlots !== null && remainingSlots <= 0) || problemCount > 0
|
||||
const detailDisabledReason = detailOrder
|
||||
? resolveGrabDisabledReason(detailOrder, worker, remainingSlots)
|
||||
: ''
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([
|
||||
@@ -137,7 +133,6 @@ export default function WorkerHallPage() {
|
||||
try {
|
||||
await grabWorkerOrder(order.workOrderId)
|
||||
message.success('抢单成功,已进入我的订单')
|
||||
setDetailOrder(null)
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '抢单失败')
|
||||
@@ -160,7 +155,6 @@ export default function WorkerHallPage() {
|
||||
})
|
||||
message.success('拼单成功,已进入我的订单')
|
||||
setSharingOrder(null)
|
||||
setDetailOrder(null)
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '拼单失败')
|
||||
@@ -211,32 +205,40 @@ export default function WorkerHallPage() {
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="worker-page-head">
|
||||
<Typography.Title level={3}>抢单大厅</Typography.Title>
|
||||
<Space wrap className="worker-hall-toolbar">
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={
|
||||
ordersQuery.isFetching ||
|
||||
profileQuery.isFetching ||
|
||||
hallSummaryQuery.isFetching
|
||||
}
|
||||
onClick={() => refreshAll()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Tooltip
|
||||
title={
|
||||
autoRefresh
|
||||
? '已开启自动刷新,每 5 秒刷新一次,点击关闭'
|
||||
: '开启后每 5 秒自动刷新订单列表'
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={autoRefresh}
|
||||
onChange={toggleAutoRefresh}
|
||||
/>
|
||||
</Tooltip>
|
||||
<div className="worker-page-head-title-row">
|
||||
<Typography.Title level={3}>抢单大厅</Typography.Title>
|
||||
<div className="worker-hall-head-controls">
|
||||
<Tooltip
|
||||
title={
|
||||
autoRefresh
|
||||
? '已开启自动刷新,每 5 秒刷新一次'
|
||||
: '开启后每 5 秒自动刷新订单列表'
|
||||
}
|
||||
>
|
||||
<div className="worker-hall-autorefresh-box">
|
||||
<span className="worker-hall-autorefresh-label">自动刷新</span>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={autoRefresh}
|
||||
onChange={toggleAutoRefresh}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={
|
||||
ordersQuery.isFetching ||
|
||||
profileQuery.isFetching ||
|
||||
hallSummaryQuery.isFetching
|
||||
}
|
||||
onClick={() => refreshAll()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="worker-hall-search-bar">
|
||||
<Space.Compact className="worker-hall-search">
|
||||
<Input
|
||||
allowClear
|
||||
@@ -262,7 +264,7 @@ export default function WorkerHallPage() {
|
||||
搜索
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="worker-hall-board-card" bordered={false}>
|
||||
@@ -340,13 +342,12 @@ export default function WorkerHallPage() {
|
||||
return (
|
||||
<article key={order.workOrderId} className="worker-hall-card">
|
||||
<div className="worker-hall-card-title">
|
||||
<Typography.Link
|
||||
className="worker-hall-card-link"
|
||||
<strong
|
||||
className="worker-hall-card-title-text"
|
||||
title={order.productName || '未命名商品'}
|
||||
onClick={() => setDetailOrder(order)}
|
||||
>
|
||||
{order.productName || '未命名商品'}
|
||||
</Typography.Link>
|
||||
</strong>
|
||||
<Typography.Text type="secondary">
|
||||
{order.categoryName || '默认分类'}
|
||||
</Typography.Text>
|
||||
@@ -397,16 +398,6 @@ export default function WorkerHallPage() {
|
||||
) : null}
|
||||
|
||||
<div className="worker-hall-card-actions">
|
||||
<div className="worker-hall-card-actions-row">
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => setDetailOrder(order)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
</div>
|
||||
<div className="worker-hall-card-grab-row">
|
||||
{order.sharing?.enabled && !disabledReason ? (
|
||||
<Button
|
||||
@@ -443,9 +434,10 @@ export default function WorkerHallPage() {
|
||||
current={pagination?.page || page}
|
||||
pageSize={pagination?.pageSize || pageSize}
|
||||
total={pagination?.total || 0}
|
||||
showSizeChanger
|
||||
showSizeChanger={!isMobile}
|
||||
simple={isMobile}
|
||||
pageSizeOptions={[20, 50, 100]}
|
||||
showTotal={(total) => `共 ${total} 条`}
|
||||
showTotal={isMobile ? undefined : (total) => `共 ${total} 条`}
|
||||
onChange={(nextPage, nextPageSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
@@ -457,87 +449,9 @@ export default function WorkerHallPage() {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
detailOrder
|
||||
? `${detailOrder.productName || '工单详情'} · ${getDisplayOrderNo(detailOrder)}`
|
||||
: '工单详情'
|
||||
}
|
||||
width={760}
|
||||
open={Boolean(detailOrder)}
|
||||
destroyOnHidden
|
||||
onClose={() => setDetailOrder(null)}
|
||||
extra={
|
||||
detailOrder ? (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={Boolean(detailDisabledReason)}
|
||||
loading={grabbingId === detailOrder.workOrderId}
|
||||
onClick={() => submitGrab(detailOrder)}
|
||||
>
|
||||
立即抢单
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{detailOrder ? (
|
||||
<div className="worker-hall-detail-stack">
|
||||
<Alert
|
||||
showIcon
|
||||
type={detailDisabledReason ? 'warning' : 'success'}
|
||||
message={detailDisabledReason || buildGrabReadyHint(detailOrder)}
|
||||
/>
|
||||
|
||||
<Card title="基础信息" size="small">
|
||||
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
|
||||
<Descriptions.Item label="商品">
|
||||
{detailOrder.productName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">
|
||||
{detailOrder.categoryName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台订单号">
|
||||
{getDisplayOrderNo(detailOrder)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="接单奖励">
|
||||
{formatMoney(detailOrder.rewardAmount)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="冻结押金">
|
||||
{formatMoney(detailOrder.freezeDepositAmount)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="任务时限">
|
||||
{Number(detailOrder.timeoutMinutes || 0) > 0 ? (
|
||||
<Tag color="orange" icon={<ClockCircleOutlined />}>
|
||||
限时 {detailOrder.timeoutMinutes} 分钟
|
||||
</Tag>
|
||||
) : (
|
||||
'不限时'
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="发布时间">
|
||||
{formatDateTime(resolvePublishedTime(detailOrder))}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">
|
||||
{formatDateTime(detailOrder.updatedAt)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="资料要求">
|
||||
{formatRequirementShortLabel(getRequirementFields(detailOrder))}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="接单资格">
|
||||
{detailDisabledReason || '当前可直接抢单'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card title="资料要求" size="small">
|
||||
{renderRequirementFieldValues(detailOrder)}
|
||||
</Card>
|
||||
</div>
|
||||
) : null}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title={`拼单参与 · ${sharingOrder?.productName || ''}`}
|
||||
width={isMobile ? '92%' : 520}
|
||||
open={Boolean(sharingOrder)}
|
||||
confirmLoading={submittingSharing}
|
||||
onCancel={() => setSharingOrder(null)}
|
||||
@@ -581,47 +495,6 @@ export default function WorkerHallPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function getRequirementFields(order: WorkOrder | null): CollectField[] {
|
||||
if (!order) return []
|
||||
const rawFields = Array.isArray(order.requirement?.fields)
|
||||
? order.requirement.fields
|
||||
: []
|
||||
return rawFields
|
||||
.map((item) => {
|
||||
const source = asRecord(item)
|
||||
const key = String(source.key || '').trim()
|
||||
if (!key) return null
|
||||
return {
|
||||
key,
|
||||
label: String(source.label || key).trim(),
|
||||
required: source.required !== false,
|
||||
}
|
||||
})
|
||||
.filter((item): item is CollectField => Boolean(item))
|
||||
}
|
||||
|
||||
function renderRequirementFieldValues(order: WorkOrder) {
|
||||
const fields = getRequirementFields(order)
|
||||
if (fields.length === 0) {
|
||||
return <Typography.Text type="secondary">当前工单未配置资料要求</Typography.Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
{fields.map((field) => (
|
||||
<Descriptions.Item key={field.key} label={field.label}>
|
||||
<Space size={6} wrap>
|
||||
<Typography.Text code>{field.key}</Typography.Text>
|
||||
<Tag color={field.required ? 'orange' : 'default'}>
|
||||
{field.required ? '必填' : '选填'}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
)
|
||||
}
|
||||
|
||||
function resolveGrabDisabledReason(
|
||||
order: WorkOrder,
|
||||
worker: WorkerUser | undefined,
|
||||
@@ -644,36 +517,10 @@ function resolveGrabDisabledReason(
|
||||
return ''
|
||||
}
|
||||
|
||||
function buildGrabReadyHint(order: WorkOrder) {
|
||||
const freezeAmount = Number(order.freezeDepositAmount || 0)
|
||||
if (freezeAmount <= 0) {
|
||||
return '当前工单无需冻结押金,可直接抢单'
|
||||
}
|
||||
return `抢单后会冻结 ${formatMoney(freezeAmount)}`
|
||||
}
|
||||
|
||||
function formatRequirementShortLabel(fields: CollectField[]) {
|
||||
if (fields.length === 0) {
|
||||
return '资料简单'
|
||||
}
|
||||
const requiredCount = fields.filter((field) => field.required).length
|
||||
return `${requiredCount} 项必填 / 共 ${fields.length} 项`
|
||||
}
|
||||
|
||||
function resolvePublishedTime(order: WorkOrder) {
|
||||
return order.publishedAt || order.createdAt || order.updatedAt
|
||||
}
|
||||
|
||||
function getDisplayOrderNo(order: Pick<WorkOrder, 'platformOrderId'> | null | undefined): string {
|
||||
return String(order?.platformOrderId || '').trim() || '-'
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
const AUTO_REFRESH_STORAGE_KEY = 'worker-hall-auto-refresh'
|
||||
|
||||
function loadAutoRefreshPreference(): boolean {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
AutoComplete,
|
||||
App,
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
@@ -18,7 +19,9 @@ import {
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
@@ -45,6 +48,7 @@ import type {
|
||||
} from '@/types/worker-platform'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { getImageIdentity } from '@/utils/image-identity'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部订单' },
|
||||
@@ -72,6 +76,7 @@ type DetailFieldItem = {
|
||||
}
|
||||
|
||||
export default function WorkerOrdersPage() {
|
||||
const isMobile = useIsMobile()
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [status, setStatus] = useState('')
|
||||
@@ -449,50 +454,42 @@ export default function WorkerOrdersPage() {
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="worker-page-head">
|
||||
<Typography.Title level={3}>我的订单</Typography.Title>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ordersQuery.isFetching || overviewQuery.isFetching}
|
||||
onClick={() => refreshAll()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card bordered={false}>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Tabs
|
||||
activeKey={status || 'all'}
|
||||
items={STATUS_OPTIONS.map((item) => {
|
||||
<>
|
||||
{isMobile ? (
|
||||
<div className="worker-orders-mobile-page">
|
||||
{/* 顶部水平滑动状态切换胶囊条 */}
|
||||
<div className="worker-orders-mobile-status-bar">
|
||||
{STATUS_OPTIONS.map((item) => {
|
||||
const countKey = item.value || 'all'
|
||||
const count = Number(overviewQuery.data?.[countKey] || 0)
|
||||
return {
|
||||
key: item.value || 'all',
|
||||
label: (
|
||||
<span>
|
||||
{item.label}
|
||||
{count > 0 ? (
|
||||
<span className="worker-order-tab-count">{count}</span>
|
||||
) : null}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
const isActive = (status || '') === item.value
|
||||
return (
|
||||
<button
|
||||
key={item.value || 'all'}
|
||||
type="button"
|
||||
className={`worker-orders-status-pill ${isActive ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setStatus(item.value)
|
||||
setPage(1)
|
||||
}}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
{count > 0 ? (
|
||||
<span className="worker-orders-status-pill-count">{count}</span>
|
||||
) : null}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
onChange={(nextKey) => {
|
||||
const nextStatus = nextKey === 'all' ? '' : nextKey
|
||||
setStatus(nextStatus)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 460 }}>
|
||||
{/* 搜索与刷新栏 */}
|
||||
<div className="worker-orders-mobile-search-row">
|
||||
<Input
|
||||
allowClear
|
||||
value={keywordInput}
|
||||
placeholder="搜索订单号 / 商品名"
|
||||
prefix={<SearchOutlined />}
|
||||
className="worker-orders-mobile-search-input"
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value
|
||||
setKeywordInput(nextValue)
|
||||
@@ -504,55 +501,361 @@ export default function WorkerOrdersPage() {
|
||||
onPressEnter={applyKeywordSearch}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
loading={ordersQuery.isFetching}
|
||||
onClick={applyKeywordSearch}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ordersQuery.isFetching || overviewQuery.isFetching}
|
||||
onClick={() => refreshAll()}
|
||||
title="刷新"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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
|
||||
: '读取我的订单失败'
|
||||
}
|
||||
{/* 移动端紧凑卡片列表 */}
|
||||
<div className="worker-orders-mobile-list">
|
||||
{ordersQuery.isLoading ? (
|
||||
<div className="worker-hall-loading">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : ordersQuery.error ? (
|
||||
<Empty
|
||||
description={
|
||||
ordersQuery.error instanceof Error
|
||||
? ordersQuery.error.message
|
||||
: '读取我的订单失败'
|
||||
}
|
||||
/>
|
||||
) : orders.length === 0 ? (
|
||||
<Empty
|
||||
description={
|
||||
keyword
|
||||
? `没有找到与 “${keyword}” 相关的订单`
|
||||
: '当前筛选下暂无订单'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
orders.map((order) => {
|
||||
const items = getMaterialDetailItems(order).filter((item) =>
|
||||
String(item.value || '').trim(),
|
||||
)
|
||||
const screenshots = getMaterialScreenshots(order)
|
||||
const canCopyInfo = getCopyOrderInfoItems(order).length > 0
|
||||
const submittedAt = getAcceptanceSubmittedAt(order)
|
||||
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order))
|
||||
const hasAcceptance = acceptanceFiles.length > 0
|
||||
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
|
||||
const draftCount = draftFiles.length
|
||||
const noteText = getWorkerAcceptance(order)?.note
|
||||
const workerNote = String(order.workerNote || '').trim()
|
||||
|
||||
return (
|
||||
<article key={order.workOrderId} className="worker-order-mobile-card">
|
||||
{/* 卡片顶栏:分类 + 订单号 + 状态/倒计时 */}
|
||||
<div className="worker-order-mobile-header">
|
||||
<div className="worker-order-mobile-header-left">
|
||||
{order.categoryName ? (
|
||||
<Tag color="blue" className="worker-order-mobile-tag">
|
||||
{order.categoryName}
|
||||
</Tag>
|
||||
) : null}
|
||||
{order.myShare ? (
|
||||
<Tag color="purple" className="worker-order-mobile-tag">
|
||||
拼单
|
||||
</Tag>
|
||||
) : null}
|
||||
<span className="worker-order-mobile-orderno-text">
|
||||
{getDisplayOrderNo(order)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="worker-order-mobile-header-right">
|
||||
<Tag color={resolveStatusColor(order.status)}>
|
||||
{formatStatus(order.status)}
|
||||
</Tag>
|
||||
{order.status === 'in_progress' && order.deadlineAt ? (
|
||||
<DeadlineCountdown deadlineAt={order.deadlineAt} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 商品标题与金额 */}
|
||||
<div className="worker-order-mobile-main-row">
|
||||
<div className="worker-order-mobile-product-title">
|
||||
{order.productName || '未命名商品'}
|
||||
</div>
|
||||
<div className="worker-order-mobile-price-badge">
|
||||
<span className="worker-order-mobile-price-label">报酬</span>
|
||||
<strong className="worker-order-mobile-price-num">
|
||||
{formatMoney(order.rewardAmount)}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 辅助金额与接单时间 */}
|
||||
<div className="worker-order-mobile-sub-row">
|
||||
<span className="worker-order-mobile-sub-item">
|
||||
押金: {formatMoney(order.freezeDepositAmount)}
|
||||
</span>
|
||||
{order.myShare ? (
|
||||
<span className="worker-order-mobile-sub-item">
|
||||
已拼 {order.myShare.quantity} 份 ({formatMoney(order.myShare.shareReward)})
|
||||
</span>
|
||||
) : null}
|
||||
<span className="worker-order-mobile-sub-time">
|
||||
{formatDateTime(order.assignedAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 问题单提示 */}
|
||||
{order.status === 'problem' && String(order.problemNote || '').trim() ? (
|
||||
<div className="worker-order-mobile-problem-box">
|
||||
<strong>问题:</strong>
|
||||
<span>{order.problemNote}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 买家资料与截图 */}
|
||||
{(items.length > 0 || screenshots.length > 0) && (
|
||||
<div className="worker-order-mobile-credentials-box">
|
||||
{items.length > 0 ? (
|
||||
<div className="worker-order-mobile-credentials-grid">
|
||||
{items.map((item) => (
|
||||
<div key={item.key} className="worker-order-mobile-credential-line">
|
||||
<span className="worker-order-mobile-credential-label">{item.label}:</span>
|
||||
<span className="worker-order-mobile-credential-val">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{screenshots.length > 0 ? (
|
||||
<div className="worker-order-mobile-screenshots-row">
|
||||
<span className="worker-order-mobile-credential-label">截图:</span>
|
||||
<ImagePreviewList files={screenshots} size={34} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="worker-order-mobile-copy-toolbar">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
disabled={!canCopyInfo}
|
||||
onClick={() => copyOrderInfo(order)}
|
||||
>
|
||||
复制资料
|
||||
</Button>
|
||||
{screenshots.length > 0 ? (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PictureOutlined />}
|
||||
onClick={() => copyMaterialImages(order)}
|
||||
>
|
||||
复制图片
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 验收凭据与暂存状态 */}
|
||||
{(hasAcceptance || (draftCount > 0 && !hasSubmittedAcceptance(order)) || noteText) && (
|
||||
<div className="worker-order-mobile-acceptance-strip">
|
||||
{hasAcceptance ? (
|
||||
<div className="worker-order-mobile-acceptance-files">
|
||||
<span className="worker-order-mobile-acceptance-tag">
|
||||
已提交凭据 ({acceptanceFiles.length}张)
|
||||
</span>
|
||||
<ImagePreviewList files={acceptanceFiles} size={32} maxVisible={3} />
|
||||
</div>
|
||||
) : null}
|
||||
{draftCount > 0 && !hasSubmittedAcceptance(order) ? (
|
||||
<div className="worker-order-mobile-draft-files">
|
||||
<Tag color="processing" icon={<PictureOutlined />}>
|
||||
已暂存 {draftCount} 张图片
|
||||
</Tag>
|
||||
<ImagePreviewList files={draftFiles} size={32} maxVisible={3} />
|
||||
</div>
|
||||
) : null}
|
||||
{noteText ? (
|
||||
<div className="worker-order-mobile-acceptance-text">
|
||||
说明:{noteText}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部备注与操作 */}
|
||||
<div className="worker-order-mobile-footer-row">
|
||||
<div className="worker-order-mobile-note-block">
|
||||
{workerNote ? (
|
||||
<div className="worker-note-badge" onClick={() => openNoteModal(order)}>
|
||||
<span className="worker-note-text" title={workerNote}>
|
||||
{workerNote}
|
||||
</span>
|
||||
<EditOutlined className="worker-note-edit-icon" />
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="worker-order-mobile-add-note-btn"
|
||||
onClick={() => openNoteModal(order)}
|
||||
>
|
||||
<EditOutlined /> 添加备注
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="worker-order-mobile-btn-group">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => setDetailOrder(order)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
{canEditAcceptanceImages(order) ? (
|
||||
<Button
|
||||
size="small"
|
||||
type={canSubmitAcceptance(order) ? 'primary' : 'default'}
|
||||
icon={<PictureOutlined />}
|
||||
className="worker-order-mobile-action-submit"
|
||||
onClick={() => openAcceptanceModal(order)}
|
||||
>
|
||||
{hasSubmittedAcceptance(order) ? '补充图片' : '提交验收'}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})
|
||||
)}
|
||||
|
||||
{(pagination?.total || 0) > 0 ? (
|
||||
<div className="worker-orders-mobile-pagination">
|
||||
<Pagination
|
||||
current={pagination?.page || page}
|
||||
pageSize={pagination?.pageSize || pageSize}
|
||||
total={pagination?.total || 0}
|
||||
simple
|
||||
onChange={(nextPage: number, nextPageSize: number) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty
|
||||
description={
|
||||
keyword
|
||||
? `没有找到与 “${keyword}” 相关的订单`
|
||||
: '当前筛选下暂无订单'
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<section className="page-stack">
|
||||
<div className="worker-page-head">
|
||||
<Typography.Title level={3}>我的订单</Typography.Title>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ordersQuery.isFetching || overviewQuery.isFetching}
|
||||
onClick={() => refreshAll()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card bordered={false}>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Tabs
|
||||
activeKey={status || 'all'}
|
||||
items={STATUS_OPTIONS.map((item) => {
|
||||
const countKey = item.value || 'all'
|
||||
const count = Number(overviewQuery.data?.[countKey] || 0)
|
||||
return {
|
||||
key: item.value || 'all',
|
||||
label: (
|
||||
<span>
|
||||
{item.label}
|
||||
{count > 0 ? (
|
||||
<span className="worker-order-tab-count">{count}</span>
|
||||
) : null}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
})}
|
||||
onChange={(nextKey) => {
|
||||
const nextStatus = nextKey === 'all' ? '' : nextKey
|
||||
setStatus(nextStatus)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 460 }}>
|
||||
<Input
|
||||
allowClear
|
||||
value={keywordInput}
|
||||
placeholder="搜索订单号 / 商品名"
|
||||
prefix={<SearchOutlined />}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value
|
||||
setKeywordInput(nextValue)
|
||||
if (!nextValue.trim()) {
|
||||
setKeyword('')
|
||||
setPage(1)
|
||||
}
|
||||
}}
|
||||
onPressEnter={applyKeywordSearch}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
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: 1440 }}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
loading={ordersQuery.isFetching}
|
||||
onClick={applyKeywordSearch}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
|
||||
<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: number, nextPageSize: number) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
},
|
||||
}}
|
||||
scroll={{ x: 1440 }}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
@@ -560,7 +863,7 @@ export default function WorkerOrdersPage() {
|
||||
? `${detailOrder.productName || '订单详情'} · ${getDisplayOrderNo(detailOrder)}`
|
||||
: '订单详情'
|
||||
}
|
||||
width={820}
|
||||
width={isMobile ? '100%' : 820}
|
||||
open={Boolean(detailOrder)}
|
||||
destroyOnHidden
|
||||
onClose={() => setDetailOrder(null)}
|
||||
@@ -698,7 +1001,7 @@ export default function WorkerOrdersPage() {
|
||||
<Modal
|
||||
title={`订单备注 · ${getDisplayOrderNo(noteOrder)}`}
|
||||
open={Boolean(noteOrder)}
|
||||
width={460}
|
||||
width={isMobile ? '92%' : 460}
|
||||
onCancel={() => {
|
||||
setNoteOrder(null)
|
||||
noteForm.resetFields()
|
||||
@@ -740,6 +1043,7 @@ export default function WorkerOrdersPage() {
|
||||
: '验收图片'
|
||||
}
|
||||
open={Boolean(editingOrder)}
|
||||
width={isMobile ? '96%' : 620}
|
||||
onCancel={() => {
|
||||
setEditingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
@@ -789,7 +1093,7 @@ export default function WorkerOrdersPage() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,11 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Pagination,
|
||||
Progress,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
@@ -52,6 +54,7 @@ import type {
|
||||
WorkerWalletLedger,
|
||||
} from '@/types/worker-platform'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
import { clearWorkerSession } from '@/utils/worker-auth'
|
||||
|
||||
type RechargeFormValues = {
|
||||
@@ -76,6 +79,7 @@ type PasswordFormValues = {
|
||||
}
|
||||
|
||||
export default function WorkerProfilePage() {
|
||||
const isMobile = useIsMobile()
|
||||
const { message } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -673,33 +677,93 @@ export default function WorkerProfilePage() {
|
||||
</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 }}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<div className="worker-profile-mobile-records">
|
||||
{ledgersQuery.isLoading ? (
|
||||
<div className="worker-hall-loading"><Spin size="large" /></div>
|
||||
) : (ledgersQuery.data?.data.items || []).length === 0 ? (
|
||||
<Empty description="暂无钱包流水" />
|
||||
) : (
|
||||
(ledgersQuery.data?.data.items || []).map((row) => (
|
||||
<div key={row.ledgerId} className="worker-profile-record-card">
|
||||
<div className="worker-profile-record-row-head">
|
||||
<Tag color={resolveLedgerTagColor(row.ledgerType)}>
|
||||
{formatLedgerType(row.ledgerType)}
|
||||
</Tag>
|
||||
<strong
|
||||
className={
|
||||
row.amount >= 0
|
||||
? 'worker-amount-positive'
|
||||
: 'worker-amount-negative'
|
||||
}
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
{row.amount >= 0 ? '+' : ''}
|
||||
{formatMoney(row.amount)}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="worker-profile-record-row-meta">
|
||||
<span>变动后余额: {formatMoney(row.balanceAfter)}</span>
|
||||
<span>冻结押金: {formatMoney(row.frozenAfter)}</span>
|
||||
</div>
|
||||
{row.note ? (
|
||||
<div className="worker-profile-record-note">{row.note}</div>
|
||||
) : null}
|
||||
{row.relatedWorkOrderId ? (
|
||||
<div className="worker-profile-record-sub">
|
||||
关联工单 #{row.relatedWorkOrderId}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="worker-profile-record-time">
|
||||
{formatDateTime(String(row.createdAt || ''))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{(ledgersQuery.data?.data.pagination.total || 0) > 0 ? (
|
||||
<div className="worker-orders-mobile-pagination">
|
||||
<Pagination
|
||||
current={ledgersQuery.data?.data.pagination.page || ledgerPage}
|
||||
pageSize={ledgersQuery.data?.data.pagination.pageSize || ledgerPageSize}
|
||||
total={ledgersQuery.data?.data.pagination.total || 0}
|
||||
simple
|
||||
onChange={(nextPage: number, nextPageSize: number) => {
|
||||
setLedgerPage(nextPage)
|
||||
setLedgerPageSize(nextPageSize)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</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>
|
||||
),
|
||||
},
|
||||
@@ -749,33 +813,93 @@ export default function WorkerProfilePage() {
|
||||
</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 }}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<div className="worker-profile-mobile-records">
|
||||
{requestsQuery.isLoading ? (
|
||||
<div className="worker-hall-loading"><Spin size="large" /></div>
|
||||
) : (requestsQuery.data?.data.items || []).length === 0 ? (
|
||||
<Empty description="暂无申请记录" />
|
||||
) : (
|
||||
(requestsQuery.data?.data.items || []).map((row) => (
|
||||
<div key={row.requestId} className="worker-profile-record-card">
|
||||
<div className="worker-profile-record-row-head">
|
||||
<Space size={4}>
|
||||
<Tag color={row.requestType === 'withdraw' ? 'green' : 'blue'}>
|
||||
{formatRequestType(row.requestType)}
|
||||
</Tag>
|
||||
<Tag color={resolveRequestStatusColor(row.status)}>
|
||||
{formatRequestStatus(row.status)}
|
||||
</Tag>
|
||||
</Space>
|
||||
<strong style={{ fontSize: 16 }}>{formatMoney(row.amount)}</strong>
|
||||
</div>
|
||||
{row.requestType === 'withdraw' ? (
|
||||
<div className="worker-profile-record-row-meta">
|
||||
收款: {formatWithdrawChannel(row.accountChannel)} / {row.accountName || '-'} ({maskAccountNo(row.accountNo)})
|
||||
</div>
|
||||
) : (
|
||||
<div className="worker-profile-record-row-meta">
|
||||
{renderRechargeRequestSummary(row)}
|
||||
</div>
|
||||
)}
|
||||
{row.note ? (
|
||||
<div className="worker-profile-record-note">备注: {row.note}</div>
|
||||
) : null}
|
||||
{row.reviewedNote ? (
|
||||
<div className="worker-profile-record-note" style={{ color: '#d97706' }}>
|
||||
审核备注: {row.reviewedNote}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="worker-profile-record-time">
|
||||
{formatDateTime(String(row.createdAt || ''))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{(requestsQuery.data?.data.pagination.total || 0) > 0 ? (
|
||||
<div className="worker-orders-mobile-pagination">
|
||||
<Pagination
|
||||
current={requestsQuery.data?.data.pagination.page || requestPage}
|
||||
pageSize={requestsQuery.data?.data.pagination.pageSize || requestPageSize}
|
||||
total={requestsQuery.data?.data.pagination.total || 0}
|
||||
simple
|
||||
onChange={(nextPage: number, nextPageSize: number) => {
|
||||
setRequestPage(nextPage)
|
||||
setRequestPageSize(nextPageSize)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
),
|
||||
},
|
||||
@@ -787,6 +911,7 @@ export default function WorkerProfilePage() {
|
||||
<Modal
|
||||
title="充值申请"
|
||||
open={rechargeOpen}
|
||||
width={isMobile ? '92%' : 540}
|
||||
destroyOnHidden
|
||||
confirmLoading={submittingRecharge}
|
||||
onCancel={() => {
|
||||
@@ -884,6 +1009,7 @@ export default function WorkerProfilePage() {
|
||||
<Modal
|
||||
title="提现申请"
|
||||
open={withdrawOpen}
|
||||
width={isMobile ? '92%' : 540}
|
||||
destroyOnHidden
|
||||
confirmLoading={submittingWithdraw}
|
||||
onCancel={() => {
|
||||
@@ -965,6 +1091,7 @@ export default function WorkerProfilePage() {
|
||||
<Modal
|
||||
title="修改密码"
|
||||
open={passwordOpen}
|
||||
width={isMobile ? '92%' : 480}
|
||||
destroyOnHidden
|
||||
confirmLoading={submittingPassword}
|
||||
onCancel={() => {
|
||||
@@ -1015,6 +1142,7 @@ export default function WorkerProfilePage() {
|
||||
<Modal
|
||||
title="联系管理员"
|
||||
open={contactOpen}
|
||||
width={isMobile ? '92%' : 500}
|
||||
destroyOnHidden
|
||||
footer={
|
||||
<Button type="primary" onClick={() => setContactOpen(false)}>
|
||||
|
||||
@@ -2356,10 +2356,13 @@ select {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.worker-hall-card-link {
|
||||
.worker-hall-card-link,
|
||||
.worker-hall-card-title-text {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -2653,3 +2656,591 @@ select {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
打手端 (Worker Portal) 移动端专属响应式自适应样式
|
||||
========================================================================== */
|
||||
|
||||
html,
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.worker-mobile-shell {
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f1f5f9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.worker-mobile-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.worker-mobile-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.worker-mobile-brand-badge {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: var(--theme-primary);
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 2px 6px rgba(255, 106, 0, 0.28);
|
||||
}
|
||||
|
||||
.worker-mobile-brand-text strong {
|
||||
font-size: 15px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.worker-mobile-user-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.worker-mobile-status-tag {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.worker-mobile-username {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
max-width: 90px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.worker-mobile-logout-btn {
|
||||
color: #94a3b8;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.worker-mobile-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 10px calc(65px + env(safe-area-inset-bottom, 0px)) 10px;
|
||||
}
|
||||
|
||||
.worker-mobile-content .ant-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.worker-mobile-content .ant-card .ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.worker-mobile-tabbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
height: calc(54px + env(safe-area-inset-bottom, 0px));
|
||||
box-sizing: border-box;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
box-shadow: 0 -2px 10px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.worker-mobile-tab-item {
|
||||
flex: 1 1 0;
|
||||
width: 33.333%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
padding: 4px 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.worker-mobile-tab-item.active {
|
||||
color: var(--theme-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worker-mobile-tab-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.worker-mobile-tab-label {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
移动端 我的订单 (Worker Orders Mobile Page)
|
||||
========================================================================== */
|
||||
|
||||
.worker-orders-mobile-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 顶部滑动状态胶囊栏 */
|
||||
.worker-orders-mobile-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding: 2px 2px 6px 2px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.worker-orders-mobile-status-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.worker-orders-status-pill {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.worker-orders-status-pill.active {
|
||||
background: var(--theme-primary);
|
||||
color: #ffffff;
|
||||
border-color: var(--theme-primary);
|
||||
box-shadow: 0 2px 6px rgba(255, 106, 0, 0.25);
|
||||
}
|
||||
|
||||
.worker-orders-status-pill-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.worker-orders-status-pill.active .worker-orders-status-pill-count {
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 搜索与快捷刷新栏 */
|
||||
.worker-orders-mobile-search-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.worker-orders-mobile-search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 订单卡片列表 */
|
||||
.worker-orders-mobile-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.worker-order-mobile-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
/* 头部:分类 + 订单号 + 状态 */
|
||||
.worker-order-mobile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.worker-order-mobile-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.worker-order-mobile-tag {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
padding: 0 6px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-orderno-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.worker-order-mobile-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.worker-order-mobile-header-right .ant-tag {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 商品名称与奖励 */
|
||||
.worker-order-mobile-main-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-product-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.35;
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.worker-order-mobile-price-badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.worker-order-mobile-price-label {
|
||||
font-size: 10px;
|
||||
color: #9a3412;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-price-num {
|
||||
font-size: 18px;
|
||||
color: #ea580c;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* 辅助信息行 */
|
||||
.worker-order-mobile-sub-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.worker-order-mobile-sub-item {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.worker-order-mobile-sub-time {
|
||||
margin-left: auto;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 问题单提示 */
|
||||
.worker-order-mobile-problem-box {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
color: #b91c1c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 资料凭据区 */
|
||||
.worker-order-mobile-credentials-box {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-credentials-grid {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-credential-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.worker-order-mobile-credential-label {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.worker-order-mobile-credential-val {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.worker-order-mobile-screenshots-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-copy-toolbar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* 验收凭据条 */
|
||||
.worker-order-mobile-acceptance-strip {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-acceptance-files,
|
||||
.worker-order-mobile-draft-files {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-acceptance-tag {
|
||||
color: #15803d;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worker-order-mobile-acceptance-text {
|
||||
color: #374151;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 底部操作行 */
|
||||
.worker-order-mobile-footer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.worker-order-mobile-note-block {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.worker-order-mobile-add-note-btn {
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.worker-order-mobile-btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.worker-order-mobile-action-submit {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worker-orders-mobile-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 0 6px 0;
|
||||
}
|
||||
|
||||
/* 移动端接单大厅顶部控制栏 */
|
||||
.worker-page-head-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.worker-page-head-title-row h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.worker-hall-head-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.worker-hall-autorefresh-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.worker-hall-autorefresh-label {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.worker-hall-search-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 移动端个人中心资金明细/申请记录卡片 */
|
||||
.worker-profile-mobile-records {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.worker-profile-record-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.worker-profile-record-row-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.worker-profile-record-row-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.worker-profile-record-note {
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
background: #f8fafc;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.worker-profile-record-sub {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.worker-profile-record-time {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useIsMobile(breakpoint = 768): boolean {
|
||||
const [isMobile, setIsMobile] = useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false
|
||||
return window.innerWidth <= breakpoint
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
function checkMobile() {
|
||||
setIsMobile(window.innerWidth <= breakpoint)
|
||||
}
|
||||
|
||||
// Initialize state
|
||||
checkMobile()
|
||||
|
||||
window.addEventListener('resize', checkMobile)
|
||||
return () => window.removeEventListener('resize', checkMobile)
|
||||
}, [breakpoint])
|
||||
|
||||
return isMobile
|
||||
}
|
||||
Reference in New Issue
Block a user