From 18ac95ca27aed0055efec911aa0cab8db408f01f Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 13 Sep 2026 11:14:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(worker-hall):=20=E6=8E=A5=E5=8D=95?= =?UTF-8?q?=E5=A4=A7=E5=8E=85=E6=94=AF=E6=8C=81=E5=86=85=E9=83=A8=E6=89=93?= =?UTF-8?q?=E6=89=8B=E5=8F=AF=E8=A7=81=E8=AE=A2=E5=8D=95=E5=B7=B2=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=97=B6=E9=95=BF=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker-platform/worker-hall-service.ts | 7 ++- .../src/pages/worker/WorkerHallPage.tsx | 30 +++++++++++++ apps/frontend/src/styles/worker.css | 18 ++++++++ apps/frontend/src/utils/date-time.ts | 31 +++++++++++++ apps/frontend/tests/date-time.test.ts | 43 +++++++++++++++++++ 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/tests/date-time.test.ts diff --git a/apps/backend/src/services/worker-platform/worker-hall-service.ts b/apps/backend/src/services/worker-platform/worker-hall-service.ts index 014c94b2..a78b65cb 100644 --- a/apps/backend/src/services/worker-platform/worker-hall-service.ts +++ b/apps/backend/src/services/worker-platform/worker-hall-service.ts @@ -77,11 +77,16 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work if (share.status === 'joined') progress.pendingSubmissionCount += 1 sharingProgressByOrderId.set(orderId, progress) } + const isInternalWorker = String(worker.worker_type || '').trim() === 'internal' return { items: items.map((item) => { const myShare = myShareByOrderId.get(Number(item.id)) + const mappedOrder = mapWorkOrderForWorker(item, permissions) return { - ...mapWorkOrderForWorker(item, permissions), + ...mappedOrder, + publishedAt: isInternalWorker + ? item.published_at || item.hall_queued_at || item.created_at || null + : null, myShare: myShare ? mapWorkOrderShare(myShare) : null, sharingProgress: sharingProgressByOrderId.get(Number(item.id)) || { joinedQuantity: 0, diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index 94179ea6..a7f62b01 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -62,6 +62,7 @@ import { resolveHallRemainingQuantity, resolveHallRewardAmount, } from '@/utils/worker-product-name' +import { formatDateTime, formatPublishedElapsedTime } from '@/utils/date-time' type HallSummary = { pendingAcceptanceCount: number @@ -88,8 +89,16 @@ export default function WorkerHallPage() { const [failedImageKeys, setFailedImageKeys] = useState>({}) const [confirmOpen, setConfirmOpen] = useState(false) const [grabConfirmEnabled, setGrabConfirmEnabled] = useState(isGrabConfirmEnabled) + const [nowMs, setNowMs] = useState(() => Date.now()) const loadMoreRef = useRef(null) + useEffect(() => { + const timer = setInterval(() => { + setNowMs(Date.now()) + }, 30_000) + return () => clearInterval(timer) + }, []) + const categoriesQuery = useQuery({ queryKey: ['worker-hall-categories'], queryFn: () => fetchWorkerHallCategories(), @@ -227,6 +236,7 @@ export default function WorkerHallPage() { setFailedImageKeys({}) }, [allCategories.length, allIconUrl]) const worker = profileQuery.data?.data.worker + const isInternalWorker = worker?.workerType === 'internal' const activeCount = Number(profileQuery.data?.data.summary.activeOrderCount || 0) const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 0 const problemCount = hallSummaryQuery.data?.problemCount || 0 @@ -623,6 +633,16 @@ export default function WorkerHallPage() { : order.categoryName} ) : null} + {isInternalWorker && order.publishedAt ? ( + + } + className="worker-hall-published-tag" + > + {formatPublishedElapsedTime(order.publishedAt, nowMs)} + + + ) : null} {order.sharing?.enabled ? ( 拼单 {order.sharingProgress?.joinedQuantity || 0}/ @@ -1047,6 +1067,16 @@ export default function WorkerHallPage() { ? `${order.parentCategoryName} · ${order.categoryName}` : order.categoryName || '默认分类'} + {isInternalWorker && order.publishedAt ? ( + + } + className="worker-hall-published-tag" + > + {formatPublishedElapsedTime(order.publishedAt, nowMs)} + + + ) : null} {order.sharing?.enabled ? 拼单 : null} {Number(order.timeoutMinutes || 0) > 0 ? ( }> diff --git a/apps/frontend/src/styles/worker.css b/apps/frontend/src/styles/worker.css index dcc32a5b..73b91b0d 100644 --- a/apps/frontend/src/styles/worker.css +++ b/apps/frontend/src/styles/worker.css @@ -1122,6 +1122,24 @@ 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 { font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; diff --git a/apps/frontend/src/utils/date-time.ts b/apps/frontend/src/utils/date-time.ts index 1567b29f..fe36c398 100644 --- a/apps/frontend/src/utils/date-time.ts +++ b/apps/frontend/src/utils/date-time.ts @@ -52,3 +52,34 @@ export function normalizeDateTimeValue(value: unknown): unknown { export function stringifyDisplayJson(value: unknown, space = 2) { 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` +} diff --git a/apps/frontend/tests/date-time.test.ts b/apps/frontend/tests/date-time.test.ts new file mode 100644 index 00000000..d7da4dac --- /dev/null +++ b/apps/frontend/tests/date-time.test.ts @@ -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', + ) +})