接单平台分类管理完善:抢单大厅分类筛选与分类 CRUD

- 抢单大厅新增分类 Tab 筛选(仅显示启用分类,支持按分类查询工单)
- 管理端分类面板支持编辑、启用/停用切换、删除(被工单/规则引用时禁止删除)
- 后端工单列表支持 categoryId 筛选,大厅接口返回分类列表
This commit is contained in:
yml2213
2026-08-01 14:59:58 +08:00
parent 8dfe797c25
commit 0369186b18
8 changed files with 234 additions and 15 deletions
@@ -166,6 +166,7 @@ type ListInput = {
status?: string status?: string
keyword?: string keyword?: string
workerId?: number workerId?: number
categoryId?: number
} }
type ProductRuleListInput = { type ProductRuleListInput = {
@@ -912,6 +913,14 @@ export async function getWorkCategoryByKey(categoryKey: string): Promise<WorkCat
return result.rows[0] || null return result.rows[0] || null
} }
export async function getWorkCategoryById(categoryId: number | string): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>(
'SELECT * FROM work_categories WHERE id = $1 LIMIT 1',
[Number(categoryId)],
)
return result.rows[0] || null
}
export async function listWorkCategories(): Promise<WorkCategoryRow[]> { export async function listWorkCategories(): Promise<WorkCategoryRow[]> {
const result = await query<WorkCategoryRow>( const result = await query<WorkCategoryRow>(
`SELECT * FROM work_categories WHERE status = 'active' ORDER BY sort_order ASC, id ASC`, `SELECT * FROM work_categories WHERE status = 'active' ORDER BY sort_order ASC, id ASC`,
@@ -946,6 +955,36 @@ export async function upsertWorkCategory(input: {
return result.rows[0] || null return result.rows[0] || null
} }
export async function countWorkCategoryUsages(categoryId: number | string): Promise<{
workOrderCount: number
productRuleCount: number
}> {
const [orders, rules] = await Promise.all([
query<{ total: number }>(
'SELECT COUNT(*)::int AS total FROM work_orders WHERE category_id = $1',
[Number(categoryId)],
),
query<{ total: number }>(
'SELECT COUNT(*)::int AS total FROM work_product_rules WHERE category_id = $1',
[Number(categoryId)],
),
])
return {
workOrderCount: Number(orders.rows[0]?.total || 0),
productRuleCount: Number(rules.rows[0]?.total || 0),
}
}
export async function deleteWorkCategory(
categoryId: number | string,
): Promise<{ deleted: boolean }> {
const result = await query<{ id: number }>(
'DELETE FROM work_categories WHERE id = $1 RETURNING id',
[Number(categoryId)],
)
return { deleted: Boolean(result.rows[0]) }
}
export async function listWorkProductRules({ export async function listWorkProductRules({
enabled = null, enabled = null,
keyword = '', keyword = '',
@@ -1123,8 +1162,14 @@ export async function listWorkOrders({
status = '', status = '',
keyword = '', keyword = '',
workerId = 0, workerId = 0,
categoryId = 0,
}: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> { }: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> {
const { whereClause, params } = buildWorkOrderWhere({ status, keyword, workerId }) const { whereClause, params } = buildWorkOrderWhere({
status,
keyword,
workerId,
categoryId,
})
const totalResult = await query<{ total: number }>( const totalResult = await query<{ total: number }>(
` `
SELECT COUNT(*)::int AS total SELECT COUNT(*)::int AS total
@@ -1713,7 +1758,12 @@ function buildWorkerUserWhere({
} }
} }
function buildWorkOrderWhere({ status = '', keyword = '', workerId = 0 }: ListInput) { function buildWorkOrderWhere({
status = '',
keyword = '',
workerId = 0,
categoryId = 0,
}: ListInput) {
const filters: string[] = [] const filters: string[] = []
const params: unknown[] = [] const params: unknown[] = []
if (status) { if (status) {
@@ -1730,6 +1780,10 @@ function buildWorkOrderWhere({ status = '', keyword = '', workerId = 0 }: ListIn
params.push(workerId) params.push(workerId)
filters.push(`wo.assigned_worker_id = $${params.length}`) filters.push(`wo.assigned_worker_id = $${params.length}`)
} }
if (categoryId) {
params.push(categoryId)
filters.push(`wo.category_id = $${params.length}`)
}
return { return {
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '', whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params, params,
@@ -4,6 +4,7 @@ import {
acceptAdminWorkOrder, acceptAdminWorkOrder,
creditAdminWorkerWallet, creditAdminWorkerWallet,
createAdminMockWorkOrder, createAdminMockWorkOrder,
deleteAdminWorkCategory,
getAdminWorkerFinanceConfig, getAdminWorkerFinanceConfig,
getAdminWorkerPlatformSummary, getAdminWorkerPlatformSummary,
listAdminWorkerFinanceRequests, listAdminWorkerFinanceRequests,
@@ -92,6 +93,22 @@ router.post(
}), }),
) )
router.delete(
'/worker-platform/categories/:categoryId',
requireAdminRoles(['admin', 'operator']),
createJsonHandler((req) => deleteAdminWorkCategory(String(req.params.categoryId || '')), {
successMessage: '接单分类已删除',
errorMessage: '删除接单分类失败',
scope: '[admin/worker-platform/categories/:categoryId]',
audit: (_req, data) => ({
action: 'work_category_deleted',
targetType: 'work_category',
targetId: String(_req.params.categoryId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
)
router.get( router.get(
'/worker-platform/product-rules', '/worker-platform/product-rules',
requireAdminRoles(['admin', 'operator', 'support']), requireAdminRoles(['admin', 'operator', 'support']),
@@ -9,12 +9,15 @@ import {
addWorkerWalletCredit, addWorkerWalletCredit,
countWorkerAcceptedOrders, countWorkerAcceptedOrders,
countWorkerActiveOrders, countWorkerActiveOrders,
countWorkCategoryUsages,
createWorkOrder, createWorkOrder,
createWorkOrderEvent, createWorkOrderEvent,
createWorkerFinanceRequest, createWorkerFinanceRequest,
createWorkerUser, createWorkerUser,
deleteWorkCategory,
getWorkerFinanceRequestById, getWorkerFinanceRequestById,
getWorkerFinanceRequestSummary, getWorkerFinanceRequestSummary,
getWorkCategoryById,
getWorkerLevelById, getWorkerLevelById,
getWorkerLevelByKey, getWorkerLevelByKey,
getWorkerUserByInviteCode, getWorkerUserByInviteCode,
@@ -28,6 +31,7 @@ import {
listPendingMaterialWorkOrdersByPlatformOrderId, listPendingMaterialWorkOrdersByPlatformOrderId,
listWorkerWalletLedgers, listWorkerWalletLedgers,
grabWorkOrder, grabWorkOrder,
listWorkCategories,
listWorkOrders, listWorkOrders,
listWorkProductRules, listWorkProductRules,
listWorkerLevels, listWorkerLevels,
@@ -627,17 +631,21 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
requireActiveWorkerSession(session) requireActiveWorkerSession(session)
const page = normalizePage(query.page) const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize) const pageSize = normalizePageSize(query.pageSize)
const categoryId = normalizeOptionalId(query.categoryId)
const { items, total } = await listWorkOrders({ const { items, total } = await listWorkOrders({
page, page,
pageSize, pageSize,
status: WORK_ORDER_STATUS.OPEN, status: WORK_ORDER_STATUS.OPEN,
keyword: String(query.keyword || '').trim(), keyword: String(query.keyword || '').trim(),
categoryId: categoryId || 0,
}) })
const worker = await getRequiredWorker(session.workerId) const worker = await getRequiredWorker(session.workerId)
const permissions = resolveWorkerPermissions(worker) const permissions = resolveWorkerPermissions(worker)
const categories = (await listWorkCategories()).map(mapWorkCategory)
return { return {
items: items.map((item) => mapWorkOrderForWorker(item, permissions)), items: items.map((item) => mapWorkOrderForWorker(item, permissions)),
pagination: { page, pageSize, total }, pagination: { page, pageSize, total },
categories,
} }
} }
@@ -880,6 +888,28 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) {
return { category: mapWorkCategory(category) } return { category: mapWorkCategory(category) }
} }
export async function deleteAdminWorkCategory(categoryId: number | string) {
const category = await getWorkCategoryById(Number(categoryId))
if (!category) {
throw createHttpError('分类不存在', {
statusCode: 404,
errorCode: 'work_category_not_found',
})
}
const usages = await countWorkCategoryUsages(Number(categoryId))
if (usages.workOrderCount > 0 || usages.productRuleCount > 0) {
throw createHttpError(
`该分类已被 ${usages.workOrderCount} 个工单和 ${usages.productRuleCount} 条物品规则使用,无法删除,请先停用`,
{
statusCode: 409,
errorCode: 'work_category_in_use',
},
)
}
const { deleted } = await deleteWorkCategory(Number(categoryId))
return { deleted }
}
export async function saveAdminWorkerLevel(payload: JsonObject = {}) { export async function saveAdminWorkerLevel(payload: JsonObject = {}) {
const now = nowIso() const now = nowIso()
const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY
@@ -39,6 +39,7 @@ import {
acceptAdminWorkOrder, acceptAdminWorkOrder,
createAdminMockWorkOrder, createAdminMockWorkOrder,
creditAdminWorkerWallet, creditAdminWorkerWallet,
deleteAdminWorkCategory,
fetchAdminWorkerFinanceConfig, fetchAdminWorkerFinanceConfig,
fetchAdminWorkerFinanceRequests, fetchAdminWorkerFinanceRequests,
fetchAdminWorkCategories, fetchAdminWorkCategories,
@@ -1034,11 +1035,34 @@ function CategoriesPanel() {
const { message } = App.useApp() const { message } = App.useApp()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [form] = Form.useForm() const [form] = Form.useForm()
const [editingCategory, setEditingCategory] = useState<WorkCategory | null>(null)
const [deletingCategory, setDeletingCategory] = useState<WorkCategory | null>(null)
const categoriesQuery = useQuery({ const categoriesQuery = useQuery({
queryKey: ['admin-worker-platform-categories'], queryKey: ['admin-worker-platform-categories'],
queryFn: () => fetchAdminWorkCategories(), queryFn: () => fetchAdminWorkCategories(),
}) })
async function refreshCategories() {
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-categories'],
})
}
function startEdit(category: WorkCategory) {
setEditingCategory(category)
form.setFieldsValue({
categoryKey: category.categoryKey,
name: category.name,
sortOrder: category.sortOrder,
status: category.status,
})
}
function cancelEdit() {
setEditingCategory(null)
form.resetFields()
}
async function saveCategory(values: { async function saveCategory(values: {
categoryKey: string categoryKey: string
name: string name: string
@@ -1047,16 +1071,42 @@ function CategoriesPanel() {
}) { }) {
try { try {
await saveAdminWorkCategory(values) await saveAdminWorkCategory(values)
message.success('分类已保存') message.success(editingCategory ? '分类已更新' : '分类已保存')
form.resetFields() cancelEdit()
await queryClient.invalidateQueries({ await refreshCategories()
queryKey: ['admin-worker-platform-categories'],
})
} catch (error) { } catch (error) {
message.error(error instanceof Error ? error.message : '保存失败') message.error(error instanceof Error ? error.message : '保存失败')
} }
} }
async function toggleCategory(category: WorkCategory) {
try {
await saveAdminWorkCategory({
categoryKey: category.categoryKey,
name: category.name,
sortOrder: category.sortOrder,
status: category.status === 'active' ? 'disabled' : 'active',
})
message.success(category.status === 'active' ? '分类已停用' : '分类已启用')
await refreshCategories()
} catch (error) {
message.error(error instanceof Error ? error.message : '操作失败')
}
}
async function submitDelete() {
if (!deletingCategory) return
try {
await deleteAdminWorkCategory(deletingCategory.categoryId)
message.success('分类已删除')
setDeletingCategory(null)
await refreshCategories()
} catch (error) {
message.error(error instanceof Error ? error.message : '删除失败')
setDeletingCategory(null)
}
}
const columns: TableColumnsType<WorkCategory> = [ const columns: TableColumnsType<WorkCategory> = [
{ title: '分类', dataIndex: 'name' }, { title: '分类', dataIndex: 'name' },
{ title: '标识', dataIndex: 'categoryKey' }, { title: '标识', dataIndex: 'categoryKey' },
@@ -1070,11 +1120,28 @@ function CategoriesPanel() {
</Tag> </Tag>
), ),
}, },
{
title: '操作',
width: 240,
render: (_, row) => (
<Space wrap>
<Button size="small" onClick={() => startEdit(row)}>
</Button>
<Button size="small" onClick={() => toggleCategory(row)}>
{row.status === 'active' ? '停用' : '启用'}
</Button>
<Button size="small" danger onClick={() => setDeletingCategory(row)}>
</Button>
</Space>
),
},
] ]
return ( return (
<section className="platform-panel-stack"> <section className="platform-panel-stack">
<Card title="保存分类" bordered={false}> <Card title={editingCategory ? `编辑分类:${editingCategory.name}` : '保存分类'} bordered={false}>
<Form <Form
form={form} form={form}
layout="inline" layout="inline"
@@ -1108,9 +1175,12 @@ function CategoriesPanel() {
/> />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<Button type="primary" htmlType="submit"> <Space>
<Button type="primary" htmlType="submit">
</Button>
</Button>
{editingCategory ? <Button onClick={cancelEdit}></Button> : null}
</Space>
</Form.Item> </Form.Item>
</Form> </Form>
</Card> </Card>
@@ -1123,6 +1193,19 @@ function CategoriesPanel() {
pagination={false} pagination={false}
/> />
</Card> </Card>
<Modal
title="删除分类"
open={Boolean(deletingCategory)}
onOk={submitDelete}
onCancel={() => setDeletingCategory(null)}
okButtonProps={{ danger: true }}
okText="删除"
cancelText="取消"
>
<Typography.Text>
{deletingCategory?.name}
</Typography.Text>
</Modal>
</section> </section>
) )
} }
@@ -12,6 +12,7 @@ import {
Pagination, Pagination,
Space, Space,
Spin, Spin,
Tabs,
Tag, Tag,
Typography, Typography,
} from 'antd' } from 'antd'
@@ -37,14 +38,16 @@ export default function WorkerHallPage() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [keywordInput, setKeywordInput] = useState('') const [keywordInput, setKeywordInput] = useState('')
const [keyword, setKeyword] = useState('') const [keyword, setKeyword] = useState('')
const [categoryId, setCategoryId] = useState(0)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20) const [pageSize, setPageSize] = useState(20)
const [grabbingId, setGrabbingId] = useState(0) const [grabbingId, setGrabbingId] = useState(0)
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null) const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
const ordersQuery = useQuery({ const ordersQuery = useQuery({
queryKey: ['worker-hall-orders', keyword, page, pageSize], queryKey: ['worker-hall-orders', keyword, categoryId, page, pageSize],
queryFn: () => fetchWorkerHallOrders({ keyword, page, pageSize }), queryFn: () =>
fetchWorkerHallOrders({ keyword, page, pageSize, categoryId: categoryId || undefined }),
retry: false, retry: false,
}) })
@@ -94,6 +97,7 @@ export default function WorkerHallPage() {
const orders = ordersQuery.data?.data.items || [] const orders = ordersQuery.data?.data.items || []
const pagination = ordersQuery.data?.data.pagination const pagination = ordersQuery.data?.data.pagination
const categories = ordersQuery.data?.data.categories || []
const worker = profileQuery.data?.data.worker const worker = profileQuery.data?.data.worker
const activeCount = hallSummaryQuery.data?.activeCount || 0 const activeCount = hallSummaryQuery.data?.activeCount || 0
const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 0 const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 0
@@ -210,6 +214,24 @@ export default function WorkerHallPage() {
))} ))}
</div> </div>
{categories.length > 0 ? (
<Tabs
size="small"
activeKey={String(categoryId)}
items={[
{ key: '0', label: '全部' },
...categories.map((category) => ({
key: String(category.categoryId),
label: category.name,
})),
]}
onChange={(nextKey) => {
setCategoryId(Number(nextKey) || 0)
setPage(1)
}}
/>
) : null}
{shouldWarn ? ( {shouldWarn ? (
<Alert <Alert
className="worker-hall-warning" className="worker-hall-warning"
@@ -1,4 +1,4 @@
import { apiGet, apiPost } from '@/lib/http' import { apiDelete, apiGet, apiPost } from '@/lib/http'
import type { import type {
WorkCategory, WorkCategory,
WorkOrder, WorkOrder,
@@ -43,6 +43,12 @@ export function saveAdminWorkCategory(payload: {
) )
} }
export function deleteAdminWorkCategory(categoryId: number) {
return apiDelete<{ deleted: boolean }>(
`/api/v1/admin/worker-platform/categories/${categoryId}`,
)
}
export function saveAdminWorkerLevel(payload: { export function saveAdminWorkerLevel(payload: {
levelKey: string levelKey: string
name: string name: string
+2 -1
View File
@@ -4,6 +4,7 @@ import type {
UploadedFile, UploadedFile,
WorkOrder, WorkOrder,
WorkerFinanceRequest, WorkerFinanceRequest,
WorkerHallOrdersResponse,
WorkerListResponse, WorkerListResponse,
WorkerLoginResponse, WorkerLoginResponse,
WorkerProfileResponse, WorkerProfileResponse,
@@ -92,7 +93,7 @@ export function changeWorkerPassword(payload: {
} }
export function fetchWorkerHallOrders(params?: Record<string, unknown>) { export function fetchWorkerHallOrders(params?: Record<string, unknown>) {
return apiGet<WorkerListResponse<WorkOrder>>( return apiGet<WorkerHallOrdersResponse>(
'/api/v1/worker/hall/orders', '/api/v1/worker/hall/orders',
params, params,
) )
@@ -224,6 +224,12 @@ export type WorkerListResponse<T> = {
pagination: Pagination pagination: Pagination
} }
export type WorkerHallOrdersResponse = {
items: WorkOrder[]
pagination: Pagination
categories: WorkCategory[]
}
export type CollectField = { export type CollectField = {
key: string key: string
label: string label: string