优化打手订单结算和大厅加载
This commit is contained in:
@@ -48,6 +48,14 @@ const WORK_ORDER_SHARE_SELECT = `
|
|||||||
LEFT JOIN worker_users wu ON wu.id = wos.worker_id
|
LEFT JOIN worker_users wu ON wu.id = wos.worker_id
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type WorkerOrderOverviewRow = {
|
||||||
|
status: string
|
||||||
|
order_count: number
|
||||||
|
settlement_amount: number
|
||||||
|
vip_evidence_pending_count: number
|
||||||
|
vip_evidence_pending_settlement_amount: number
|
||||||
|
}
|
||||||
|
|
||||||
export async function listWorkOrderShares(
|
export async function listWorkOrderShares(
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
): Promise<WorkOrderShareRow[]> {
|
): Promise<WorkOrderShareRow[]> {
|
||||||
@@ -820,6 +828,81 @@ export async function listWorkOrders({
|
|||||||
return { items: itemsResult.rows, total: Number(totalResult.rows[0]?.total || 0) }
|
return { items: itemsResult.rows, total: Number(totalResult.rows[0]?.total || 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 汇总打手本人各订单状态的可结算金额,拼单按个人份额计算。 */
|
||||||
|
export async function getWorkerOrderOverview(workerId: number | string): Promise<
|
||||||
|
Array<{
|
||||||
|
status: string
|
||||||
|
orderCount: number
|
||||||
|
settlementAmount: number
|
||||||
|
vipEvidencePendingCount: number
|
||||||
|
vipEvidencePendingSettlementAmount: number
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const normalizedWorkerId = Number(workerId)
|
||||||
|
const result = await query<WorkerOrderOverviewRow>(
|
||||||
|
`
|
||||||
|
WITH worker_orders AS (
|
||||||
|
SELECT
|
||||||
|
wo.status,
|
||||||
|
wo.acceptance_json,
|
||||||
|
wos.id IS NOT NULL AS is_worker_share,
|
||||||
|
CASE
|
||||||
|
WHEN wos.id IS NOT NULL THEN wos.share_reward
|
||||||
|
WHEN wo.assigned_worker_id = $1 THEN wo.reward_amount
|
||||||
|
ELSE 0
|
||||||
|
END AS worker_settlement_amount
|
||||||
|
FROM work_orders wo
|
||||||
|
LEFT JOIN work_order_shares wos
|
||||||
|
ON wos.work_order_id = wo.id
|
||||||
|
AND wos.worker_id = $1
|
||||||
|
AND wos.status != 'cancelled'
|
||||||
|
WHERE wo.assigned_worker_id = $1
|
||||||
|
OR wo.last_assigned_worker_id = $1
|
||||||
|
OR wos.id IS NOT NULL
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
status,
|
||||||
|
COUNT(*)::int AS order_count,
|
||||||
|
COALESCE(
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN status IN ('in_progress', 'pending_acceptance', 'problem', 'accepted')
|
||||||
|
OR (status = 'open' AND is_worker_share)
|
||||||
|
THEN worker_settlement_amount
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)::int AS settlement_amount,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE status = 'accepted'
|
||||||
|
AND acceptance_json->>'reviewMode' = 'vip_auto'
|
||||||
|
AND acceptance_json->>'evidenceStatus' = 'pending'
|
||||||
|
AND NULLIF(acceptance_json->>'evidenceDueAt', '')::timestamptz > NOW()
|
||||||
|
)::int AS vip_evidence_pending_count,
|
||||||
|
COALESCE(
|
||||||
|
SUM(worker_settlement_amount) FILTER (
|
||||||
|
WHERE status = 'accepted'
|
||||||
|
AND acceptance_json->>'reviewMode' = 'vip_auto'
|
||||||
|
AND acceptance_json->>'evidenceStatus' = 'pending'
|
||||||
|
AND NULLIF(acceptance_json->>'evidenceDueAt', '')::timestamptz > NOW()
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)::int AS vip_evidence_pending_settlement_amount
|
||||||
|
FROM worker_orders
|
||||||
|
GROUP BY status
|
||||||
|
`,
|
||||||
|
[normalizedWorkerId],
|
||||||
|
)
|
||||||
|
return result.rows.map((row) => ({
|
||||||
|
status: row.status,
|
||||||
|
orderCount: Number(row.order_count || 0),
|
||||||
|
settlementAmount: Number(row.settlement_amount || 0),
|
||||||
|
vipEvidencePendingCount: Number(row.vip_evidence_pending_count || 0),
|
||||||
|
vipEvidencePendingSettlementAmount: Number(row.vip_evidence_pending_settlement_amount || 0),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateWorkOrder(
|
export async function updateWorkOrder(
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
patch: Partial<
|
patch: Partial<
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getWorkerProfile,
|
getWorkerProfile,
|
||||||
getWorkerSessionSummary,
|
getWorkerSessionSummary,
|
||||||
getWorkerHallLeaderboard,
|
getWorkerHallLeaderboard,
|
||||||
|
getWorkerMyOrderOverview,
|
||||||
grabWorkerHallOrder,
|
grabWorkerHallOrder,
|
||||||
joinWorkerSharingOrder,
|
joinWorkerSharingOrder,
|
||||||
listWorkerProfileFinanceRequests,
|
listWorkerProfileFinanceRequests,
|
||||||
@@ -268,6 +269,16 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/orders/overview',
|
||||||
|
requireActiveWorker,
|
||||||
|
createRouteHandler((req) => getWorkerMyOrderOverview(getRequiredWorkerSession(req)), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取我的订单概览失败',
|
||||||
|
scope: '[worker/orders/overview]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/orders/:workOrderId/events',
|
'/orders/:workOrderId/events',
|
||||||
requireActiveWorker,
|
requireActiveWorker,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
getWorkerUserByDisplayName,
|
getWorkerUserByDisplayName,
|
||||||
getWorkOrderById,
|
getWorkOrderById,
|
||||||
getWorkOrderShare,
|
getWorkOrderShare,
|
||||||
|
getWorkerOrderOverview,
|
||||||
grabWorkOrder,
|
grabWorkOrder,
|
||||||
incrementWorkerSessionVersion,
|
incrementWorkerSessionVersion,
|
||||||
joinWorkOrderShare,
|
joinWorkOrderShare,
|
||||||
@@ -1148,6 +1149,40 @@ export async function listWorkerMyOrders(query: JsonObject = {}, session: Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 打手订单标签概览:订单数与预计/已结算金额。 */
|
||||||
|
export async function getWorkerMyOrderOverview(session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
await settleOverdueWorkOrders({ workerId: worker.id, limit: 50 })
|
||||||
|
const rows = await getWorkerOrderOverview(worker.id)
|
||||||
|
const items = rows.map((row) => ({
|
||||||
|
status: row.status,
|
||||||
|
orderCount: row.orderCount,
|
||||||
|
settlementAmount: row.settlementAmount,
|
||||||
|
}))
|
||||||
|
const vipEvidencePending = rows.reduce(
|
||||||
|
(summary, row) => ({
|
||||||
|
orderCount: summary.orderCount + row.vipEvidencePendingCount,
|
||||||
|
settlementAmount: summary.settlementAmount + row.vipEvidencePendingSettlementAmount,
|
||||||
|
}),
|
||||||
|
{ orderCount: 0, settlementAmount: 0 },
|
||||||
|
)
|
||||||
|
const all = items.reduce(
|
||||||
|
(summary, item) => ({
|
||||||
|
orderCount: summary.orderCount + item.orderCount,
|
||||||
|
settlementAmount: summary.settlementAmount + item.settlementAmount,
|
||||||
|
}),
|
||||||
|
{ orderCount: 0, settlementAmount: 0 },
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
items: [
|
||||||
|
...items,
|
||||||
|
{ status: 'vip_evidence_pending', ...vipEvidencePending },
|
||||||
|
{ status: 'all', ...all },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getWorkerMyOrderEvents(workOrderId: number | string, session: WorkerSession) {
|
export async function getWorkerMyOrderEvents(workOrderId: number | string, session: WorkerSession) {
|
||||||
requireActiveWorkerSession(session)
|
requireActiveWorkerSession(session)
|
||||||
if (runtimeConfig.worker.orderTimelineVisible !== true) {
|
if (runtimeConfig.worker.orderTimelineVisible !== true) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
TrophyOutlined,
|
TrophyOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
App,
|
App,
|
||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Modal,
|
Modal,
|
||||||
Pagination,
|
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -26,7 +25,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchWorkerHallOrders,
|
fetchWorkerHallOrders,
|
||||||
@@ -44,6 +43,8 @@ type HallSummary = {
|
|||||||
problemCount: number
|
problemCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HALL_PAGE_SIZE = 20
|
||||||
|
|
||||||
export default function WorkerHallPage() {
|
export default function WorkerHallPage() {
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
@@ -51,14 +52,13 @@ export default function WorkerHallPage() {
|
|||||||
const [keywordInput, setKeywordInput] = useState('')
|
const [keywordInput, setKeywordInput] = useState('')
|
||||||
const [keyword, setKeyword] = useState('')
|
const [keyword, setKeyword] = useState('')
|
||||||
const [categoryId, setCategoryId] = useState(0)
|
const [categoryId, setCategoryId] = useState(0)
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [pageSize, setPageSize] = useState(20)
|
|
||||||
const [grabbingId, setGrabbingId] = useState(0)
|
const [grabbingId, setGrabbingId] = useState(0)
|
||||||
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
||||||
const [leaderboardOpen, setLeaderboardOpen] = useState(false)
|
const [leaderboardOpen, setLeaderboardOpen] = useState(false)
|
||||||
const [sharingQuantity, setSharingQuantity] = useState(1)
|
const [sharingQuantity, setSharingQuantity] = useState(1)
|
||||||
const [submittingSharing, setSubmittingSharing] = useState(false)
|
const [submittingSharing, setSubmittingSharing] = useState(false)
|
||||||
const [autoRefresh, setAutoRefresh] = useState(loadAutoRefreshPreference)
|
const [autoRefresh, setAutoRefresh] = useState(loadAutoRefreshPreference)
|
||||||
|
const loadMoreRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoRefresh) return
|
if (!autoRefresh) return
|
||||||
@@ -73,10 +73,22 @@ export default function WorkerHallPage() {
|
|||||||
localStorage.setItem(AUTO_REFRESH_STORAGE_KEY, enabled ? '1' : '0')
|
localStorage.setItem(AUTO_REFRESH_STORAGE_KEY, enabled ? '1' : '0')
|
||||||
}
|
}
|
||||||
|
|
||||||
const ordersQuery = useQuery({
|
const ordersQuery = useInfiniteQuery({
|
||||||
queryKey: ['worker-hall-orders', keyword, categoryId, page, pageSize],
|
queryKey: ['worker-hall-orders', keyword, categoryId],
|
||||||
queryFn: () =>
|
initialPageParam: 1,
|
||||||
fetchWorkerHallOrders({ keyword, page, pageSize, categoryId: categoryId || undefined }),
|
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,
|
retry: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -121,9 +133,10 @@ export default function WorkerHallPage() {
|
|||||||
retry: false,
|
retry: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const orders = ordersQuery.data?.data.items || []
|
const orders = ordersQuery.data?.pages.flatMap((page) => page.data.items) || []
|
||||||
const pagination = ordersQuery.data?.data.pagination
|
const firstPage = ordersQuery.data?.pages[0]?.data
|
||||||
const categories = ordersQuery.data?.data.categories || []
|
const categories = firstPage?.categories || []
|
||||||
|
const totalOrderCount = Number(firstPage?.pagination.total || 0)
|
||||||
const worker = profileQuery.data?.data.worker
|
const worker = profileQuery.data?.data.worker
|
||||||
const activeCount = Number(profileQuery.data?.data.summary.activeOrderCount || 0)
|
const activeCount = Number(profileQuery.data?.data.summary.activeOrderCount || 0)
|
||||||
const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 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 remainingSlots = maxActiveOrders > 0 ? Math.max(0, maxActiveOrders - activeCount) : null
|
||||||
const shouldWarn = (remainingSlots !== null && remainingSlots <= 0) || problemCount > 0
|
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() {
|
async function refreshAll() {
|
||||||
await Promise.all([
|
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-hall-summary'] }),
|
||||||
queryClient.invalidateQueries({ queryKey: ['worker-profile'] }),
|
queryClient.invalidateQueries({ queryKey: ['worker-profile'] }),
|
||||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
|
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
|
||||||
@@ -195,9 +235,20 @@ export default function WorkerHallPage() {
|
|||||||
|
|
||||||
function applyKeywordSearch() {
|
function applyKeywordSearch() {
|
||||||
setKeyword(keywordInput.trim())
|
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 = [
|
const summaryItems = [
|
||||||
{
|
{
|
||||||
label: '当前余额',
|
label: '当前余额',
|
||||||
@@ -272,7 +323,6 @@ export default function WorkerHallPage() {
|
|||||||
className={`worker-hall-mobile-cat-pill ${categoryId === 0 ? 'active' : ''}`}
|
className={`worker-hall-mobile-cat-pill ${categoryId === 0 ? 'active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCategoryId(0)
|
setCategoryId(0)
|
||||||
setPage(1)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
全部
|
全部
|
||||||
@@ -284,7 +334,6 @@ export default function WorkerHallPage() {
|
|||||||
className={`worker-hall-mobile-cat-pill ${categoryId === category.categoryId ? 'active' : ''}`}
|
className={`worker-hall-mobile-cat-pill ${categoryId === category.categoryId ? 'active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCategoryId(category.categoryId)
|
setCategoryId(category.categoryId)
|
||||||
setPage(1)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{category.name}
|
{category.name}
|
||||||
@@ -306,7 +355,6 @@ export default function WorkerHallPage() {
|
|||||||
setKeywordInput(nextKeyword)
|
setKeywordInput(nextKeyword)
|
||||||
if (!nextKeyword.trim()) {
|
if (!nextKeyword.trim()) {
|
||||||
setKeyword('')
|
setKeyword('')
|
||||||
setPage(1)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPressEnter={applyKeywordSearch}
|
onPressEnter={applyKeywordSearch}
|
||||||
@@ -340,7 +388,7 @@ export default function WorkerHallPage() {
|
|||||||
<div className="worker-hall-loading">
|
<div className="worker-hall-loading">
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
</div>
|
</div>
|
||||||
) : ordersQuery.error ? (
|
) : ordersQuery.error && orders.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
description={
|
description={
|
||||||
ordersQuery.error instanceof Error
|
ordersQuery.error instanceof Error
|
||||||
@@ -455,20 +503,7 @@ export default function WorkerHallPage() {
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(pagination?.total || 0) > 0 ? (
|
{loadMoreIndicator}
|
||||||
<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}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -528,7 +563,6 @@ export default function WorkerHallPage() {
|
|||||||
setKeywordInput(nextKeyword)
|
setKeywordInput(nextKeyword)
|
||||||
if (!nextKeyword.trim()) {
|
if (!nextKeyword.trim()) {
|
||||||
setKeyword('')
|
setKeyword('')
|
||||||
setPage(1)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPressEnter={applyKeywordSearch}
|
onPressEnter={applyKeywordSearch}
|
||||||
@@ -569,7 +603,6 @@ export default function WorkerHallPage() {
|
|||||||
]}
|
]}
|
||||||
onChange={(nextKey) => {
|
onChange={(nextKey) => {
|
||||||
setCategoryId(Number(nextKey) || 0)
|
setCategoryId(Number(nextKey) || 0)
|
||||||
setPage(1)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -591,7 +624,7 @@ export default function WorkerHallPage() {
|
|||||||
<div className="worker-hall-loading">
|
<div className="worker-hall-loading">
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
</div>
|
</div>
|
||||||
) : ordersQuery.error ? (
|
) : ordersQuery.error && orders.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
description={
|
description={
|
||||||
ordersQuery.error instanceof Error
|
ordersQuery.error instanceof Error
|
||||||
@@ -704,23 +737,7 @@ export default function WorkerHallPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(pagination?.total || 0) > 0 ? (
|
{loadMoreIndicator}
|
||||||
<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}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
|
|||||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||||
import ImageUpload from '@/components/files/ImageUpload'
|
import ImageUpload from '@/components/files/ImageUpload'
|
||||||
import {
|
import {
|
||||||
|
fetchWorkerMyOrderOverview,
|
||||||
fetchWorkerMyOrders,
|
fetchWorkerMyOrders,
|
||||||
fetchWorkerProfile,
|
fetchWorkerProfile,
|
||||||
saveWorkerAcceptanceDraft,
|
saveWorkerAcceptanceDraft,
|
||||||
@@ -215,17 +216,12 @@ export default function WorkerOrdersPage() {
|
|||||||
|
|
||||||
const overviewQuery = useQuery({
|
const overviewQuery = useQuery({
|
||||||
queryKey: ['worker-my-orders-overview'],
|
queryKey: ['worker-my-orders-overview'],
|
||||||
queryFn: async () => {
|
queryFn: fetchWorkerMyOrderOverview,
|
||||||
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>
|
|
||||||
},
|
|
||||||
retry: false,
|
retry: false,
|
||||||
})
|
})
|
||||||
|
const overviewByStatus = new Map(
|
||||||
|
(overviewQuery.data?.data.items || []).map((item) => [item.status, item]),
|
||||||
|
)
|
||||||
|
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -615,8 +611,11 @@ export default function WorkerOrdersPage() {
|
|||||||
<div className="worker-orders-mobile-status-bar">
|
<div className="worker-orders-mobile-status-bar">
|
||||||
{STATUS_OPTIONS.map((item) => {
|
{STATUS_OPTIONS.map((item) => {
|
||||||
const countKey = item.value || 'all'
|
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 isActive = (status || '') === item.value
|
||||||
|
const settlementLabel = getSettlementLabel(item.value)
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.value || 'all'}
|
key={item.value || 'all'}
|
||||||
@@ -627,10 +626,15 @@ export default function WorkerOrdersPage() {
|
|||||||
setPage(1)
|
setPage(1)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<span className="worker-orders-status-pill-main">
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
{count > 0 ? (
|
{count > 0 ? (
|
||||||
<span className="worker-orders-status-pill-count">{count}</span>
|
<span className="worker-orders-status-pill-count">{count}</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
</span>
|
||||||
|
<span className="worker-orders-status-pill-settlement">
|
||||||
|
{settlementLabel} {formatMoney(settlementAmount)}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -928,16 +932,26 @@ export default function WorkerOrdersPage() {
|
|||||||
<Card bordered={false}>
|
<Card bordered={false}>
|
||||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
className="worker-order-tabs"
|
||||||
activeKey={status || 'all'}
|
activeKey={status || 'all'}
|
||||||
items={STATUS_OPTIONS.map((item) => {
|
items={STATUS_OPTIONS.map((item) => {
|
||||||
const countKey = item.value || 'all'
|
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 {
|
return {
|
||||||
key: item.value || 'all',
|
key: item.value || 'all',
|
||||||
label: (
|
label: (
|
||||||
<span>
|
<span className="worker-order-tab-label">
|
||||||
|
<span className="worker-order-tab-label-main">
|
||||||
{item.label}
|
{item.label}
|
||||||
{count > 0 ? <span className="worker-order-tab-count">{count}</span> : null}
|
{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>
|
</span>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -1519,6 +1533,12 @@ function formatMoney(value: number | undefined) {
|
|||||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
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) {
|
function formatStatus(status: string) {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
open: '已超时',
|
open: '已超时',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
WorkerLeaderboardResponse,
|
WorkerLeaderboardResponse,
|
||||||
WorkerListResponse,
|
WorkerListResponse,
|
||||||
WorkerLoginResponse,
|
WorkerLoginResponse,
|
||||||
|
WorkerOrderOverviewResponse,
|
||||||
WorkerProfileResponse,
|
WorkerProfileResponse,
|
||||||
WorkerUser,
|
WorkerUser,
|
||||||
WorkerWalletLedger,
|
WorkerWalletLedger,
|
||||||
@@ -131,6 +132,10 @@ export function fetchWorkerMyOrders(params?: Record<string, unknown>) {
|
|||||||
return apiGet<WorkerListResponse<WorkOrder>>('/api/v1/worker/orders', params)
|
return apiGet<WorkerListResponse<WorkOrder>>('/api/v1/worker/orders', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchWorkerMyOrderOverview() {
|
||||||
|
return apiGet<WorkerOrderOverviewResponse>('/api/v1/worker/orders/overview')
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchWorkerMyOrderEvents(workOrderId: number) {
|
export function fetchWorkerMyOrderEvents(workOrderId: number) {
|
||||||
return apiGet<{ items: WorkOrderEvent[] }>(`/api/v1/worker/orders/${workOrderId}/events`)
|
return apiGet<{ items: WorkOrderEvent[] }>(`/api/v1/worker/orders/${workOrderId}/events`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,29 @@
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.worker-order-tab-label {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worker-order-tab-label-main {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worker-order-tab-settlement {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worker-order-tabs .ant-tabs-tab-active .worker-order-tab-settlement {
|
||||||
|
color: var(--theme-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.worker-order-tabs .ant-tabs-nav {
|
.worker-order-tabs .ant-tabs-nav {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
@@ -575,6 +598,16 @@
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.worker-hall-load-more {
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.worker-hall-card-grid {
|
.worker-hall-card-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
||||||
@@ -882,16 +915,17 @@ body {
|
|||||||
|
|
||||||
.worker-orders-status-pill {
|
.worker-orders-status-pill {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
padding: 5px 12px;
|
padding: 5px 10px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
@@ -919,6 +953,23 @@ body {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.worker-orders-status-pill-main {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worker-orders-status-pill-settlement {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worker-orders-status-pill.active .worker-orders-status-pill-settlement {
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
/* 搜索与快捷刷新栏 */
|
/* 搜索与快捷刷新栏 */
|
||||||
.worker-orders-mobile-search-row {
|
.worker-orders-mobile-search-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -323,6 +323,16 @@ export type WorkerListResponse<T> = {
|
|||||||
pagination: Pagination
|
pagination: Pagination
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerOrderOverviewItem = {
|
||||||
|
status: string
|
||||||
|
orderCount: number
|
||||||
|
settlementAmount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkerOrderOverviewResponse = {
|
||||||
|
items: WorkerOrderOverviewItem[]
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkerHallOrdersResponse = {
|
export type WorkerHallOrdersResponse = {
|
||||||
items: WorkOrder[]
|
items: WorkOrder[]
|
||||||
pagination: Pagination
|
pagination: Pagination
|
||||||
|
|||||||
Reference in New Issue
Block a user