优化打手订单结算和大厅加载

This commit is contained in:
yml2213
2026-08-17 13:06:40 +08:00
parent 1d9e102527
commit bca2e4a801
8 changed files with 306 additions and 74 deletions
@@ -5,7 +5,7 @@ import {
SearchOutlined,
TrophyOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
App,
@@ -17,7 +17,6 @@ import {
Input,
InputNumber,
Modal,
Pagination,
Space,
Spin,
Switch,
@@ -26,7 +25,7 @@ import {
Tooltip,
Typography,
} from 'antd'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import {
fetchWorkerHallOrders,
@@ -44,6 +43,8 @@ type HallSummary = {
problemCount: number
}
const HALL_PAGE_SIZE = 20
export default function WorkerHallPage() {
const isMobile = useIsMobile()
const { message } = App.useApp()
@@ -51,14 +52,13 @@ export default function WorkerHallPage() {
const [keywordInput, setKeywordInput] = useState('')
const [keyword, setKeyword] = useState('')
const [categoryId, setCategoryId] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [grabbingId, setGrabbingId] = useState(0)
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [leaderboardOpen, setLeaderboardOpen] = useState(false)
const [sharingQuantity, setSharingQuantity] = useState(1)
const [submittingSharing, setSubmittingSharing] = useState(false)
const [autoRefresh, setAutoRefresh] = useState(loadAutoRefreshPreference)
const loadMoreRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!autoRefresh) return
@@ -73,10 +73,22 @@ export default function WorkerHallPage() {
localStorage.setItem(AUTO_REFRESH_STORAGE_KEY, enabled ? '1' : '0')
}
const ordersQuery = useQuery({
queryKey: ['worker-hall-orders', keyword, categoryId, page, pageSize],
queryFn: () =>
fetchWorkerHallOrders({ keyword, page, pageSize, categoryId: categoryId || undefined }),
const ordersQuery = useInfiniteQuery({
queryKey: ['worker-hall-orders', keyword, categoryId],
initialPageParam: 1,
queryFn: ({ pageParam }) =>
fetchWorkerHallOrders({
keyword,
page: Number(pageParam),
pageSize: HALL_PAGE_SIZE,
categoryId: categoryId || undefined,
}),
getNextPageParam: (lastPage) => {
const pagination = lastPage.data.pagination
return pagination.page * pagination.pageSize < pagination.total
? pagination.page + 1
: undefined
},
retry: false,
})
@@ -121,9 +133,10 @@ export default function WorkerHallPage() {
retry: false,
})
const orders = ordersQuery.data?.data.items || []
const pagination = ordersQuery.data?.data.pagination
const categories = ordersQuery.data?.data.categories || []
const orders = ordersQuery.data?.pages.flatMap((page) => page.data.items) || []
const firstPage = ordersQuery.data?.pages[0]?.data
const categories = firstPage?.categories || []
const totalOrderCount = Number(firstPage?.pagination.total || 0)
const worker = profileQuery.data?.data.worker
const activeCount = Number(profileQuery.data?.data.summary.activeOrderCount || 0)
const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 0
@@ -132,9 +145,36 @@ export default function WorkerHallPage() {
const remainingSlots = maxActiveOrders > 0 ? Math.max(0, maxActiveOrders - activeCount) : null
const shouldWarn = (remainingSlots !== null && remainingSlots <= 0) || problemCount > 0
useEffect(() => {
const target = loadMoreRef.current
if (
!target ||
!ordersQuery.hasNextPage ||
ordersQuery.isFetchingNextPage ||
ordersQuery.isFetchNextPageError
) {
return
}
const observer = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting) {
void ordersQuery.fetchNextPage()
}
},
{ rootMargin: '320px 0px' },
)
observer.observe(target)
return () => observer.disconnect()
}, [
ordersQuery.fetchNextPage,
ordersQuery.hasNextPage,
ordersQuery.isFetchNextPageError,
ordersQuery.isFetchingNextPage,
])
async function refreshAll() {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['worker-hall-orders'] }),
queryClient.resetQueries({ queryKey: ['worker-hall-orders'] }),
queryClient.invalidateQueries({ queryKey: ['worker-hall-summary'] }),
queryClient.invalidateQueries({ queryKey: ['worker-profile'] }),
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
@@ -195,9 +235,20 @@ export default function WorkerHallPage() {
function applyKeywordSearch() {
setKeyword(keywordInput.trim())
setPage(1)
}
const loadMoreIndicator =
orders.length > 0 ? (
<div ref={loadMoreRef} className="worker-hall-load-more" aria-live="polite">
{ordersQuery.isFetchingNextPage ? <Spin size="small" /> : null}
{ordersQuery.isFetchingNextPage ? <span></span> : null}
{ordersQuery.isFetchNextPageError ? <span></span> : null}
{!ordersQuery.hasNextPage && !ordersQuery.isFetchingNextPage ? (
<span> {totalOrderCount} </span>
) : null}
</div>
) : null
const summaryItems = [
{
label: '当前余额',
@@ -272,7 +323,6 @@ export default function WorkerHallPage() {
className={`worker-hall-mobile-cat-pill ${categoryId === 0 ? 'active' : ''}`}
onClick={() => {
setCategoryId(0)
setPage(1)
}}
>
@@ -284,7 +334,6 @@ export default function WorkerHallPage() {
className={`worker-hall-mobile-cat-pill ${categoryId === category.categoryId ? 'active' : ''}`}
onClick={() => {
setCategoryId(category.categoryId)
setPage(1)
}}
>
{category.name}
@@ -306,7 +355,6 @@ export default function WorkerHallPage() {
setKeywordInput(nextKeyword)
if (!nextKeyword.trim()) {
setKeyword('')
setPage(1)
}
}}
onPressEnter={applyKeywordSearch}
@@ -340,7 +388,7 @@ export default function WorkerHallPage() {
<div className="worker-hall-loading">
<Spin size="large" />
</div>
) : ordersQuery.error ? (
) : ordersQuery.error && orders.length === 0 ? (
<Empty
description={
ordersQuery.error instanceof Error
@@ -455,20 +503,7 @@ export default function WorkerHallPage() {
})
)}
{(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)
}}
/>
</div>
) : null}
{loadMoreIndicator}
</div>
</div>
) : (
@@ -528,7 +563,6 @@ export default function WorkerHallPage() {
setKeywordInput(nextKeyword)
if (!nextKeyword.trim()) {
setKeyword('')
setPage(1)
}
}}
onPressEnter={applyKeywordSearch}
@@ -569,7 +603,6 @@ export default function WorkerHallPage() {
]}
onChange={(nextKey) => {
setCategoryId(Number(nextKey) || 0)
setPage(1)
}}
/>
) : null}
@@ -591,7 +624,7 @@ export default function WorkerHallPage() {
<div className="worker-hall-loading">
<Spin size="large" />
</div>
) : ordersQuery.error ? (
) : ordersQuery.error && orders.length === 0 ? (
<Empty
description={
ordersQuery.error instanceof Error
@@ -704,23 +737,7 @@ export default function WorkerHallPage() {
})}
</div>
{(pagination?.total || 0) > 0 ? (
<div className="worker-hall-pagination">
<Pagination
current={pagination?.page || page}
pageSize={pagination?.pageSize || pageSize}
total={pagination?.total || 0}
showSizeChanger={!isMobile}
simple={isMobile}
pageSizeOptions={[20, 50, 100]}
showTotal={isMobile ? undefined : (total) => `${total}`}
onChange={(nextPage, nextPageSize) => {
setPage(nextPage)
setPageSize(nextPageSize)
}}
/>
</div>
) : null}
{loadMoreIndicator}
</>
)}
</Card>
@@ -34,6 +34,7 @@ import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import ImageUpload from '@/components/files/ImageUpload'
import {
fetchWorkerMyOrderOverview,
fetchWorkerMyOrders,
fetchWorkerProfile,
saveWorkerAcceptanceDraft,
@@ -215,17 +216,12 @@ export default function WorkerOrdersPage() {
const overviewQuery = useQuery({
queryKey: ['worker-my-orders-overview'],
queryFn: async () => {
const results = await Promise.all(
STATUS_OPTIONS.map(async (item) => {
const response = await fetchWorkerMyOrders(buildWorkerOrderQuery(item.value, '', 1, 1))
return [item.value || 'all', response.data.pagination.total] as const
}),
)
return Object.fromEntries(results) as Record<string, number>
},
queryFn: fetchWorkerMyOrderOverview,
retry: false,
})
const overviewByStatus = new Map(
(overviewQuery.data?.data.items || []).map((item) => [item.status, item]),
)
async function refreshAll() {
await Promise.all([
@@ -615,8 +611,11 @@ export default function WorkerOrdersPage() {
<div className="worker-orders-mobile-status-bar">
{STATUS_OPTIONS.map((item) => {
const countKey = item.value || 'all'
const count = Number(overviewQuery.data?.[countKey] || 0)
const overview = overviewByStatus.get(countKey)
const count = Number(overview?.orderCount || 0)
const settlementAmount = Number(overview?.settlementAmount || 0)
const isActive = (status || '') === item.value
const settlementLabel = getSettlementLabel(item.value)
return (
<button
key={item.value || 'all'}
@@ -627,10 +626,15 @@ export default function WorkerOrdersPage() {
setPage(1)
}}
>
<span>{item.label}</span>
{count > 0 ? (
<span className="worker-orders-status-pill-count">{count}</span>
) : null}
<span className="worker-orders-status-pill-main">
<span>{item.label}</span>
{count > 0 ? (
<span className="worker-orders-status-pill-count">{count}</span>
) : null}
</span>
<span className="worker-orders-status-pill-settlement">
{settlementLabel} {formatMoney(settlementAmount)}
</span>
</button>
)
})}
@@ -928,16 +932,26 @@ export default function WorkerOrdersPage() {
<Card bordered={false}>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Tabs
className="worker-order-tabs"
activeKey={status || 'all'}
items={STATUS_OPTIONS.map((item) => {
const countKey = item.value || 'all'
const count = Number(overviewQuery.data?.[countKey] || 0)
const overview = overviewByStatus.get(countKey)
const count = Number(overview?.orderCount || 0)
const settlementAmount = Number(overview?.settlementAmount || 0)
return {
key: item.value || 'all',
label: (
<span>
{item.label}
{count > 0 ? <span className="worker-order-tab-count">{count}</span> : null}
<span className="worker-order-tab-label">
<span className="worker-order-tab-label-main">
{item.label}
{count > 0 ? (
<span className="worker-order-tab-count">{count}</span>
) : null}
</span>
<span className="worker-order-tab-settlement">
{getSettlementLabel(item.value)} {formatMoney(settlementAmount)}
</span>
</span>
),
}
@@ -1519,6 +1533,12 @@ function formatMoney(value: number | undefined) {
return `${((Number(value || 0) || 0) / 100).toFixed(2)}`
}
function getSettlementLabel(status: string) {
if (status === 'accepted' || status === 'vip_evidence_pending') return '已结算'
if (!status) return '结算合计'
return '预计'
}
function formatStatus(status: string) {
const labels: Record<string, string> = {
open: '已超时',