增加工单首次查看标识
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
-- 032_worker_work_order_views.sql —— 打手个人工单首次查看状态。
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS worker_work_order_views (
|
||||||
|
work_order_id BIGINT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
|
||||||
|
worker_id BIGINT NOT NULL REFERENCES worker_users(id) ON DELETE CASCADE,
|
||||||
|
viewed_at TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (work_order_id, worker_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_worker_work_order_views_worker_order
|
||||||
|
ON worker_work_order_views(worker_id, work_order_id);
|
||||||
|
|
||||||
|
COMMENT ON TABLE worker_work_order_views IS '打手个人工单首次查看记录,用于展示未读“新”标识';
|
||||||
|
COMMENT ON COLUMN worker_work_order_views.viewed_at IS '首次点击订单详情、复制资料、编辑备注或验收时写入';
|
||||||
@@ -472,6 +472,51 @@ export async function upsertWorkerWorkOrderNote(input: {
|
|||||||
return String(result.rows[0]?.note || '')
|
return String(result.rows[0]?.note || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 查询打手已查看过的工单,用于计算“新”标识。 */
|
||||||
|
export async function listWorkerWorkOrderViews(
|
||||||
|
workerId: number | string,
|
||||||
|
workOrderIds: number[],
|
||||||
|
): Promise<Array<{ work_order_id: number; viewed_at: string }>> {
|
||||||
|
if (workOrderIds.length === 0) return []
|
||||||
|
const result = await query<{ work_order_id: number; viewed_at: string }>(
|
||||||
|
`
|
||||||
|
SELECT work_order_id, viewed_at
|
||||||
|
FROM worker_work_order_views
|
||||||
|
WHERE worker_id = $1 AND work_order_id = ANY($2::bigint[])
|
||||||
|
`,
|
||||||
|
[Number(workerId), workOrderIds],
|
||||||
|
)
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 首次查看工单时写入已读记录;重复调用保持原查看时间不变。 */
|
||||||
|
export async function markWorkerWorkOrderViewed(input: {
|
||||||
|
workOrderId: number
|
||||||
|
workerId: number
|
||||||
|
now: string
|
||||||
|
}): Promise<string> {
|
||||||
|
const inserted = await query<{ viewed_at: string }>(
|
||||||
|
`
|
||||||
|
INSERT INTO worker_work_order_views (work_order_id, worker_id, viewed_at)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (work_order_id, worker_id) DO NOTHING
|
||||||
|
RETURNING viewed_at
|
||||||
|
`,
|
||||||
|
[input.workOrderId, input.workerId, input.now],
|
||||||
|
)
|
||||||
|
if (inserted.rows[0]?.viewed_at) return String(inserted.rows[0].viewed_at)
|
||||||
|
const existing = await query<{ viewed_at: string }>(
|
||||||
|
`
|
||||||
|
SELECT viewed_at
|
||||||
|
FROM worker_work_order_views
|
||||||
|
WHERE work_order_id = $1 AND worker_id = $2
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[input.workOrderId, input.workerId],
|
||||||
|
)
|
||||||
|
return String(existing.rows[0]?.viewed_at || input.now)
|
||||||
|
}
|
||||||
|
|
||||||
async function getWorkOrderShareById(shareId: number): Promise<WorkOrderShareRow | null> {
|
async function getWorkOrderShareById(shareId: number): Promise<WorkOrderShareRow | null> {
|
||||||
const result = await query<WorkOrderShareRow>(
|
const result = await query<WorkOrderShareRow>(
|
||||||
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
listWorkerProfileWalletLedgers,
|
listWorkerProfileWalletLedgers,
|
||||||
listWorkerHallOrders,
|
listWorkerHallOrders,
|
||||||
listWorkerMyOrders,
|
listWorkerMyOrders,
|
||||||
|
markWorkerOrderRead,
|
||||||
getWorkerMyOrderEvents,
|
getWorkerMyOrderEvents,
|
||||||
loginWorker,
|
loginWorker,
|
||||||
getWorkerSessions,
|
getWorkerSessions,
|
||||||
@@ -376,6 +377,20 @@ router.get(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/orders/:workOrderId/read',
|
||||||
|
requireActiveWorker,
|
||||||
|
createRouteHandler(
|
||||||
|
(req) =>
|
||||||
|
markWorkerOrderRead(String(req.params.workOrderId || ''), getRequiredWorkerSession(req)),
|
||||||
|
{
|
||||||
|
successMessage: '订单已标记为已读',
|
||||||
|
errorMessage: '标记订单已读失败',
|
||||||
|
scope: '[worker/orders/:workOrderId/read]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/orders/:workOrderId/note',
|
'/orders/:workOrderId/note',
|
||||||
requireActiveWorker,
|
requireActiveWorker,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getWorkerUserById,
|
getWorkerUserById,
|
||||||
getWorkOrderById,
|
getWorkOrderById,
|
||||||
listWorkerWorkOrderNotes,
|
listWorkerWorkOrderNotes,
|
||||||
|
listWorkerWorkOrderViews,
|
||||||
listWorkOrderShares,
|
listWorkOrderShares,
|
||||||
sumPendingUnfreezeByOrderIds,
|
sumPendingUnfreezeByOrderIds,
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
@@ -283,15 +284,26 @@ async function publishWorkOrderSnapshots(input: {
|
|||||||
)) {
|
)) {
|
||||||
const worker = await getWorkerUserById(workerId)
|
const worker = await getWorkerUserById(workerId)
|
||||||
if (!worker) continue
|
if (!worker) continue
|
||||||
const notes = await listWorkerWorkOrderNotes(workerId, [workOrderId])
|
const [notes, views] = await Promise.all([
|
||||||
|
listWorkerWorkOrderNotes(workerId, [workOrderId]),
|
||||||
|
listWorkerWorkOrderViews(workerId, [workOrderId]),
|
||||||
|
])
|
||||||
const note = notes.find((item) => Number(item.work_order_id) === workOrderId)?.note || ''
|
const note = notes.find((item) => Number(item.work_order_id) === workOrderId)?.note || ''
|
||||||
|
const viewedAt =
|
||||||
|
views.find((item) => Number(item.work_order_id) === workOrderId)?.viewed_at || null
|
||||||
const share = shares.find((item) => Number(item.worker_id) === workerId)
|
const share = shares.find((item) => Number(item.worker_id) === workerId)
|
||||||
publishRealtimeEvent({
|
publishRealtimeEvent({
|
||||||
type: 'work_order.changed',
|
type: 'work_order.changed',
|
||||||
entityId: workOrderId,
|
entityId: workOrderId,
|
||||||
scopes: ['worker_my_orders'],
|
scopes: ['worker_my_orders'],
|
||||||
operation: 'upsert',
|
operation: 'upsert',
|
||||||
data: mapWorkOrderForWorker(workOrder, resolveWorkerPermissions(worker), share, note),
|
data: mapWorkOrderForWorker(
|
||||||
|
workOrder,
|
||||||
|
resolveWorkerPermissions(worker),
|
||||||
|
share,
|
||||||
|
note,
|
||||||
|
viewedAt,
|
||||||
|
),
|
||||||
workerIds: [workerId],
|
workerIds: [workerId],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ export function mapWorkOrderForWorker(
|
|||||||
permissions: { depositFreeAmount: number },
|
permissions: { depositFreeAmount: number },
|
||||||
myShare?: WorkOrderShareRow | null,
|
myShare?: WorkOrderShareRow | null,
|
||||||
workerNote = '',
|
workerNote = '',
|
||||||
|
viewedAt?: string | null,
|
||||||
) {
|
) {
|
||||||
const mapped = mapWorkOrderAdmin(workOrder)
|
const mapped = mapWorkOrderAdmin(workOrder)
|
||||||
const {
|
const {
|
||||||
@@ -380,6 +381,7 @@ export function mapWorkOrderForWorker(
|
|||||||
freezeDepositAmount: resolveFreezeDepositAmount(workOrder, permissions),
|
freezeDepositAmount: resolveFreezeDepositAmount(workOrder, permissions),
|
||||||
workerNote,
|
workerNote,
|
||||||
myShare: myShare ? mapWorkOrderShare(myShare) : null,
|
myShare: myShare ? mapWorkOrderShare(myShare) : null,
|
||||||
|
...(viewedAt === undefined ? {} : { isUnread: !viewedAt }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
isWorkerPasswordStrong,
|
isWorkerPasswordStrong,
|
||||||
normalizeWithdrawChannel,
|
normalizeWithdrawChannel,
|
||||||
normalizeRequirementFields,
|
normalizeRequirementFields,
|
||||||
|
mapWorkOrderForWorker,
|
||||||
resolveCollectSubmitTargetWorkOrder,
|
resolveCollectSubmitTargetWorkOrder,
|
||||||
resolveWorkerPermissions,
|
resolveWorkerPermissions,
|
||||||
resolveSkuNameQuantity,
|
resolveSkuNameQuantity,
|
||||||
@@ -197,6 +198,17 @@ function buildWorkOrderRow(overrides: Partial<WorkOrderRow> = {}): WorkOrderRow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('mapWorkOrderForWorker reflects the personal first-view state', () => {
|
||||||
|
const workOrder = buildWorkOrderRow({ assigned_worker_id: 1 })
|
||||||
|
const permissions = resolveWorkerPermissions(buildWorkerUserRow())
|
||||||
|
assert.equal(mapWorkOrderForWorker(workOrder, permissions, null, '', null).isUnread, true)
|
||||||
|
assert.equal(
|
||||||
|
mapWorkOrderForWorker(workOrder, permissions, null, '', '2026-08-17T00:00:00.000Z').isUnread,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
assert.equal(mapWorkOrderForWorker(workOrder, permissions).isUnread, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
test('resolveCollectSubmitTargetWorkOrder returns the only pending work order when no id is provided', () => {
|
test('resolveCollectSubmitTargetWorkOrder returns the only pending work order when no id is provided', () => {
|
||||||
const workOrder = buildWorkOrderRow()
|
const workOrder = buildWorkOrderRow()
|
||||||
const selected = resolveCollectSubmitTargetWorkOrder('ORDER-001', [workOrder], '')
|
const selected = resolveCollectSubmitTargetWorkOrder('ORDER-001', [workOrder], '')
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
listWorkerSharesByWorker,
|
listWorkerSharesByWorker,
|
||||||
listWorkerWalletLedgers,
|
listWorkerWalletLedgers,
|
||||||
listWorkerWorkOrderNotes,
|
listWorkerWorkOrderNotes,
|
||||||
|
listWorkerWorkOrderViews,
|
||||||
listWorkerWithdrawalAccounts,
|
listWorkerWithdrawalAccounts,
|
||||||
listWorkCategories,
|
listWorkCategories,
|
||||||
listWorkOrders,
|
listWorkOrders,
|
||||||
@@ -60,6 +61,7 @@ import {
|
|||||||
updateWorkOrderShareAcceptanceRecord,
|
updateWorkOrderShareAcceptanceRecord,
|
||||||
updateWorkOrderShareDraftAcceptance,
|
updateWorkOrderShareDraftAcceptance,
|
||||||
upsertWorkerWorkOrderNote,
|
upsertWorkerWorkOrderNote,
|
||||||
|
markWorkerWorkOrderViewed,
|
||||||
updateWorkerPassword,
|
updateWorkerPassword,
|
||||||
upsertWorkCategory,
|
upsertWorkCategory,
|
||||||
upsertWorkerLevel,
|
upsertWorkerLevel,
|
||||||
@@ -1335,13 +1337,17 @@ export async function listWorkerMyOrders(query: JsonObject = {}, session: Worker
|
|||||||
const permissions = resolveWorkerPermissions(worker)
|
const permissions = resolveWorkerPermissions(worker)
|
||||||
const myShares = await listWorkerSharesByWorker(worker.id)
|
const myShares = await listWorkerSharesByWorker(worker.id)
|
||||||
const myShareByOrderId = new Map(myShares.map((share) => [Number(share.work_order_id), share]))
|
const myShareByOrderId = new Map(myShares.map((share) => [Number(share.work_order_id), share]))
|
||||||
const notes = await listWorkerWorkOrderNotes(
|
const workOrderIds = items.map((item) => Number(item.id))
|
||||||
worker.id,
|
const [notes, views] = await Promise.all([
|
||||||
items.map((item) => Number(item.id)),
|
listWorkerWorkOrderNotes(worker.id, workOrderIds),
|
||||||
)
|
listWorkerWorkOrderViews(worker.id, workOrderIds),
|
||||||
|
])
|
||||||
const noteByOrderId = new Map(
|
const noteByOrderId = new Map(
|
||||||
notes.map((item) => [Number(item.work_order_id), String(item.note || '')]),
|
notes.map((item) => [Number(item.work_order_id), String(item.note || '')]),
|
||||||
)
|
)
|
||||||
|
const viewedAtByOrderId = new Map(
|
||||||
|
views.map((item) => [Number(item.work_order_id), String(item.viewed_at || '')]),
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
items: items.map((item) =>
|
items: items.map((item) =>
|
||||||
mapWorkOrderForWorker(
|
mapWorkOrderForWorker(
|
||||||
@@ -1349,6 +1355,7 @@ export async function listWorkerMyOrders(query: JsonObject = {}, session: Worker
|
|||||||
permissions,
|
permissions,
|
||||||
myShareByOrderId.get(Number(item.id)),
|
myShareByOrderId.get(Number(item.id)),
|
||||||
noteByOrderId.get(Number(item.id)) || '',
|
noteByOrderId.get(Number(item.id)) || '',
|
||||||
|
viewedAtByOrderId.get(Number(item.id)) || null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
pagination: { page, pageSize, total },
|
pagination: { page, pageSize, total },
|
||||||
@@ -1456,6 +1463,34 @@ export async function saveWorkerOrderNote(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 标记打手本人已查看工单,首次操作后不再展示“新”标识。 */
|
||||||
|
export async function markWorkerOrderRead(workOrderId: number | string, session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
const workerId = Number(session.workerId)
|
||||||
|
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
||||||
|
const isOrderOwner =
|
||||||
|
Number(workOrder.assigned_worker_id || 0) === workerId ||
|
||||||
|
Number(workOrder.last_assigned_worker_id || 0) === workerId
|
||||||
|
if (!sharingShare && !isOrderOwner) {
|
||||||
|
throw createHttpError('只能标记自己的订单为已读', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'work_order_read_owner_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewedAt = await markWorkerWorkOrderViewed({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerId,
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerIds: [workerId],
|
||||||
|
})
|
||||||
|
return { viewedAt, isUnread: false }
|
||||||
|
}
|
||||||
|
|
||||||
export async function joinWorkerSharingOrder(
|
export async function joinWorkerSharingOrder(
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
payload: JsonObject = {},
|
payload: JsonObject = {},
|
||||||
|
|||||||
@@ -269,12 +269,24 @@ function applyWorkerDepositRule(queryClient: ReturnType<typeof useQueryClient>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchesWorkOrderKeyword(
|
function matchesWorkOrderKeyword(
|
||||||
item: { productName?: unknown; platformOrderId?: unknown },
|
item: { productName?: unknown; platformOrderId?: unknown; material?: unknown },
|
||||||
keyword: string,
|
keyword: string,
|
||||||
) {
|
) {
|
||||||
const normalizedKeyword = keyword.trim().toLowerCase()
|
const normalizedKeyword = keyword.trim().toLowerCase()
|
||||||
if (!normalizedKeyword) return true
|
if (!normalizedKeyword) return true
|
||||||
return [item.productName, item.platformOrderId]
|
const material = isRecord(item.material) ? item.material : {}
|
||||||
|
const collect = isRecord(material.collect) ? material.collect : {}
|
||||||
|
const fields = isRecord(collect.fields) ? collect.fields : {}
|
||||||
|
return [
|
||||||
|
item.productName,
|
||||||
|
item.platformOrderId,
|
||||||
|
fields.gameId,
|
||||||
|
fields.gameNickname,
|
||||||
|
fields.gameAccount,
|
||||||
|
fields.roleName,
|
||||||
|
material.gameId,
|
||||||
|
material.gameNickname,
|
||||||
|
]
|
||||||
.map((value) => String(value || '').toLowerCase())
|
.map((value) => String(value || '').toLowerCase())
|
||||||
.some((value) => value.includes(normalizedKeyword))
|
.some((value) => value.includes(normalizedKeyword))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
fetchWorkerMyOrderOverview,
|
fetchWorkerMyOrderOverview,
|
||||||
fetchWorkerMyOrders,
|
fetchWorkerMyOrders,
|
||||||
fetchWorkerProfile,
|
fetchWorkerProfile,
|
||||||
|
markWorkerOrderRead,
|
||||||
remindWorkerAcceptance,
|
remindWorkerAcceptance,
|
||||||
saveWorkerAcceptanceDraft,
|
saveWorkerAcceptanceDraft,
|
||||||
saveWorkerOrderNote,
|
saveWorkerOrderNote,
|
||||||
@@ -231,7 +232,23 @@ export default function WorkerOrdersPage() {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markOrderAsRead(order: WorkOrder) {
|
||||||
|
if (!order.isUnread) return
|
||||||
|
queryClient.setQueriesData({ queryKey: ['worker-my-orders'] }, (current: unknown) =>
|
||||||
|
patchWorkOrderReadState(current, order.workOrderId),
|
||||||
|
)
|
||||||
|
void markWorkerOrderRead(order.workOrderId).catch(() => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOrderDetail(order: WorkOrder) {
|
||||||
|
markOrderAsRead(order)
|
||||||
|
setDetailOrder(order)
|
||||||
|
}
|
||||||
|
|
||||||
function copyOrderInfo(order: WorkOrder) {
|
function copyOrderInfo(order: WorkOrder) {
|
||||||
|
markOrderAsRead(order)
|
||||||
const items = getCopyOrderInfoItems(order)
|
const items = getCopyOrderInfoItems(order)
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
message.warning('该订单暂无可复制信息')
|
message.warning('该订单暂无可复制信息')
|
||||||
@@ -273,6 +290,7 @@ export default function WorkerOrdersPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openAcceptanceModal(order: WorkOrder) {
|
function openAcceptanceModal(order: WorkOrder) {
|
||||||
|
markOrderAsRead(order)
|
||||||
setEditingOrder(order)
|
setEditingOrder(order)
|
||||||
// 已提交:以已提交的验收资料为初始内容;未提交:以暂存草稿为初始内容
|
// 已提交:以已提交的验收资料为初始内容;未提交:以暂存草稿为初始内容
|
||||||
const source = hasSubmittedAcceptance(order)
|
const source = hasSubmittedAcceptance(order)
|
||||||
@@ -284,6 +302,7 @@ export default function WorkerOrdersPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openNoteModal(order: WorkOrder) {
|
function openNoteModal(order: WorkOrder) {
|
||||||
|
markOrderAsRead(order)
|
||||||
setNoteOrder(order)
|
setNoteOrder(order)
|
||||||
noteForm.setFieldsValue({ note: String(order.workerNote || '') })
|
noteForm.setFieldsValue({ note: String(order.workerNote || '') })
|
||||||
}
|
}
|
||||||
@@ -388,14 +407,21 @@ export default function WorkerOrdersPage() {
|
|||||||
width: 270,
|
width: 270,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
|
<Space size={4} align="center">
|
||||||
|
{row.isUnread ? (
|
||||||
|
<Tag color="red" className="worker-order-unread-tag">
|
||||||
|
新
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
strong
|
strong
|
||||||
onClick={() => setDetailOrder(row)}
|
onClick={() => openOrderDetail(row)}
|
||||||
title={row.productName || '未命名商品'}
|
title={row.productName || '未命名商品'}
|
||||||
ellipsis
|
ellipsis
|
||||||
>
|
>
|
||||||
{row.productName || '未命名商品'}
|
{row.productName || '未命名商品'}
|
||||||
</Typography.Link>
|
</Typography.Link>
|
||||||
|
</Space>
|
||||||
<Space size={[4, 4]} wrap>
|
<Space size={[4, 4]} wrap>
|
||||||
{row.categoryName ? <Tag color="blue">{row.categoryName}</Tag> : null}
|
{row.categoryName ? <Tag color="blue">{row.categoryName}</Tag> : null}
|
||||||
{row.myShare ? <Tag color="purple">拼单</Tag> : null}
|
{row.myShare ? <Tag color="purple">拼单</Tag> : null}
|
||||||
@@ -596,7 +622,7 @@ export default function WorkerOrdersPage() {
|
|||||||
<Button
|
<Button
|
||||||
aria-label="查看详情"
|
aria-label="查看详情"
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined />}
|
||||||
onClick={() => setDetailOrder(row)}
|
onClick={() => openOrderDetail(row)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{canEditAcceptanceImages(row) ? (
|
{canEditAcceptanceImages(row) ? (
|
||||||
@@ -724,6 +750,11 @@ export default function WorkerOrdersPage() {
|
|||||||
{/* 卡片顶栏:分类 + 订单号 + 状态/倒计时 */}
|
{/* 卡片顶栏:分类 + 订单号 + 状态/倒计时 */}
|
||||||
<div className="worker-order-mobile-header">
|
<div className="worker-order-mobile-header">
|
||||||
<div className="worker-order-mobile-header-left">
|
<div className="worker-order-mobile-header-left">
|
||||||
|
{order.isUnread ? (
|
||||||
|
<Tag color="red" className="worker-order-unread-tag">
|
||||||
|
新
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
{order.categoryName ? (
|
{order.categoryName ? (
|
||||||
<Tag color="blue" className="worker-order-mobile-tag">
|
<Tag color="blue" className="worker-order-mobile-tag">
|
||||||
{order.categoryName}
|
{order.categoryName}
|
||||||
@@ -892,7 +923,7 @@ export default function WorkerOrdersPage() {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined />}
|
||||||
onClick={() => setDetailOrder(order)}
|
onClick={() => openOrderDetail(order)}
|
||||||
>
|
>
|
||||||
详情
|
详情
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1304,6 +1335,27 @@ export default function WorkerOrdersPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patchWorkOrderReadState(current: unknown, workOrderId: number) {
|
||||||
|
if (!isRecord(current) || !isRecord(current.data) || !Array.isArray(current.data.items)) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
data: {
|
||||||
|
...current.data,
|
||||||
|
items: current.data.items.map((item) =>
|
||||||
|
isRecord(item) && Number(item.workOrderId) === workOrderId
|
||||||
|
? { ...item, isUnread: false }
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||||
|
}
|
||||||
|
|
||||||
function canSubmitAcceptance(order: WorkOrder) {
|
function canSubmitAcceptance(order: WorkOrder) {
|
||||||
if (order.myShare) {
|
if (order.myShare) {
|
||||||
return order.myShare.status === 'joined' && order.status !== 'accepted'
|
return order.myShare.status === 'joined' && order.status !== 'accepted'
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ export function fetchWorkerMyOrderEvents(workOrderId: number) {
|
|||||||
return apiGet<{ items: WorkOrderEvent[] }>(`/api/v1/worker/orders/${workOrderId}/events`)
|
return apiGet<{ items: WorkOrderEvent[] }>(`/api/v1/worker/orders/${workOrderId}/events`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function markWorkerOrderRead(workOrderId: number) {
|
||||||
|
return apiPost<{ viewedAt: string; isUnread: false }>(`/api/v1/worker/orders/${workOrderId}/read`)
|
||||||
|
}
|
||||||
|
|
||||||
export function saveWorkerOrderNote(workOrderId: number, note: string) {
|
export function saveWorkerOrderNote(workOrderId: number, note: string) {
|
||||||
return apiPost<{ order: WorkOrder }>(`/api/v1/worker/orders/${workOrderId}/note`, { note })
|
return apiPost<{ order: WorkOrder }>(`/api/v1/worker/orders/${workOrderId}/note`, { note })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1008,6 +1008,11 @@ body {
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.worker-order-unread-tag {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.worker-order-mobile-orderno-text {
|
.worker-order-mobile-orderno-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
|||||||
@@ -309,6 +309,8 @@ export type WorkOrder = {
|
|||||||
/** 暂存的验收草稿(未提交时保存于此,刷新不丢失;提交后清空) */
|
/** 暂存的验收草稿(未提交时保存于此,刷新不丢失;提交后清空) */
|
||||||
draftAcceptance: WorkerAcceptance
|
draftAcceptance: WorkerAcceptance
|
||||||
workerNote?: string
|
workerNote?: string
|
||||||
|
/** 打手本人尚未点击查看或操作过该工单。 */
|
||||||
|
isUnread?: boolean
|
||||||
problemNote: string
|
problemNote: string
|
||||||
worker: null | {
|
worker: null | {
|
||||||
workerId: number
|
workerId: number
|
||||||
|
|||||||
Reference in New Issue
Block a user