接单工单支持置顶,抢单大厅优先展示
- 新增 work_orders.pinned_at 字段与迁移 - 管理端对已发布(待抢单)订单提供置顶/取消置顶操作,置顶订单带标记 - 抢单大厅按置顶时间优先排序,打手端卡片展示置顶标签
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
-- 022_work_order_pinned_at.sql —— 工单置顶。
|
||||
--
|
||||
-- 已发布到抢单大厅(待抢单)的工单可由管理员置顶,置顶后按置顶时间在抢单大厅优先展示。
|
||||
|
||||
ALTER TABLE work_orders ADD COLUMN IF NOT EXISTS pinned_at TIMESTAMPTZ NULL;
|
||||
|
||||
COMMENT ON COLUMN work_orders.pinned_at IS '置顶时间;非空表示该工单在抢单大厅置顶展示';
|
||||
@@ -177,6 +177,7 @@ export type WorkOrderRow = {
|
||||
sharing_total_quantity: number
|
||||
sharing_unit_reward: number
|
||||
published_at: string | null
|
||||
pinned_at: string | null
|
||||
assigned_at: string | null
|
||||
submitted_at: string | null
|
||||
accepted_at: string | null
|
||||
@@ -218,6 +219,8 @@ export type ListInput = {
|
||||
categoryId?: number
|
||||
workerSharingId?: number
|
||||
visibleAfterIso?: string
|
||||
/** 置顶订单优先展示(抢单大厅使用) */
|
||||
pinnedFirst?: boolean
|
||||
sort?: 'id_desc' | 'worker_claimed_at_desc'
|
||||
}
|
||||
|
||||
|
||||
@@ -744,6 +744,7 @@ export async function listWorkOrders({
|
||||
categoryId = 0,
|
||||
workerSharingId = 0,
|
||||
visibleAfterIso = '',
|
||||
pinnedFirst = false,
|
||||
sort = 'id_desc',
|
||||
}: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> {
|
||||
const effectiveStatuses =
|
||||
@@ -786,11 +787,14 @@ export async function listWorkOrders({
|
||||
) DESC NULLS LAST, wo.id DESC`
|
||||
})()
|
||||
: 'wo.id DESC'
|
||||
const effectiveOrderBy = pinnedFirst
|
||||
? `wo.pinned_at DESC NULLS LAST, ${orderBy}`
|
||||
: orderBy
|
||||
params.push(pageSize, offset)
|
||||
const itemsResult = await query<WorkOrderRow>(
|
||||
`${WORK_ORDER_SELECT}
|
||||
${whereClause}
|
||||
ORDER BY ${orderBy}
|
||||
ORDER BY ${effectiveOrderBy}
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
||||
params,
|
||||
)
|
||||
@@ -813,6 +817,7 @@ export async function updateWorkOrder(
|
||||
| 'sharing_total_quantity'
|
||||
| 'sharing_unit_reward'
|
||||
| 'published_at'
|
||||
| 'pinned_at'
|
||||
| 'assigned_at'
|
||||
| 'submitted_at'
|
||||
| 'accepted_at'
|
||||
@@ -838,11 +843,12 @@ export async function updateWorkOrder(
|
||||
sharing_total_quantity = $9,
|
||||
sharing_unit_reward = $10,
|
||||
published_at = $11,
|
||||
assigned_at = $12,
|
||||
submitted_at = $13,
|
||||
accepted_at = $14,
|
||||
updated_at = $15
|
||||
WHERE id = $16
|
||||
pinned_at = $12,
|
||||
assigned_at = $13,
|
||||
submitted_at = $14,
|
||||
accepted_at = $15,
|
||||
updated_at = $16
|
||||
WHERE id = $17
|
||||
`,
|
||||
[
|
||||
next.status,
|
||||
@@ -856,6 +862,7 @@ export async function updateWorkOrder(
|
||||
toPositiveInteger(next.sharing_total_quantity, 1),
|
||||
toPositiveInteger(next.sharing_unit_reward, 0),
|
||||
next.published_at || null,
|
||||
next.pinned_at || null,
|
||||
next.assigned_at || null,
|
||||
next.submitted_at || null,
|
||||
next.accepted_at || null,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
listAdminWorkerUsers,
|
||||
listAdminWorkOrders,
|
||||
markAdminWorkOrderProblem,
|
||||
pinAdminWorkOrder,
|
||||
publishAdminWorkOrder,
|
||||
reviewAdminWorkerFinanceRequest,
|
||||
resolveAdminProblemWorkOrder,
|
||||
@@ -450,6 +451,25 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/pin',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) => pinAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}),
|
||||
{
|
||||
successMessage: '置顶状态已更新',
|
||||
errorMessage: '更新置顶状态失败',
|
||||
scope: '[admin/worker-platform/orders/:workOrderId/pin]',
|
||||
audit: (req, data) => ({
|
||||
action: 'work_order_pin_toggled',
|
||||
targetType: 'work_order',
|
||||
targetId: String(req.params.workOrderId || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/orders/:workOrderId/sharing',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
|
||||
@@ -1083,6 +1083,32 @@ export async function unpublishAdminWorkOrder(workOrderId: number | string) {
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
export async function pinAdminWorkOrder(workOrderId: number | string, payload: JsonObject = {}) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const pinned = payload.pinned === true
|
||||
if (pinned && workOrder.status !== WORK_ORDER_STATUS.OPEN) {
|
||||
throw createHttpError('只有待抢单订单可以置顶', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_pin_status_invalid',
|
||||
})
|
||||
}
|
||||
const now = nowIso()
|
||||
const updated = await updateWorkOrder(workOrder.id, {
|
||||
pinned_at: pinned ? now : null,
|
||||
updated_at: now,
|
||||
})
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
eventType: pinned ? 'pinned' : 'unpinned',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
now,
|
||||
})
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
export async function markAdminWorkOrderProblem(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
|
||||
@@ -404,6 +404,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
|
||||
}
|
||||
: null,
|
||||
publishedAt: workOrder.published_at,
|
||||
pinnedAt: workOrder.pinned_at,
|
||||
assignedAt: workOrder.assigned_at,
|
||||
submittedAt: workOrder.submitted_at,
|
||||
acceptedAt: workOrder.accepted_at,
|
||||
|
||||
@@ -824,6 +824,7 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
categoryId: categoryId || 0,
|
||||
visibleAfterIso,
|
||||
pinnedFirst: true,
|
||||
})
|
||||
const categories = (await listWorkCategories()).map(mapWorkCategory)
|
||||
const myShares = await listWorkerSharesByWorker(worker.id)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
EditOutlined,
|
||||
FormOutlined,
|
||||
PlusOutlined,
|
||||
PushpinOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
SendOutlined,
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
fetchAdminWorkerUsers,
|
||||
fetchAdminWorkOrders,
|
||||
markAdminWorkOrderProblem,
|
||||
pinAdminWorkOrder,
|
||||
publishAdminWorkOrder,
|
||||
reviewAdminWorkerFinanceRequest,
|
||||
resolveAdminProblemWorkOrder,
|
||||
@@ -693,11 +695,16 @@ export default function WorkOrdersPanel() {
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 130,
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<Tag color={resolveStatusColor(row.status)}>
|
||||
{formatStatus(row.status)}
|
||||
</Tag>
|
||||
<Space wrap size={4}>
|
||||
<Tag color={resolveStatusColor(row.status)}>
|
||||
{formatStatus(row.status)}
|
||||
</Tag>
|
||||
{row.status === 'open' && row.pinnedAt ? (
|
||||
<Tag color="gold" icon={<PushpinOutlined />}>置顶</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -795,6 +802,20 @@ export default function WorkOrdersPanel() {
|
||||
发布
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'open' ? (
|
||||
<Button
|
||||
icon={<PushpinOutlined />}
|
||||
title={row.pinnedAt ? '取消置顶' : '置顶后抢单大厅优先展示'}
|
||||
onClick={() =>
|
||||
runAction(
|
||||
() => pinAdminWorkOrder(row.workOrderId, !row.pinnedAt),
|
||||
row.pinnedAt ? '已取消置顶' : '已置顶,抢单大厅优先展示',
|
||||
)
|
||||
}
|
||||
>
|
||||
{row.pinnedAt ? '取消置顶' : '置顶'}
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'open' ? (
|
||||
<Button
|
||||
danger
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClockCircleOutlined, ReloadOutlined, SearchOutlined, TrophyOutlined } from '@ant-design/icons'
|
||||
import { ClockCircleOutlined, PushpinOutlined, ReloadOutlined, SearchOutlined, TrophyOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
@@ -367,6 +367,11 @@ export default function WorkerHallPage() {
|
||||
{/* 卡片头部: 分类 + 拼单/时限标签 + 订单号 */}
|
||||
<div className="worker-hall-mobile-card-top">
|
||||
<div className="worker-hall-mobile-card-tags">
|
||||
{order.pinnedAt ? (
|
||||
<Tag color="gold" icon={<PushpinOutlined />} className="worker-hall-mobile-tag">
|
||||
置顶
|
||||
</Tag>
|
||||
) : null}
|
||||
{order.categoryName ? (
|
||||
<Tag color="blue" className="worker-hall-mobile-tag">
|
||||
{order.categoryName}
|
||||
@@ -610,6 +615,11 @@ export default function WorkerHallPage() {
|
||||
return (
|
||||
<article key={order.workOrderId} className="worker-hall-card">
|
||||
<div className="worker-hall-card-title">
|
||||
{order.pinnedAt ? (
|
||||
<Tag color="gold" icon={<PushpinOutlined />} className="worker-hall-pinned-tag">
|
||||
置顶
|
||||
</Tag>
|
||||
) : null}
|
||||
<strong
|
||||
className="worker-hall-card-title-text"
|
||||
title={order.productName || '未命名商品'}
|
||||
|
||||
@@ -283,6 +283,13 @@ export function unpublishAdminWorkOrder(workOrderId: number) {
|
||||
)
|
||||
}
|
||||
|
||||
export function pinAdminWorkOrder(workOrderId: number, pinned: boolean) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/pin`,
|
||||
{ pinned },
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminWorkOrderSharing(workOrderId: number) {
|
||||
return apiGet<{ order: WorkOrder; shares: WorkOrderShare[] }>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/sharing`,
|
||||
|
||||
@@ -580,6 +580,11 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.worker-hall-pinned-tag {
|
||||
width: fit-content;
|
||||
margin: 0 0 2px;
|
||||
}
|
||||
|
||||
.worker-hall-card-title .ant-typography {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -260,6 +260,8 @@ export type WorkOrder = {
|
||||
displayName: string
|
||||
}
|
||||
publishedAt: string | null
|
||||
/** 置顶时间;非空表示该工单在抢单大厅置顶展示 */
|
||||
pinnedAt?: string | null
|
||||
assignedAt: string | null
|
||||
submittedAt: string | null
|
||||
acceptedAt: string | null
|
||||
|
||||
Reference in New Issue
Block a user