diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 0ebf69b5..6a062482 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -262,8 +262,11 @@ export type WorkOrderStatistics = { } export type ProductRuleListInput = { + page?: number + pageSize?: number enabled?: boolean | null keyword?: string + categoryId?: number } export type WalletLedgerListInput = { diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index b0c86891..30e44516 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -548,8 +548,9 @@ export async function deleteWorkCategory( export async function listWorkProductRules({ enabled = null, keyword = '', + categoryId = 0, }: ProductRuleListInput = {}): Promise { - const { whereClause, params } = buildWorkProductRuleWhere({ enabled, keyword }) + const { whereClause, params } = buildWorkProductRuleWhere({ enabled, keyword, categoryId }) const result = await query( `${WORK_PRODUCT_RULE_SELECT} ${whereClause} @@ -559,6 +560,56 @@ export async function listWorkProductRules({ return result.rows } +export async function listWorkProductRulesPage({ + page = 1, + pageSize = 20, + enabled = null, + keyword = '', + categoryId = 0, +}: ProductRuleListInput): Promise<{ items: WorkProductRuleRow[]; total: number }> { + const { whereClause, params } = buildWorkProductRuleWhere({ enabled, keyword, categoryId }) + const totalResult = await query<{ total: number }>( + ` + SELECT COUNT(*)::int AS total + FROM work_product_rules wpr + ${whereClause} + `, + params, + ) + const offset = (page - 1) * pageSize + const listParams = [...params, pageSize, offset] + const result = await query( + `${WORK_PRODUCT_RULE_SELECT} + ${whereClause} + ORDER BY wpr.sort_order ASC, wpr.id DESC + LIMIT $${listParams.length - 1} OFFSET $${listParams.length}`, + listParams, + ) + return { items: result.rows, total: Number(totalResult.rows[0]?.total || 0) } +} + +export async function countWorkProductRulesByCategory( + input: { + enabled?: boolean | null + keyword?: string + } = {}, +): Promise> { + const { whereClause, params } = buildWorkProductRuleWhere(input) + const result = await query<{ category_id: number | null; total: number }>( + ` + SELECT wpr.category_id, COUNT(*)::int AS total + FROM work_product_rules wpr + ${whereClause} + GROUP BY wpr.category_id + `, + params, + ) + return result.rows.map((row) => ({ + categoryId: row.category_id ? Number(row.category_id) : null, + total: Number(row.total || 0), + })) +} + export async function getWorkProductRuleByKey(ruleKey: string): Promise { const result = await query( `${WORK_PRODUCT_RULE_SELECT} WHERE wpr.rule_key = $1 LIMIT 1`, @@ -2803,7 +2854,11 @@ function buildWorkOrderWhere({ } } -function buildWorkProductRuleWhere({ enabled = null, keyword = '' }: ProductRuleListInput) { +function buildWorkProductRuleWhere({ + enabled = null, + keyword = '', + categoryId = 0, +}: ProductRuleListInput) { const filters: string[] = [] const params: unknown[] = [] if (enabled !== null && enabled !== undefined) { @@ -2818,6 +2873,10 @@ function buildWorkProductRuleWhere({ enabled = null, keyword = '' }: ProductRule OR wpr.product_name ILIKE $${params.length} )`) } + if (categoryId) { + params.push(categoryId) + filters.push(`wpr.category_id = $${params.length}`) + } return { whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '', params, diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index ee19124b..31de18a2 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -9,6 +9,7 @@ import { countWorkerActiveOrders, countWorkCategoryUsages, countWorkerLevelUsages, + countWorkProductRulesByCategory, countWorkOrderPendingSharingSubmissions, countTimeoutEventsByWorkerIds, createWorkOrder, @@ -31,6 +32,7 @@ import { listWorkOrders, listWorkOrderEventsByOrderId, listWorkProductRules, + listWorkProductRulesPage, listWorkerLevels, listWorkerUsers, listWorkerWithdrawalAccounts, @@ -232,15 +234,32 @@ export async function deleteAdminWorkerLevel(levelId: number | string) { export async function listAdminWorkProductRules(query: JsonObject = {}) { await ensureWorkerPlatformDefaults() + const page = normalizePage(query.page) + const pageSize = normalizePageSize(query.pageSize) const enabledValue = String(query.enabled ?? '').trim() const enabled = enabledValue ? ['true', '1', 'enabled', 'active'].includes(enabledValue.toLowerCase()) : null - const items = await listWorkProductRules({ - enabled, - keyword: String(query.keyword || '').trim(), - }) - return { items: items.map(mapWorkProductRule) } + const keyword = String(query.keyword || '').trim() + const categoryId = normalizeOptionalId(query.categoryId ?? query.category_id) || 0 + const [{ items, total }, categoryRows] = await Promise.all([ + listWorkProductRulesPage({ + page, + pageSize, + enabled, + keyword, + categoryId, + }), + countWorkProductRulesByCategory({ enabled, keyword }), + ]) + return { + items: items.map(mapWorkProductRule), + pagination: { page, pageSize, total }, + categoryCounts: categoryRows.map((row) => ({ + categoryId: row.categoryId, + total: row.total, + })), + } } export async function saveAdminWorkProductRule(payload: JsonObject = {}) { diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index 5f1f3a9e..827739e6 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -33,6 +33,7 @@ import { saveAdminWorkProductRule, } from '@/services/admin' import type { CollectField, WorkProductRule } from '@/types/worker-platform' +import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatMoney } from './shared' export default function ProductRulesPanel() { @@ -41,37 +42,40 @@ export default function ProductRulesPanel() { const [form] = Form.useForm() const [editingRule, setEditingRule] = useState(null) + const [keywordInput, setKeywordInput] = useState('') const [keyword, setKeyword] = useState('') const [categoryId, setCategoryId] = useState() + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE) const rulesQuery = useQuery({ - queryKey: ['admin-worker-platform-product-rules'], - queryFn: () => fetchAdminWorkProductRules(), + queryKey: ['admin-worker-platform-product-rules', keyword, categoryId, page, pageSize], + queryFn: () => + fetchAdminWorkProductRules({ + keyword, + categoryId, + page, + pageSize, + }), }) const categoriesQuery = useQuery({ queryKey: ['admin-worker-platform-categories'], queryFn: () => fetchAdminWorkCategories(), }) - const allRules = rulesQuery.data?.data.items || [] + const rules = rulesQuery.data?.data.items || [] + const rulesPagination = rulesQuery.data?.data.pagination + const categoryCounts = new Map( + (rulesQuery.data?.data.categoryCounts || []).map((item) => [ + item.categoryId === null ? 'uncategorized' : String(item.categoryId), + item.total, + ]), + ) - const filteredRules = allRules.filter((rule) => { - if (categoryId && Number(rule.categoryId || 0) !== categoryId) return false - const keywordText = keyword.trim().toLowerCase() - if (!keywordText) return true - return [ - rule.ruleKey, - rule.productName, - rule.skuCode, - rule.provider, - rule.shopId, - rule.categoryName, - ].some((value) => - String(value || '') - .toLowerCase() - .includes(keywordText), - ) - }) + function searchRules(value = keywordInput) { + setKeyword(value.trim()) + setPage(1) + } function resetRuleForm() { setEditingRule(null) @@ -448,18 +452,7 @@ export default function ProductRulesPanel() { { - const key = String(value || '').trim() - if (!editingRule && allRules.some((rule) => rule.ruleKey === key)) { - return Promise.reject(new Error('标识已存在')) - } - return Promise.resolve() - }, - }, - ]} + rules={[{ required: true, message: '请输入规则标识' }]} > @@ -652,7 +645,7 @@ export default function ProductRulesPanel() { 物品规则列表 - {filteredRules.length} 条规则 + {rulesPagination?.total || 0} 条规则 } @@ -662,8 +655,13 @@ export default function ProductRulesPanel() { allowClear placeholder="搜索规则/商品/SKU/店铺" style={{ width: 200 }} - value={keyword} - onChange={(event) => setKeyword(event.target.value)} + value={keywordInput} + onChange={(event) => { + const value = event.target.value + setKeywordInput(value) + if (!value) searchRules('') + }} + onSearch={searchRules} />