feat(worker-hall): 接单大厅支持内部打手可见订单已发布时长标签
This commit is contained in:
@@ -77,11 +77,16 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
|
|||||||
if (share.status === 'joined') progress.pendingSubmissionCount += 1
|
if (share.status === 'joined') progress.pendingSubmissionCount += 1
|
||||||
sharingProgressByOrderId.set(orderId, progress)
|
sharingProgressByOrderId.set(orderId, progress)
|
||||||
}
|
}
|
||||||
|
const isInternalWorker = String(worker.worker_type || '').trim() === 'internal'
|
||||||
return {
|
return {
|
||||||
items: items.map((item) => {
|
items: items.map((item) => {
|
||||||
const myShare = myShareByOrderId.get(Number(item.id))
|
const myShare = myShareByOrderId.get(Number(item.id))
|
||||||
|
const mappedOrder = mapWorkOrderForWorker(item, permissions)
|
||||||
return {
|
return {
|
||||||
...mapWorkOrderForWorker(item, permissions),
|
...mappedOrder,
|
||||||
|
publishedAt: isInternalWorker
|
||||||
|
? item.published_at || item.hall_queued_at || item.created_at || null
|
||||||
|
: null,
|
||||||
myShare: myShare ? mapWorkOrderShare(myShare) : null,
|
myShare: myShare ? mapWorkOrderShare(myShare) : null,
|
||||||
sharingProgress: sharingProgressByOrderId.get(Number(item.id)) || {
|
sharingProgress: sharingProgressByOrderId.get(Number(item.id)) || {
|
||||||
joinedQuantity: 0,
|
joinedQuantity: 0,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
resolveHallRemainingQuantity,
|
resolveHallRemainingQuantity,
|
||||||
resolveHallRewardAmount,
|
resolveHallRewardAmount,
|
||||||
} from '@/utils/worker-product-name'
|
} from '@/utils/worker-product-name'
|
||||||
|
import { formatDateTime, formatPublishedElapsedTime } from '@/utils/date-time'
|
||||||
|
|
||||||
type HallSummary = {
|
type HallSummary = {
|
||||||
pendingAcceptanceCount: number
|
pendingAcceptanceCount: number
|
||||||
@@ -88,8 +89,16 @@ export default function WorkerHallPage() {
|
|||||||
const [failedImageKeys, setFailedImageKeys] = useState<Record<string | number, boolean>>({})
|
const [failedImageKeys, setFailedImageKeys] = useState<Record<string | number, boolean>>({})
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||||
const [grabConfirmEnabled, setGrabConfirmEnabled] = useState(isGrabConfirmEnabled)
|
const [grabConfirmEnabled, setGrabConfirmEnabled] = useState(isGrabConfirmEnabled)
|
||||||
|
const [nowMs, setNowMs] = useState(() => Date.now())
|
||||||
const loadMoreRef = useRef<HTMLDivElement | null>(null)
|
const loadMoreRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setNowMs(Date.now())
|
||||||
|
}, 30_000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const categoriesQuery = useQuery({
|
const categoriesQuery = useQuery({
|
||||||
queryKey: ['worker-hall-categories'],
|
queryKey: ['worker-hall-categories'],
|
||||||
queryFn: () => fetchWorkerHallCategories(),
|
queryFn: () => fetchWorkerHallCategories(),
|
||||||
@@ -227,6 +236,7 @@ export default function WorkerHallPage() {
|
|||||||
setFailedImageKeys({})
|
setFailedImageKeys({})
|
||||||
}, [allCategories.length, allIconUrl])
|
}, [allCategories.length, allIconUrl])
|
||||||
const worker = profileQuery.data?.data.worker
|
const worker = profileQuery.data?.data.worker
|
||||||
|
const isInternalWorker = worker?.workerType === 'internal'
|
||||||
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
|
||||||
const problemCount = hallSummaryQuery.data?.problemCount || 0
|
const problemCount = hallSummaryQuery.data?.problemCount || 0
|
||||||
@@ -623,6 +633,16 @@ export default function WorkerHallPage() {
|
|||||||
: order.categoryName}
|
: order.categoryName}
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isInternalWorker && order.publishedAt ? (
|
||||||
|
<Tooltip title={`发布时间: ${formatDateTime(order.publishedAt)}`}>
|
||||||
|
<Tag
|
||||||
|
icon={<ClockCircleOutlined />}
|
||||||
|
className="worker-hall-published-tag"
|
||||||
|
>
|
||||||
|
{formatPublishedElapsedTime(order.publishedAt, nowMs)}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
{order.sharing?.enabled ? (
|
{order.sharing?.enabled ? (
|
||||||
<Tag color="purple" className="worker-hall-mobile-tag">
|
<Tag color="purple" className="worker-hall-mobile-tag">
|
||||||
拼单 {order.sharingProgress?.joinedQuantity || 0}/
|
拼单 {order.sharingProgress?.joinedQuantity || 0}/
|
||||||
@@ -1047,6 +1067,16 @@ export default function WorkerHallPage() {
|
|||||||
? `${order.parentCategoryName} · ${order.categoryName}`
|
? `${order.parentCategoryName} · ${order.categoryName}`
|
||||||
: order.categoryName || '默认分类'}
|
: order.categoryName || '默认分类'}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{isInternalWorker && order.publishedAt ? (
|
||||||
|
<Tooltip title={`发布时间: ${formatDateTime(order.publishedAt)}`}>
|
||||||
|
<Tag
|
||||||
|
icon={<ClockCircleOutlined />}
|
||||||
|
className="worker-hall-published-tag"
|
||||||
|
>
|
||||||
|
{formatPublishedElapsedTime(order.publishedAt, nowMs)}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
{order.sharing?.enabled ? <Tag color="purple">拼单</Tag> : null}
|
{order.sharing?.enabled ? <Tag color="purple">拼单</Tag> : null}
|
||||||
{Number(order.timeoutMinutes || 0) > 0 ? (
|
{Number(order.timeoutMinutes || 0) > 0 ? (
|
||||||
<Tag color="orange" icon={<ClockCircleOutlined />}>
|
<Tag color="orange" icon={<ClockCircleOutlined />}>
|
||||||
|
|||||||
@@ -1122,6 +1122,24 @@
|
|||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.worker-hall-published-tag {
|
||||||
|
background: #fff7ed !important;
|
||||||
|
color: #ea580c !important;
|
||||||
|
border: 1px solid #fed7aa !important;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 11px !important;
|
||||||
|
line-height: 18px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 !important;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worker-hall-published-tag .anticon {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.worker-hall-card-orderno {
|
.worker-hall-card-orderno {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
|||||||
@@ -52,3 +52,34 @@ export function normalizeDateTimeValue(value: unknown): unknown {
|
|||||||
export function stringifyDisplayJson(value: unknown, space = 2) {
|
export function stringifyDisplayJson(value: unknown, space = 2) {
|
||||||
return JSON.stringify(normalizeDateTimeValue(value), null, space)
|
return JSON.stringify(normalizeDateTimeValue(value), null, space)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 抢单大厅发布耗时格式化(仅内部打手可见):发布 <1m / 发布 5m / 发布 1h 20m / 发布 1d 4h */
|
||||||
|
export function formatPublishedElapsedTime(
|
||||||
|
value: string | null | undefined,
|
||||||
|
nowMs = Date.now(),
|
||||||
|
): string {
|
||||||
|
const normalized = String(value || '').trim()
|
||||||
|
if (!normalized) return ''
|
||||||
|
|
||||||
|
const date = new Date(normalized)
|
||||||
|
const time = date.getTime()
|
||||||
|
if (Number.isNaN(time)) return ''
|
||||||
|
|
||||||
|
const diffSeconds = Math.max(0, Math.floor((nowMs - time) / 1000))
|
||||||
|
|
||||||
|
if (diffSeconds < 60) {
|
||||||
|
return '发布 <1m'
|
||||||
|
}
|
||||||
|
const diffMinutes = Math.floor(diffSeconds / 60)
|
||||||
|
if (diffMinutes < 60) {
|
||||||
|
return `发布 ${diffMinutes}m`
|
||||||
|
}
|
||||||
|
const diffHours = Math.floor(diffMinutes / 60)
|
||||||
|
const remMinutes = diffMinutes % 60
|
||||||
|
if (diffHours < 24) {
|
||||||
|
return remMinutes > 0 ? `发布 ${diffHours}h ${remMinutes}m` : `发布 ${diffHours}h`
|
||||||
|
}
|
||||||
|
const diffDays = Math.floor(diffHours / 24)
|
||||||
|
const remHours = diffHours % 24
|
||||||
|
return remHours > 0 ? `发布 ${diffDays}d ${remHours}h` : `发布 ${diffDays}d`
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import { formatDateTime, formatPublishedElapsedTime } from '../src/utils/date-time.ts'
|
||||||
|
|
||||||
|
test('formatPublishedElapsedTime formats elapsed time for worker hall', () => {
|
||||||
|
const baseTime = new Date('2026-09-13T10:00:00.000Z').getTime()
|
||||||
|
|
||||||
|
// Invalid or empty
|
||||||
|
assert.equal(formatPublishedElapsedTime(null, baseTime), '')
|
||||||
|
assert.equal(formatPublishedElapsedTime('', baseTime), '')
|
||||||
|
assert.equal(formatPublishedElapsedTime('invalid-date', baseTime), '')
|
||||||
|
|
||||||
|
// <1m
|
||||||
|
assert.equal(
|
||||||
|
formatPublishedElapsedTime(new Date(baseTime - 30 * 1000).toISOString(), baseTime),
|
||||||
|
'发布 <1m',
|
||||||
|
)
|
||||||
|
|
||||||
|
// 5m
|
||||||
|
assert.equal(
|
||||||
|
formatPublishedElapsedTime(new Date(baseTime - 5 * 60 * 1000).toISOString(), baseTime),
|
||||||
|
'发布 5m',
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1h
|
||||||
|
assert.equal(
|
||||||
|
formatPublishedElapsedTime(new Date(baseTime - 60 * 60 * 1000).toISOString(), baseTime),
|
||||||
|
'发布 1h',
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1h 25m
|
||||||
|
assert.equal(
|
||||||
|
formatPublishedElapsedTime(new Date(baseTime - 85 * 60 * 1000).toISOString(), baseTime),
|
||||||
|
'发布 1h 25m',
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2d 3h
|
||||||
|
assert.equal(
|
||||||
|
formatPublishedElapsedTime(new Date(baseTime - (48 + 3) * 3600 * 1000).toISOString(), baseTime),
|
||||||
|
'发布 2d 3h',
|
||||||
|
)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user