From 1cebdd0487c98d21b6fbf039c19f39cfdf1c3aaf Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 23 Aug 2026 17:37:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8E=A5=E5=8D=95=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E5=92=8C=E5=A4=A7=E5=8E=85=E5=88=86=E7=B1=BB=E9=85=8D?= =?UTF-8?q?=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositories/worker-platform-repo.test.ts | 18 ++++- .../src/repositories/worker-platform/types.ts | 1 + .../worker-platform/work-order-grab-repo.ts | 2 + .../worker-platform/work-order-query-repo.ts | 62 +++++++++++------ .../worker-platform/work-order-share-repo.ts | 2 + .../admin-mock-work-order-service.ts | 15 +++- .../admin-work-order-management-service.ts | 1 + .../admin-work-order-query-service.ts | 2 + .../admin-worker-platform-summary-service.ts | 69 ++++++++++++------- .../worker-hall-action-service.ts | 3 + .../worker-hall-config-service.test.ts | 27 ++++++-- .../worker-hall-config-service.ts | 18 ++++- .../worker-platform/worker-hall-service.ts | 1 + .../pages/admin/panels/HallConfigPanel.tsx | 63 +++++++++++++++-- .../admin/panels/WorkOrderPanelParts.tsx | 40 ++++++++++- .../pages/admin/panels/WorkOrdersPanel.tsx | 22 ++++++ .../src/services/admin/worker-platform.ts | 5 +- apps/frontend/src/types/worker-platform.ts | 7 +- 18 files changed, 297 insertions(+), 61 deletions(-) diff --git a/apps/backend/src/repositories/worker-platform-repo.test.ts b/apps/backend/src/repositories/worker-platform-repo.test.ts index 8410d684..19c56f1d 100644 --- a/apps/backend/src/repositories/worker-platform-repo.test.ts +++ b/apps/backend/src/repositories/worker-platform-repo.test.ts @@ -101,5 +101,21 @@ test('大厅容量先确定全局候选集合,再应用页面筛选条件', () assert.match(query.whereClause, /LIMIT \$4/) const candidateSql = query.whereClause.slice(query.whereClause.indexOf('candidate.status')) assert.doesNotMatch(candidateSql, /candidate\.category_id|candidate.*测试商品/) - assert.deepEqual(query.params, [['open'], '%测试商品%', 3, 50, '2026-08-21T00:00:00.000Z']) + assert.deepEqual(query.params, [['open'], '%测试商品%', 3, 50, '{}', '2026-08-21T00:00:00.000Z']) +}) + +test('后台接单工单支持按接单分类筛选', () => { + const query = buildWorkOrderWhere({ categoryId: 7 }) + assert.match(query.whereClause, /wo\.category_id = \$1/) + assert.deepEqual(query.params, [7]) +}) + +test('大厅候选按分类上限取队首后再受全局上限限制', () => { + const query = buildWorkOrderWhere({ + hallCandidateLimit: 20, + hallCategoryLimits: { '1': 3, '2': 5 }, + }) + assert.match(query.whereClause, /ROW_NUMBER\(\) OVER/) + assert.match(query.whereClause, /category_rank/) + assert.deepEqual(query.params, [20, '{"1":3,"2":5}']) }) diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 1f8884c0..b3c5d38c 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -431,6 +431,7 @@ export type ListInput = { workerSharingId?: number /** 大厅候选集合的总展示上限;筛选与分页在候选集合内执行。 */ hallCandidateLimit?: number + hallCategoryLimits?: Record visibleAfterIso?: string /** 抢单大厅隐藏已被拼完的工单。 */ excludeFilledSharing?: boolean diff --git a/apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts index 37ec9c12..21c9247f 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts @@ -14,6 +14,7 @@ export async function grabWorkOrder(input: { depositAmount: number maxActiveOrders: number hallCandidateLimit?: number + hallCategoryLimits?: Record visibleAfterIso?: string deadlineAt: string | null now: string @@ -42,6 +43,7 @@ export async function grabWorkOrder(input: { !(await isWorkOrderWithinHallCapacityWithClient(client, { workOrderId: input.workOrderId, hallCandidateLimit: input.hallCandidateLimit, + ...(input.hallCategoryLimits ? { hallCategoryLimits: input.hallCategoryLimits } : {}), visibleAfterIso: input.visibleAfterIso, })) ) { diff --git a/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts index 777f505c..b55d4b29 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts @@ -226,6 +226,7 @@ export async function listWorkOrders({ categoryId = 0, workerSharingId = 0, hallCandidateLimit = 0, + hallCategoryLimits = {}, visibleAfterIso = '', excludeFilledSharing = false, vipEvidencePending = false, @@ -247,6 +248,7 @@ export async function listWorkOrders({ categoryId, workerSharingId, hallCandidateLimit, + hallCategoryLimits, visibleAfterIso, excludeFilledSharing, vipEvidencePending, @@ -652,6 +654,7 @@ export function buildWorkOrderWhere({ categoryId = 0, workerSharingId = 0, hallCandidateLimit = 0, + hallCategoryLimits = {}, visibleAfterIso = '', excludeFilledSharing = false, vipEvidencePending = false, @@ -843,7 +846,9 @@ export function buildWorkOrderWhere({ } if (hallCandidateLimit > 0) { params.push(hallCandidateLimit) - filters.push(`wo.id IN (${buildHallCandidateIdsSql(`$${params.length}`)})`) + const limitPlaceholder = `$${params.length}` + params.push(JSON.stringify(hallCategoryLimits)) + filters.push(`wo.id IN (${buildHallCandidateIdsSql(limitPlaceholder, `$${params.length}`)})`) } if (visibleAfterIso) { params.push(visibleAfterIso) @@ -863,23 +868,34 @@ export function buildWorkOrderWhere({ } /** 大厅容量先选出全局队首,分类、关键词和 VIP 延迟只能在该集合内继续筛选。 */ -function buildHallCandidateIdsSql(limitPlaceholder: string) { +function buildHallCandidateIdsSql(limitPlaceholder: string, categoryLimitsPlaceholder: string) { return ` - SELECT candidate.id - FROM work_orders candidate - LEFT JOIN ( - SELECT - work_order_id, - SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity - FROM work_order_shares - GROUP BY work_order_id - ) candidate_shares ON candidate_shares.work_order_id = candidate.id - WHERE candidate.status = 'open' - AND ( - candidate.sharing_enabled IS NOT TRUE - OR COALESCE(candidate_shares.active_share_quantity, 0) < candidate.sharing_total_quantity - ) - ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC + SELECT ranked.id + FROM ( + SELECT candidate.id, candidate.category_id, candidate.hall_queued_at, + ROW_NUMBER() OVER ( + PARTITION BY candidate.category_id + ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC + ) AS category_rank + FROM work_orders candidate + LEFT JOIN ( + SELECT + work_order_id, + SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity + FROM work_order_shares + GROUP BY work_order_id + ) candidate_shares ON candidate_shares.work_order_id = candidate.id + WHERE candidate.status = 'open' + AND ( + candidate.sharing_enabled IS NOT TRUE + OR COALESCE(candidate_shares.active_share_quantity, 0) < candidate.sharing_total_quantity + ) + ) ranked + WHERE ranked.category_rank <= COALESCE( + NULLIF(${categoryLimitsPlaceholder}::jsonb ->> ranked.category_id::text, '')::int, + ${limitPlaceholder} + ) + ORDER BY ranked.hall_queued_at ASC NULLS LAST, ranked.id ASC LIMIT ${limitPlaceholder} ` } @@ -887,7 +903,12 @@ function buildHallCandidateIdsSql(limitPlaceholder: string) { /** 抢单操作在事务中复核容量与当前打手的可见延迟,避免通过订单 ID 绕过大厅队列。 */ export async function isWorkOrderWithinHallCapacityWithClient( client: PoolClient, - input: { workOrderId: number; hallCandidateLimit: number; visibleAfterIso?: string | undefined }, + input: { + workOrderId: number + hallCandidateLimit: number + hallCategoryLimits?: Record + visibleAfterIso?: string | undefined + }, ) { const result = await client.query<{ visible: boolean }>( ` @@ -895,13 +916,14 @@ export async function isWorkOrderWithinHallCapacityWithClient( SELECT 1 FROM work_orders wo WHERE wo.id = $1 - AND wo.id IN (${buildHallCandidateIdsSql('$2')}) - AND ($3::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $3) + AND wo.id IN (${buildHallCandidateIdsSql('$2', '$3')}) + AND ($4::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $4) ) AS visible `, [ input.workOrderId, input.hallCandidateLimit, + JSON.stringify(input.hallCategoryLimits || {}), input.visibleAfterIso ? input.visibleAfterIso : null, ], ) diff --git a/apps/backend/src/repositories/worker-platform/work-order-share-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-share-repo.ts index 752b5e43..b1ae56fb 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-share-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-share-repo.ts @@ -103,6 +103,7 @@ export async function joinWorkOrderShare(input: { takeRemaining?: boolean maxActiveOrders: number hallCandidateLimit?: number + hallCategoryLimits?: Record visibleAfterIso?: string now: string }): Promise<{ @@ -137,6 +138,7 @@ export async function joinWorkOrderShare(input: { !(await isWorkOrderWithinHallCapacityWithClient(client, { workOrderId: input.workOrderId, hallCandidateLimit: input.hallCandidateLimit, + ...(input.hallCategoryLimits ? { hallCategoryLimits: input.hallCategoryLimits } : {}), visibleAfterIso: input.visibleAfterIso, })) ) { diff --git a/apps/backend/src/services/worker-platform/admin-mock-work-order-service.ts b/apps/backend/src/services/worker-platform/admin-mock-work-order-service.ts index 0398d772..d20f750a 100644 --- a/apps/backend/src/services/worker-platform/admin-mock-work-order-service.ts +++ b/apps/backend/src/services/worker-platform/admin-mock-work-order-service.ts @@ -6,6 +6,7 @@ import { import { createWorkOrder, createWorkOrderEvent, + getWorkCategoryById, listWorkOrdersByPlatformOrderId, } from '../../repositories/worker-platform/index.js' import type { JsonObject } from '../../types/json.js' @@ -47,6 +48,18 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) { : 0 : normalizeAmountFen(payload.requiredDepositAmount, 0) const paymentFen = input.paymentAmount + const requestedCategoryId = Number(payload.categoryId || payload.category_id || 0) + let categoryId = defaults.category?.id || null + if (requestedCategoryId > 0) { + const category = await getWorkCategoryById(requestedCategoryId) + if (!category || category.status !== 'active') { + throw createHttpError('所选接单分类不存在或已停用', { + statusCode: 409, + errorCode: 'mock_work_order_category_invalid', + }) + } + categoryId = Number(category.id) + } const materialComplete = payload.materialComplete === true || payload.materialComplete === 'true' const fields = normalizeRequirementFields(payload.fields) const { platformOrderId, productName, shop } = input @@ -92,7 +105,7 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) { workOrderNo: randomId('WO'), platformOrderId, productName, - categoryId: defaults.category?.id || null, + categoryId, status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL, rewardAmount, requiredDepositAmount, diff --git a/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts b/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts index 3cffee3a..54a0b4ad 100644 --- a/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts +++ b/apps/backend/src/services/worker-platform/admin-work-order-management-service.ts @@ -250,6 +250,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN workOrderId: Number(workOrder.id), statuses: [WORK_ORDER_STATUS.OPEN], hallCandidateLimit: hallConfig.maxVisibleOrders, + hallCategoryLimits: hallConfig.categoryMaxVisibleOrders, excludeFilledSharing: true, }) return { diff --git a/apps/backend/src/services/worker-platform/admin-work-order-query-service.ts b/apps/backend/src/services/worker-platform/admin-work-order-query-service.ts index 80d8c0bd..fc99dbbb 100644 --- a/apps/backend/src/services/worker-platform/admin-work-order-query-service.ts +++ b/apps/backend/src/services/worker-platform/admin-work-order-query-service.ts @@ -30,6 +30,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) { const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) const workOrderId = normalizeOptionalId(query.workOrderId ?? query.work_order_id) || 0 + const categoryId = normalizeOptionalId(query.categoryId ?? query.category_id) || 0 const workerId = normalizeOptionalId(query.workerId ?? query.worker_id) || 0 const workerLevelIds = normalizeStatuses(query.workerLevelIds ?? query.worker_level_ids) .map(normalizeOptionalId) @@ -43,6 +44,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) { const filters = { statuses, workOrderId, + categoryId, keyword: String(query.keyword || '').trim(), workerSharingId: workerId, workerLevelIds, diff --git a/apps/backend/src/services/worker-platform/admin-worker-platform-summary-service.ts b/apps/backend/src/services/worker-platform/admin-worker-platform-summary-service.ts index 9a864a90..11787f54 100644 --- a/apps/backend/src/services/worker-platform/admin-worker-platform-summary-service.ts +++ b/apps/backend/src/services/worker-platform/admin-worker-platform-summary-service.ts @@ -5,9 +5,44 @@ import { getWorkerHallConfig, saveWorkerHallConfig } from './worker-hall-config- export async function getAdminWorkerPlatformSummary() { const hallConfig = getWorkerHallConfig() - const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([ - listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }), - listWorkOrders({ page: 1, pageSize: 1, statuses: [WORK_ORDER_STATUS.PENDING_MATERIAL] }), + const [pendingWorkers, pendingMaterial, openOrders, visibleOpenOrders, inProgressOrders] = + await Promise.all([ + listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }), + listWorkOrders({ page: 1, pageSize: 1, statuses: [WORK_ORDER_STATUS.PENDING_MATERIAL] }), + listWorkOrders({ + page: 1, + pageSize: 1, + statuses: [WORK_ORDER_STATUS.OPEN], + excludeFilledSharing: true, + }), + listWorkOrders({ + page: 1, + pageSize: 1, + statuses: [WORK_ORDER_STATUS.OPEN], + hallCandidateLimit: hallConfig.maxVisibleOrders, + hallCategoryLimits: hallConfig.categoryMaxVisibleOrders, + excludeFilledSharing: true, + }), + listWorkOrders({ + page: 1, + pageSize: 1, + statuses: [WORK_ORDER_STATUS.IN_PROGRESS], + useOperationalStatus: true, + }), + ]) + return { + pendingWorkerCount: pendingWorkers.total, + pendingMaterialCount: pendingMaterial.total, + openOrderCount: visibleOpenOrders.total, + queuedHallOrderCount: Math.max(0, openOrders.total - visibleOpenOrders.total), + hallMaxVisibleOrders: hallConfig.maxVisibleOrders, + inProgressOrderCount: inProgressOrders.total, + } +} + +export async function getAdminWorkerHallConfig() { + const config = getWorkerHallConfig() + const [openOrders, visibleOpenOrders] = await Promise.all([ listWorkOrders({ page: 1, pageSize: 1, @@ -17,32 +52,16 @@ export async function getAdminWorkerPlatformSummary() { listWorkOrders({ page: 1, pageSize: 1, - statuses: [WORK_ORDER_STATUS.IN_PROGRESS], - useOperationalStatus: true, + statuses: [WORK_ORDER_STATUS.OPEN], + hallCandidateLimit: config.maxVisibleOrders, + hallCategoryLimits: config.categoryMaxVisibleOrders, + excludeFilledSharing: true, }), ]) - return { - pendingWorkerCount: pendingWorkers.total, - pendingMaterialCount: pendingMaterial.total, - openOrderCount: Math.min(openOrders.total, hallConfig.maxVisibleOrders), - queuedHallOrderCount: Math.max(0, openOrders.total - hallConfig.maxVisibleOrders), - hallMaxVisibleOrders: hallConfig.maxVisibleOrders, - inProgressOrderCount: inProgressOrders.total, - } -} - -export async function getAdminWorkerHallConfig() { - const config = getWorkerHallConfig() - const openOrders = await listWorkOrders({ - page: 1, - pageSize: 1, - statuses: [WORK_ORDER_STATUS.OPEN], - excludeFilledSharing: true, - }) return { ...config, - visibleOrderCount: Math.min(openOrders.total, config.maxVisibleOrders), - queuedOrderCount: Math.max(0, openOrders.total - config.maxVisibleOrders), + visibleOrderCount: visibleOpenOrders.total, + queuedOrderCount: Math.max(0, openOrders.total - visibleOpenOrders.total), } } diff --git a/apps/backend/src/services/worker-platform/worker-hall-action-service.ts b/apps/backend/src/services/worker-platform/worker-hall-action-service.ts index 3310e6c9..cc93db4b 100644 --- a/apps/backend/src/services/worker-platform/worker-hall-action-service.ts +++ b/apps/backend/src/services/worker-platform/worker-hall-action-service.ts @@ -55,6 +55,7 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session: takeRemaining: true, maxActiveOrders: permissions.maxActiveOrders, hallCandidateLimit: hallConfig.maxVisibleOrders, + hallCategoryLimits: hallConfig.categoryMaxVisibleOrders, visibleAfterIso, now, }) @@ -91,6 +92,7 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session: depositAmount: freezeAmount, maxActiveOrders: permissions.maxActiveOrders, hallCandidateLimit: hallConfig.maxVisibleOrders, + hallCategoryLimits: hallConfig.categoryMaxVisibleOrders, visibleAfterIso, deadlineAt: resolveWorkOrderDeadlineAt(workOrder, now), now, @@ -143,6 +145,7 @@ export async function joinWorkerSharingOrder( quantity, maxActiveOrders: permissions.maxActiveOrders, hallCandidateLimit: hallConfig.maxVisibleOrders, + hallCategoryLimits: hallConfig.categoryMaxVisibleOrders, visibleAfterIso, now, }) diff --git a/apps/backend/src/services/worker-platform/worker-hall-config-service.test.ts b/apps/backend/src/services/worker-platform/worker-hall-config-service.test.ts index 838cf591..e4b17e27 100644 --- a/apps/backend/src/services/worker-platform/worker-hall-config-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-hall-config-service.test.ts @@ -7,17 +7,36 @@ import { } from './worker-hall-config-service.js' test('大厅配置默认展示 50 个订单', () => { - assert.deepEqual(createDefaultWorkerHallConfig(), { maxVisibleOrders: 50 }) - assert.deepEqual(normalizeWorkerHallConfig({}), { maxVisibleOrders: 50 }) + assert.deepEqual(createDefaultWorkerHallConfig(), { + maxVisibleOrders: 50, + categoryMaxVisibleOrders: {}, + }) + assert.deepEqual(normalizeWorkerHallConfig({}), { + maxVisibleOrders: 50, + categoryMaxVisibleOrders: {}, + }) }) test('大厅配置只接受 1 到 500 的整数', () => { - assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), { maxVisibleOrders: 88 }) + assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), { + maxVisibleOrders: 88, + categoryMaxVisibleOrders: {}, + }) assert.deepEqual(normalizeWorkerHallConfig({ max_visible_orders: 999 }), { maxVisibleOrders: 500, + categoryMaxVisibleOrders: {}, + }) + assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), { + maxVisibleOrders: 1, + categoryMaxVisibleOrders: {}, }) - assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), { maxVisibleOrders: 1 }) assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 1.5 }), { maxVisibleOrders: 50, + categoryMaxVisibleOrders: {}, }) + assert.deepEqual( + normalizeWorkerHallConfig({ categoryMaxVisibleOrders: { '1': 12, '2': 0, '3': 501 } }) + .categoryMaxVisibleOrders, + { '1': 12 }, + ) }) diff --git a/apps/backend/src/services/worker-platform/worker-hall-config-service.ts b/apps/backend/src/services/worker-platform/worker-hall-config-service.ts index c6cc958a..63821a37 100644 --- a/apps/backend/src/services/worker-platform/worker-hall-config-service.ts +++ b/apps/backend/src/services/worker-platform/worker-hall-config-service.ts @@ -4,6 +4,7 @@ import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-sto export type WorkerHallConfig = { maxVisibleOrders: number + categoryMaxVisibleOrders: Record } export function getWorkerHallConfig(): WorkerHallConfig { @@ -23,7 +24,7 @@ export async function saveWorkerHallConfig(rawValue: unknown): Promise { + if (!isPlainObject(value)) return {} + return Object.entries(value).reduce>((result, [categoryId, raw]) => { + const id = Number(categoryId) + const limit = Number(raw) + if (Number.isInteger(id) && id > 0 && Number.isInteger(limit) && limit >= 1 && limit <= 500) { + result[String(id)] = limit + } + return result + }, {}) +} + function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } 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 2ff44a3d..ea297a9f 100644 --- a/apps/backend/src/services/worker-platform/worker-hall-service.ts +++ b/apps/backend/src/services/worker-platform/worker-hall-service.ts @@ -39,6 +39,7 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work keyword: String(query.keyword || '').trim(), categoryId: categoryId || 0, hallCandidateLimit: hallConfig.maxVisibleOrders, + hallCategoryLimits: hallConfig.categoryMaxVisibleOrders, visibleAfterIso, excludeFilledSharing: true, pinnedFirst: true, diff --git a/apps/frontend/src/pages/admin/panels/HallConfigPanel.tsx b/apps/frontend/src/pages/admin/panels/HallConfigPanel.tsx index e5a8c78b..b0812723 100644 --- a/apps/frontend/src/pages/admin/panels/HallConfigPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/HallConfigPanel.tsx @@ -1,11 +1,17 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' -import { App, Button, Card, Descriptions, Form, InputNumber, Space } from 'antd' +import { App, Button, Card, Descriptions, Form, InputNumber, Space, Table, Tag } from 'antd' import { useEffect, useState } from 'react' -import { fetchAdminWorkerHallConfig, saveAdminWorkerHallConfig } from '@/services/admin' +import { + fetchAdminWorkCategories, + fetchAdminWorkerHallConfig, + saveAdminWorkerHallConfig, +} from '@/services/admin' +import type { WorkCategory } from '@/types/worker-platform' type HallConfigFormValues = { maxVisibleOrders: number + categoryMaxVisibleOrders: Record } export default function HallConfigPanel() { @@ -20,13 +26,25 @@ export default function HallConfigPanel() { const config = configQuery.data?.data useEffect(() => { - if (config) form.setFieldsValue({ maxVisibleOrders: config.maxVisibleOrders }) + if (config) { + form.setFieldsValue({ + maxVisibleOrders: config.maxVisibleOrders, + categoryMaxVisibleOrders: config.categoryMaxVisibleOrders || {}, + }) + } }, [config, form]) + const categoriesQuery = useQuery({ + queryKey: ['admin-worker-platform-categories'], + queryFn: fetchAdminWorkCategories, + }) async function saveConfig(values: HallConfigFormValues) { setSaving(true) try { - await saveAdminWorkerHallConfig({ maxVisibleOrders: Number(values.maxVisibleOrders) }) + await saveAdminWorkerHallConfig({ + maxVisibleOrders: Number(values.maxVisibleOrders), + categoryMaxVisibleOrders: values.categoryMaxVisibleOrders || {}, + }) await Promise.all([ queryClient.invalidateQueries({ queryKey: ['admin-worker-hall-config'] }), queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }), @@ -47,7 +65,7 @@ export default function HallConfigPanel() { {config?.queuedOrderCount || 0} {config?.maxVisibleOrders || '-'} -
+ +
+ 分类上限 + + 未填写的分类沿用大厅总上限;填写后仅限制该分类最多展示数量。 + +
+ + rowKey="categoryId" + size="small" + pagination={false} + loading={categoriesQuery.isLoading} + dataSource={categoriesQuery.data?.data.items || []} + columns={[ + { title: '分类', dataIndex: 'name' }, + { + title: '状态', + render: (_, category) => ( + + {category.status === 'active' ? '启用' : '停用'} + + ), + }, + { + title: '最多展示', + render: (_, category) => ( + + + + ), + }, + ]} + /> diff --git a/apps/frontend/src/pages/admin/panels/WorkOrderPanelParts.tsx b/apps/frontend/src/pages/admin/panels/WorkOrderPanelParts.tsx index 598b83af..e0682039 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrderPanelParts.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrderPanelParts.tsx @@ -1,9 +1,13 @@ import { PlusOutlined } from '@ant-design/icons' import { useQuery, useQueryClient } from '@tanstack/react-query' import { App, AutoComplete, Button, Card, Form, Input, InputNumber, Select, Switch } from 'antd' -import { useState } from 'react' +import { useEffect, useState } from 'react' -import { createAdminMockWorkOrder, fetchAdminMockCustomShops } from '@/services/admin' +import { + createAdminMockWorkOrder, + fetchAdminMockCustomShops, + fetchAdminWorkCategories, +} from '@/services/admin' import { fetchAdminKuaishouIndustryShops } from '@/services/admin/kuaishou-industry' import type { WorkOrderStatistics } from '@/types/worker-platform' import { buildMockWorkOrderShopOptions } from '@/utils/mock-work-order' @@ -69,6 +73,19 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise category.status === 'active', + ) + const defaultCategoryId = activeCategories[0]?.categoryId + useEffect(() => { + if (!form.getFieldValue('categoryId') && defaultCategoryId) { + form.setFieldValue('categoryId', defaultCategoryId) + } + }, [defaultCategoryId, form]) const shopOptions = [ ...buildMockWorkOrderShopOptions(shopsQuery.data?.data.shops || []), ...(customShopsQuery.data?.data.items || []).map((name) => ({ @@ -81,6 +98,7 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise Promise Promise + + ({ + value: category.categoryId, + label: category.name, + }))} + onChange={(nextCategoryId) => { + setCategoryId(nextCategoryId) + setPage(1) + }} + /> {showWorkerLevelFilter ? (