fix: 修复移动端我的订单卡片与搜索栏Flex样式异常

This commit is contained in:
yml2213
2026-08-16 11:14:33 +08:00
parent 0270657f2b
commit 27bcb7dd68
7 changed files with 430 additions and 77 deletions
@@ -680,6 +680,83 @@ export async function countWorkerAcceptedOrders(workerId: number | string): Prom
return Number(result.rows[0]?.total || 0)
}
export async function listWorkerCompletionLeaderboard(input: {
limit: number
currentWorkerId: number
}): Promise<
Array<{
rank: number
workerId: number
displayName: string
levelName: string
acceptedOrderCount: number
}>
> {
const limit = Math.min(50, Math.max(1, Math.floor(Number(input.limit || 10))))
const result = await query<{
rank: number
worker_id: number
display_name: string
level_name: string | null
accepted_order_count: number
}>(
`
WITH accepted_totals AS (
SELECT
worker_id,
SUM(accepted_order_count)::int AS accepted_order_count,
MAX(last_accepted_at) AS last_accepted_at
FROM (
SELECT
assigned_worker_id AS worker_id,
COUNT(*)::int AS accepted_order_count,
MAX(accepted_at) AS last_accepted_at
FROM work_orders
WHERE assigned_worker_id IS NOT NULL AND status = 'accepted'
GROUP BY assigned_worker_id
UNION ALL
SELECT
worker_id,
COALESCE(SUM(quantity), 0)::int AS accepted_order_count,
MAX(accepted_at) AS last_accepted_at
FROM work_order_shares
WHERE status = 'accepted'
GROUP BY worker_id
) AS accepted_records
GROUP BY worker_id
),
ranked_workers AS (
SELECT
ROW_NUMBER() OVER (
ORDER BY totals.accepted_order_count DESC, totals.last_accepted_at DESC NULLS LAST, wu.id ASC
)::int AS rank,
wu.id AS worker_id,
COALESCE(NULLIF(wu.display_name, ''), wu.username) AS display_name,
wl.name AS level_name,
totals.accepted_order_count
FROM worker_users wu
INNER JOIN accepted_totals totals ON totals.worker_id = wu.id
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
WHERE wu.status = 'active'
)
SELECT rank, worker_id, display_name, level_name, accepted_order_count
FROM ranked_workers
WHERE rank <= $1 OR worker_id = $2
ORDER BY rank ASC
`,
[limit, Number(input.currentWorkerId)],
)
return result.rows.map((row) => ({
rank: Number(row.rank || 0),
workerId: Number(row.worker_id || 0),
displayName: String(row.display_name || '').trim(),
levelName: String(row.level_name || '').trim(),
acceptedOrderCount: Number(row.accepted_order_count || 0),
}))
}
export async function maybeUpgradeWorkerLevelWithClient(
client: PoolClient,
workerId: number,
+11
View File
@@ -9,6 +9,7 @@ import {
createWorkerWithdrawRequest,
getWorkerProfile,
getWorkerSessionSummary,
getWorkerHallLeaderboard,
grabWorkerHallOrder,
joinWorkerSharingOrder,
listWorkerProfileFinanceRequests,
@@ -187,6 +188,16 @@ router.get(
}),
)
router.get(
'/hall/leaderboard',
requireActiveWorker,
createRouteHandler((req) => getWorkerHallLeaderboard(getRequiredWorkerSession(req)), {
successMessage: 'ok',
errorMessage: '读取接单排行榜失败',
scope: '[worker/hall/leaderboard]',
}),
)
router.post(
'/hall/orders/:workOrderId/grab',
requireActiveWorker,
@@ -44,6 +44,7 @@ import {
listAllWorkCategories,
listPendingMaterialWorkOrdersByPlatformOrderId,
listWorkerFinanceRequests,
listWorkerCompletionLeaderboard,
listWorkerSharesByWorker,
listWorkerWalletLedgers,
listWorkerWorkOrderNotes,
@@ -841,6 +842,22 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
}
}
export async function getWorkerHallLeaderboard(session: WorkerSession) {
requireActiveWorkerSession(session)
const entries = await listWorkerCompletionLeaderboard({
limit: 10,
currentWorkerId: session.workerId,
})
const current = entries.find((entry) => entry.workerId === session.workerId)
return {
items: entries.filter((entry) => entry.rank <= 10),
current: {
rank: current?.rank || null,
acceptedOrderCount: current?.acceptedOrderCount || 0,
},
}
}
export async function grabWorkerHallOrder(workOrderId: number | string, session: WorkerSession) {
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
@@ -1,4 +1,4 @@
import { ClockCircleOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import { ClockCircleOutlined, ReloadOutlined, SearchOutlined, TrophyOutlined } from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
@@ -6,6 +6,7 @@ import {
Button,
Card,
Descriptions,
Drawer,
Empty,
Input,
InputNumber,
@@ -23,6 +24,7 @@ import { useEffect, useState } from 'react'
import {
fetchWorkerHallOrders,
fetchWorkerHallLeaderboard,
fetchWorkerMyOrders,
fetchWorkerProfile,
grabWorkerOrder,
@@ -47,6 +49,7 @@ export default function WorkerHallPage() {
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)
@@ -105,6 +108,14 @@ export default function WorkerHallPage() {
retry: false,
})
const leaderboardQuery = useQuery({
queryKey: ['worker-hall-leaderboard'],
queryFn: () => fetchWorkerHallLeaderboard(),
enabled: leaderboardOpen,
staleTime: 30_000,
retry: false,
})
const orders = ordersQuery.data?.data.items || []
const pagination = ordersQuery.data?.data.pagination
const categories = ordersQuery.data?.data.categories || []
@@ -125,6 +136,7 @@ export default function WorkerHallPage() {
queryClient.invalidateQueries({ queryKey: ['worker-profile'] }),
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
queryClient.invalidateQueries({ queryKey: ['worker-hall-leaderboard'] }),
])
}
@@ -219,6 +231,15 @@ export default function WorkerHallPage() {
</div>
<div className="worker-hall-mobile-top-actions">
<Tooltip title="接单排行榜">
<Button
aria-label="接单排行榜"
size="small"
shape="circle"
icon={<TrophyOutlined />}
onClick={() => setLeaderboardOpen(true)}
/>
</Tooltip>
<div className="worker-hall-mobile-autorefresh-switch">
<span className="worker-hall-mobile-autorefresh-text"></span>
<Switch
@@ -442,6 +463,13 @@ export default function WorkerHallPage() {
<div className="worker-page-head-title-row">
<Typography.Title level={3}></Typography.Title>
<div className="worker-hall-head-controls">
<Tooltip title="接单排行榜">
<Button
aria-label="接单排行榜"
icon={<TrophyOutlined />}
onClick={() => setLeaderboardOpen(true)}
/>
</Tooltip>
<Tooltip
title={
autoRefresh
@@ -727,6 +755,77 @@ export default function WorkerHallPage() {
</Space>
) : null}
</Modal>
<Drawer
title={
<Space size={8}>
<TrophyOutlined className="worker-hall-leaderboard-title-icon" />
<span></span>
</Space>
}
open={leaderboardOpen}
placement="right"
width={isMobile ? '88vw' : 360}
onClose={() => setLeaderboardOpen(false)}
>
<div className="worker-hall-leaderboard">
<Typography.Text type="secondary" className="worker-hall-leaderboard-caption">
</Typography.Text>
{leaderboardQuery.isLoading ? (
<div className="worker-hall-leaderboard-loading">
<Spin />
</div>
) : leaderboardQuery.error ? (
<Typography.Text type="danger">
{leaderboardQuery.error instanceof Error
? leaderboardQuery.error.message
: '排行榜读取失败'}
</Typography.Text>
) : (leaderboardQuery.data?.data.items.length || 0) > 0 ? (
<div className="worker-hall-leaderboard-list">
{leaderboardQuery.data?.data.items.map((entry) => (
<div
key={entry.workerId}
className={`worker-hall-leaderboard-row ${
entry.workerId === worker?.workerId ? 'is-current' : ''
}`}
>
<span className={`worker-hall-leaderboard-rank rank-${Math.min(entry.rank, 3)}`}>
{entry.rank}
</span>
<div className="worker-hall-leaderboard-worker">
<Typography.Text strong ellipsis>
{entry.displayName || '未命名打手'}
</Typography.Text>
<Typography.Text type="secondary" ellipsis>
{entry.levelName || '未设置等级'}
</Typography.Text>
</div>
<Typography.Text strong className="worker-hall-leaderboard-count">
{entry.acceptedOrderCount}
</Typography.Text>
</div>
))}
</div>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无完成订单" />
)}
{leaderboardQuery.data ? (
<div className="worker-hall-leaderboard-current">
<Typography.Text type="secondary"></Typography.Text>
<Typography.Text strong>
{leaderboardQuery.data.data.current.rank
? `${leaderboardQuery.data.data.current.rank}`
: '暂未上榜'}
</Typography.Text>
<Typography.Text type="secondary">
{leaderboardQuery.data.data.current.acceptedOrderCount}
</Typography.Text>
</div>
) : null}
</div>
</Drawer>
</>
)
}
+5
View File
@@ -6,6 +6,7 @@ import type {
WorkOrderShare,
WorkerFinanceRequest,
WorkerHallOrdersResponse,
WorkerLeaderboardResponse,
WorkerListResponse,
WorkerLoginResponse,
WorkerProfileResponse,
@@ -109,6 +110,10 @@ export function fetchWorkerHallOrders(params?: Record<string, unknown>) {
)
}
export function fetchWorkerHallLeaderboard() {
return apiGet<WorkerLeaderboardResponse>('/api/v1/worker/hall/leaderboard')
}
export function grabWorkerOrder(workOrderId: number) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/worker/hall/orders/${workOrderId}/grab`,
+204 -76
View File
@@ -675,7 +675,7 @@ body {
align-items: center;
gap: 6px;
overflow-x: auto;
padding-bottom: 2px;
padding: 2px 0 4px 0;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
@@ -709,7 +709,7 @@ body {
font-weight: 600;
}
.worker-orders-pill-badge {
.worker-orders-status-pill-count {
background: rgba(0, 0, 0, 0.06);
color: inherit;
font-size: 10px;
@@ -718,13 +718,13 @@ body {
line-height: 16px;
}
.worker-orders-status-pill.active .worker-orders-pill-badge {
.worker-orders-status-pill.active .worker-orders-status-pill-count {
background: rgba(255, 255, 255, 0.25);
color: #ffffff;
}
/* 搜索与快捷刷新栏 */
.worker-orders-mobile-search-bar {
.worker-orders-mobile-search-row {
display: flex;
align-items: center;
gap: 6px;
@@ -739,30 +739,30 @@ body {
.worker-orders-mobile-list {
display: flex;
flex-direction: column;
gap: 10px;
gap: 12px;
width: 100%;
}
.worker-order-mobile-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 12px;
border-radius: 14px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 8px;
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.04);
gap: 10px;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
}
/* 头部:分类 + 订单号 + 状态 */
.worker-order-mobile-card-top {
.worker-order-mobile-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
gap: 8px;
}
.worker-order-mobile-top-left {
.worker-order-mobile-header-left {
display: flex;
align-items: center;
gap: 6px;
@@ -770,14 +770,14 @@ body {
flex: 1;
}
.worker-order-mobile-category-tag {
.worker-order-mobile-tag {
margin: 0;
font-size: 11px;
line-height: 18px;
padding: 0 6px;
line-height: 18px;
}
.worker-order-mobile-orderno {
.worker-order-mobile-orderno-text {
font-size: 12px;
color: #64748b;
overflow: hidden;
@@ -785,27 +785,23 @@ body {
white-space: nowrap;
}
.worker-order-mobile-top-right {
.worker-order-mobile-header-right {
display: flex;
align-items: center;
gap: 4px;
gap: 6px;
flex-shrink: 0;
}
.worker-order-mobile-deadline {
font-size: 11px;
}
/* 商品名称与奖励 */
/* 商品名称与金额 */
.worker-order-mobile-main-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
gap: 10px;
}
.worker-order-mobile-product-title {
font-size: 14px;
font-size: 15px;
font-weight: 700;
color: #0f172a;
line-height: 1.35;
@@ -816,52 +812,50 @@ body {
flex: 1;
}
.worker-order-mobile-reward {
.worker-order-mobile-price-badge {
display: flex;
flex-direction: column;
align-items: flex-end;
align-items: baseline;
gap: 4px;
flex-shrink: 0;
line-height: 1;
}
.worker-order-mobile-reward-val {
.worker-order-mobile-price-label {
font-size: 11px;
color: #64748b;
}
.worker-order-mobile-price-num {
font-size: 18px;
font-weight: 800;
color: #ea580c;
line-height: 1;
}
.worker-order-mobile-reward-label {
font-size: 10px;
color: #94a3b8;
margin-top: 2px;
}
/* 辅助信息行 */
.worker-order-mobile-meta-row {
/* 辅助信息行 (押金 & 接单时间) */
.worker-order-mobile-sub-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.worker-order-mobile-tag {
margin: 0;
font-size: 11px;
padding: 0 6px;
line-height: 18px;
}
.worker-order-mobile-deposit-text {
font-size: 11px;
justify-content: space-between;
gap: 8px;
font-size: 12px;
color: #64748b;
}
.worker-order-mobile-sub-item {
color: #64748b;
}
.worker-order-mobile-sub-time {
color: #94a3b8;
font-size: 11px;
}
/* 问题单提示 */
.worker-order-mobile-problem-box {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 6px;
padding: 6px 10px;
border-radius: 8px;
padding: 8px 10px;
color: #b91c1c;
font-size: 12px;
line-height: 1.4;
@@ -872,7 +866,7 @@ body {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 8px 10px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 6px;
@@ -880,7 +874,7 @@ body {
.worker-order-mobile-credentials-grid {
display: grid;
gap: 3px;
gap: 4px;
font-size: 12px;
}
@@ -907,15 +901,15 @@ body {
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
margin-top: 4px;
}
.worker-order-mobile-copy-toolbar {
display: flex;
gap: 6px;
margin-top: 4px;
gap: 8px;
margin-top: 6px;
border-top: 1px dashed #e2e8f0;
padding-top: 6px;
padding-top: 8px;
}
/* 验收凭据条 */
@@ -923,41 +917,69 @@ body {
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
padding: 6px 10px;
padding: 8px 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-direction: column;
gap: 6px;
}
.worker-order-mobile-acceptance-left {
.worker-order-mobile-acceptance-files,
.worker-order-mobile-draft-files {
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
flex-wrap: wrap;
}
.worker-order-mobile-acceptance-tag {
font-size: 12px;
color: #166534;
font-weight: 600;
}
.worker-order-mobile-acceptance-text {
font-size: 12px;
color: #166534;
font-weight: 500;
line-height: 1.35;
}
.worker-order-mobile-acceptance-time {
font-size: 11px;
color: #15803d;
}
/* 底部操作行 */
.worker-order-mobile-actions-row {
/* 底部操作与备注行 */
.worker-order-mobile-footer-row {
display: flex;
align-items: center;
justify-content: flex-end;
justify-content: space-between;
gap: 8px;
border-top: 1px solid #f1f5f9;
padding-top: 8px;
border-top: 1px dashed #f1f5f9;
padding-top: 10px;
margin-top: 2px;
}
.worker-order-mobile-action-btn {
.worker-order-mobile-note-block {
min-width: 0;
flex: 1;
}
.worker-order-mobile-add-note-btn {
border: 1px dashed #cbd5e1;
background: transparent;
color: #64748b;
border-radius: 6px;
padding: 4px 8px;
font-size: 11px;
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;
}
@@ -1626,6 +1648,112 @@ body {
padding: 0 16px;
}
.worker-hall-leaderboard {
display: flex;
flex-direction: column;
gap: 12px;
}
.worker-hall-leaderboard-title-icon {
color: #d97706;
}
.worker-hall-leaderboard-caption {
font-size: 12px;
}
.worker-hall-leaderboard-loading {
display: flex;
justify-content: center;
padding: 36px 0;
}
.worker-hall-leaderboard-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.worker-hall-leaderboard-row {
display: grid;
grid-template-columns: 30px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
min-height: 56px;
padding: 8px 10px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #ffffff;
}
.worker-hall-leaderboard-row.is-current {
border-color: #fb923c;
background: #fff7ed;
}
.worker-hall-leaderboard-rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: #f1f5f9;
color: #475569;
font-size: 12px;
font-weight: 700;
}
.worker-hall-leaderboard-rank.rank-1 {
background: #fef3c7;
color: #a16207;
}
.worker-hall-leaderboard-rank.rank-2 {
background: #e2e8f0;
color: #475569;
}
.worker-hall-leaderboard-rank.rank-3 {
background: #fee2e2;
color: #b45309;
}
.worker-hall-leaderboard-worker {
display: flex;
flex-direction: column;
min-width: 0;
gap: 2px;
}
.worker-hall-leaderboard-worker .ant-typography {
display: block;
font-size: 13px;
}
.worker-hall-leaderboard-worker .ant-typography-secondary {
font-size: 11px;
}
.worker-hall-leaderboard-count {
color: #c2410c;
white-space: nowrap;
}
.worker-hall-leaderboard-current {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 4px 12px;
padding-top: 12px;
border-top: 1px solid #e2e8f0;
}
.worker-hall-leaderboard-current .ant-typography:last-child {
grid-column: 1 / -1;
font-size: 12px;
}
/* ===== 打手端响应式断点 ===== */
@media (max-width: 768px) {
@@ -291,6 +291,22 @@ export type WorkerHallOrdersResponse = {
visibleDelaySeconds?: number
}
export type WorkerLeaderboardEntry = {
rank: number
workerId: number
displayName: string
levelName: string
acceptedOrderCount: number
}
export type WorkerLeaderboardResponse = {
items: WorkerLeaderboardEntry[]
current: {
rank: number | null
acceptedOrderCount: number
}
}
export type CollectField = {
key: string
label: string