From 10ae3d01ee21c69f0d12109da05a6421d0e49e80 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 16 Aug 2026 10:33:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E4=BC=98=E5=8C=96=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E7=AB=AFui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/layouts/WorkerLayout.tsx | 92 ++- .../src/pages/worker/WorkerHallPage.tsx | 245 ++------ .../src/pages/worker/WorkerOrdersPage.tsx | 464 +++++++++++--- .../src/pages/worker/WorkerProfilePage.tsx | 236 +++++-- apps/frontend/src/styles/main.css | 593 +++++++++++++++++- apps/frontend/src/utils/use-is-mobile.ts | 24 + 6 files changed, 1316 insertions(+), 338 deletions(-) create mode 100644 apps/frontend/src/utils/use-is-mobile.ts diff --git a/apps/frontend/src/layouts/WorkerLayout.tsx b/apps/frontend/src/layouts/WorkerLayout.tsx index 9b0c86ac..78a0d42d 100644 --- a/apps/frontend/src/layouts/WorkerLayout.tsx +++ b/apps/frontend/src/layouts/WorkerLayout.tsx @@ -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: , 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 ( +
+
+
+ +
+ 王大锤接单 +
+
+
+ + {formatWorkerStatus(status)} + + + {username} + +
+
+ +
+ +
+ + +
+ ) + } + return ( @@ -53,7 +128,7 @@ export default function WorkerLayout() { navigate(String(key))} @@ -68,7 +143,8 @@ export default function WorkerLayout() { - {formatWorkerStatus(status)} + {formatWorkerStatus(status)} + {username} @@ -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' +} diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index 14270ce6..0fa9eded 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -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(null) const [sharingOrder, setSharingOrder] = useState(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 (
- 抢单大厅 - - - - - +
+ 抢单大厅 +
+ +
+ 自动刷新 + +
+
+ +
+
+ +
- +
@@ -340,13 +342,12 @@ export default function WorkerHallPage() { return (
- setDetailOrder(order)} > {order.productName || '未命名商品'} - + {order.categoryName || '默认分类'} @@ -397,16 +398,6 @@ export default function WorkerHallPage() { ) : null}
-
- -
{order.sharing?.enabled && !disabledReason ? ( - ) : null - } - > - {detailOrder ? ( -
- - - - - - {detailOrder.productName || '-'} - - - {detailOrder.categoryName || '-'} - - - {getDisplayOrderNo(detailOrder)} - - - {formatMoney(detailOrder.rewardAmount)} - - - {formatMoney(detailOrder.freezeDepositAmount)} - - - {Number(detailOrder.timeoutMinutes || 0) > 0 ? ( - }> - 限时 {detailOrder.timeoutMinutes} 分钟 - - ) : ( - '不限时' - )} - - - {formatDateTime(resolvePublishedTime(detailOrder))} - - - {formatDateTime(detailOrder.updatedAt)} - - - {formatRequirementShortLabel(getRequirementFields(detailOrder))} - - - {detailDisabledReason || '当前可直接抢单'} - - - - - - {renderRequirementFieldValues(detailOrder)} - -
- ) : null} - - 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 当前工单未配置资料要求 - } - - return ( - - {fields.map((field) => ( - - - {field.key} - - {field.required ? '必填' : '选填'} - - - - ))} - - ) -} - 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 | null | undefined): string { return String(order?.platformOrderId || '').trim() || '-' } -function asRecord(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) - ? (value as Record) - : {} -} - const AUTO_REFRESH_STORAGE_KEY = 'worker-hall-auto-refresh' function loadAutoRefreshPreference(): boolean { diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index 1ea323aa..1003ba10 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -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 ( -
-
- 我的订单 - -
- - - - { + <> + {isMobile ? ( +
+ {/* 顶部水平滑动状态切换胶囊条 */} +
+ {STATUS_OPTIONS.map((item) => { const countKey = item.value || 'all' const count = Number(overviewQuery.data?.[countKey] || 0) - return { - key: item.value || 'all', - label: ( - - {item.label} - {count > 0 ? ( - {count} - ) : null} - - ), - } + const isActive = (status || '') === item.value + return ( + + ) })} - onChange={(nextKey) => { - const nextStatus = nextKey === 'all' ? '' : nextKey - setStatus(nextStatus) - setPage(1) - }} - /> +
- + {/* 搜索与刷新栏 */} +
} + 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} /> - + /> +
- - rowKey="workOrderId" - size="small" - loading={ordersQuery.isLoading} - dataSource={orders} - columns={columns} - locale={{ - emptyText: ordersQuery.error ? ( - + {ordersQuery.isLoading ? ( +
+ +
+ ) : ordersQuery.error ? ( + + ) : orders.length === 0 ? ( + + ) : ( + 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 ( +
+ {/* 卡片顶栏:分类 + 订单号 + 状态/倒计时 */} +
+
+ {order.categoryName ? ( + + {order.categoryName} + + ) : null} + {order.myShare ? ( + + 拼单 + + ) : null} + + {getDisplayOrderNo(order)} + +
+
+ + {formatStatus(order.status)} + + {order.status === 'in_progress' && order.deadlineAt ? ( + + ) : null} +
+
+ + {/* 商品标题与金额 */} +
+
+ {order.productName || '未命名商品'} +
+
+ 报酬 + + {formatMoney(order.rewardAmount)} + +
+
+ + {/* 辅助金额与接单时间 */} +
+ + 押金: {formatMoney(order.freezeDepositAmount)} + + {order.myShare ? ( + + 已拼 {order.myShare.quantity} 份 ({formatMoney(order.myShare.shareReward)}) + + ) : null} + + {formatDateTime(order.assignedAt)} + +
+ + {/* 问题单提示 */} + {order.status === 'problem' && String(order.problemNote || '').trim() ? ( +
+ 问题: + {order.problemNote} +
+ ) : null} + + {/* 买家资料与截图 */} + {(items.length > 0 || screenshots.length > 0) && ( +
+ {items.length > 0 ? ( +
+ {items.map((item) => ( +
+ {item.label}: + {item.value} +
+ ))} +
+ ) : null} + + {screenshots.length > 0 ? ( +
+ 截图: + +
+ ) : null} + +
+ + {screenshots.length > 0 ? ( + + ) : null} +
+
+ )} + + {/* 验收凭据与暂存状态 */} + {(hasAcceptance || (draftCount > 0 && !hasSubmittedAcceptance(order)) || noteText) && ( +
+ {hasAcceptance ? ( +
+ + 已提交凭据 ({acceptanceFiles.length}张) + + +
+ ) : null} + {draftCount > 0 && !hasSubmittedAcceptance(order) ? ( +
+ }> + 已暂存 {draftCount} 张图片 + + +
+ ) : null} + {noteText ? ( +
+ 说明:{noteText} +
+ ) : null} +
+ )} + + {/* 底部备注与操作 */} +
+
+ {workerNote ? ( +
openNoteModal(order)}> + + {workerNote} + + +
+ ) : ( + + )} +
+ +
+ + {canEditAcceptanceImages(order) ? ( + + ) : null} +
+
+
+ ) + }) + )} + + {(pagination?.total || 0) > 0 ? ( +
+ { + setPage(nextPage) + setPageSize(nextPageSize) + }} /> - ) : ( - + ) : null} +
+
+ ) : ( +
+
+ 我的订单 + +
+ + + + { + const countKey = item.value || 'all' + const count = Number(overviewQuery.data?.[countKey] || 0) + return { + key: item.value || 'all', + label: ( + + {item.label} + {count > 0 ? ( + {count} + ) : null} + + ), } + })} + onChange={(nextKey) => { + const nextStatus = nextKey === 'all' ? '' : nextKey + setStatus(nextStatus) + setPage(1) + }} + /> + + + } + 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 }} - /> - - + + + + + rowKey="workOrderId" + size="small" + loading={ordersQuery.isLoading} + dataSource={orders} + columns={columns} + locale={{ + emptyText: ordersQuery.error ? ( + + ) : ( + + ), + }} + 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 }} + /> + + +
+ )} setDetailOrder(null)} @@ -698,7 +1001,7 @@ export default function WorkerOrdersPage() { { 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() { -
+ ) } diff --git a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx index 01a8a3b9..c7f30da5 100644 --- a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx @@ -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() { } > - - 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 ? ( +
+ {ledgersQuery.isLoading ? ( +
+ ) : (ledgersQuery.data?.data.items || []).length === 0 ? ( + + ) : ( + (ledgersQuery.data?.data.items || []).map((row) => ( +
+
+ + {formatLedgerType(row.ledgerType)} + + = 0 + ? 'worker-amount-positive' + : 'worker-amount-negative' + } + style={{ fontSize: 16 }} + > + {row.amount >= 0 ? '+' : ''} + {formatMoney(row.amount)} + +
+
+ 变动后余额: {formatMoney(row.balanceAfter)} + 冻结押金: {formatMoney(row.frozenAfter)} +
+ {row.note ? ( +
{row.note}
+ ) : null} + {row.relatedWorkOrderId ? ( +
+ 关联工单 #{row.relatedWorkOrderId} +
+ ) : null} +
+ {formatDateTime(String(row.createdAt || ''))} +
+
+ )) + )} + {(ledgersQuery.data?.data.pagination.total || 0) > 0 ? ( +
+ { + setLedgerPage(nextPage) + setLedgerPageSize(nextPageSize) + }} + /> +
+ ) : null} +
+ ) : ( + + 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 }} + /> + )} ), }, @@ -749,33 +813,93 @@ export default function WorkerProfilePage() { } > - - 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 ? ( +
+ {requestsQuery.isLoading ? ( +
+ ) : (requestsQuery.data?.data.items || []).length === 0 ? ( + + ) : ( + (requestsQuery.data?.data.items || []).map((row) => ( +
+
+ + + {formatRequestType(row.requestType)} + + + {formatRequestStatus(row.status)} + + + {formatMoney(row.amount)} +
+ {row.requestType === 'withdraw' ? ( +
+ 收款: {formatWithdrawChannel(row.accountChannel)} / {row.accountName || '-'} ({maskAccountNo(row.accountNo)}) +
+ ) : ( +
+ {renderRechargeRequestSummary(row)} +
+ )} + {row.note ? ( +
备注: {row.note}
+ ) : null} + {row.reviewedNote ? ( +
+ 审核备注: {row.reviewedNote} +
+ ) : null} +
+ {formatDateTime(String(row.createdAt || ''))} +
+
+ )) + )} + {(requestsQuery.data?.data.pagination.total || 0) > 0 ? ( +
+ { + setRequestPage(nextPage) + setRequestPageSize(nextPageSize) + }} + /> +
+ ) : null} +
+ ) : ( + + 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 }} + /> + )} ), }, @@ -787,6 +911,7 @@ export default function WorkerProfilePage() { { @@ -884,6 +1009,7 @@ export default function WorkerProfilePage() { { @@ -965,6 +1091,7 @@ export default function WorkerProfilePage() { { @@ -1015,6 +1142,7 @@ export default function WorkerProfilePage() { setContactOpen(false)}> diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index 4ff93544..2bff8f91 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -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; +} + + diff --git a/apps/frontend/src/utils/use-is-mobile.ts b/apps/frontend/src/utils/use-is-mobile.ts new file mode 100644 index 00000000..d7187727 --- /dev/null +++ b/apps/frontend/src/utils/use-is-mobile.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react' + +export function useIsMobile(breakpoint = 768): boolean { + const [isMobile, setIsMobile] = useState(() => { + 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 +}