接单平台新增拼单与取消接单能力

拼单功能:
- 工单/物品规则支持拼单配置(启用、总数量、单价),总价=单价×数量三方联动,启用拼单时工单金额自动同步拼单总价
- 拼单参与按数量冻结比例押金,验收通过后按份额结算报酬,升级统计按数量计入
- 抢单大厅拼单入口:紫色渐变大按钮带单价展示,拼单进度/剩余份数,弹窗输入数量实时预估报酬与押金
- 我的订单展示拼单份额(数量×单价、状态),拼单者各自提交验收,全部提交后管理员统一验收
- 新增 work_order_shares 表与迁移 008

取消接单:
- 进行中订单可取消(押金全额退还、工单回大厅),30 天内限 5 次防滥用
This commit is contained in:
yml2213
2026-08-01 16:31:27 +08:00
parent 52a85f7f9e
commit c33e859e96
12 changed files with 1745 additions and 61 deletions
@@ -5,6 +5,7 @@ import {
ReloadOutlined,
SendOutlined,
SyncOutlined,
TeamOutlined,
WarningOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
@@ -44,6 +45,7 @@ import {
fetchAdminWorkerFinanceConfig,
fetchAdminWorkerFinanceRequests,
fetchAdminWorkCategories,
fetchAdminWorkOrderSharing,
fetchAdminWorkProductRules,
fetchAdminWorkerLevels,
fetchAdminWorkerPlatformSummary,
@@ -61,12 +63,14 @@ import {
submitAdminWorkOrderMaterial,
syncAdminWorkerOrdersFromSource,
unpublishAdminWorkOrder,
updateAdminWorkOrderSharing,
} from '@/services/admin'
import type {
CollectField,
UploadedFile,
WorkCategory,
WorkOrder,
WorkOrderShare,
WorkProductRule,
WorkerFinanceConfig,
WorkerFinanceRequest,
@@ -161,15 +165,84 @@ function WorkOrdersPanel() {
const [problemOrder, setProblemOrder] = useState<WorkOrder | null>(null)
const [resolutionOrder, setResolutionOrder] = useState<WorkOrder | null>(null)
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [problemForm] = Form.useForm()
const [resolutionForm] = Form.useForm()
const [materialForm] = Form.useForm()
const [sharingForm] = Form.useForm<{
enabled?: boolean
totalQuantity?: number
unitReward?: number
totalAmount?: number
}>()
const ordersQuery = useQuery({
queryKey: ['admin-worker-platform-orders', status, page, pageSize],
queryFn: () => fetchAdminWorkOrders({ status, page, pageSize }),
})
const ordersPagination = ordersQuery.data?.data.pagination
const sharingQuery = useQuery({
queryKey: ['admin-worker-platform-order-sharing', sharingOrder?.workOrderId],
queryFn: () => fetchAdminWorkOrderSharing(Number(sharingOrder?.workOrderId)),
enabled: Boolean(sharingOrder),
})
function openSharingConfigModal(row: WorkOrder) {
setSharingOrder(row)
const unitRewardFen = Number(row.sharing?.unitReward || 0)
const totalQuantity = Number(row.sharing?.totalQuantity || 1)
const unitRewardYuan = unitRewardFen / 100
sharingForm.setFieldsValue({
enabled: row.sharing?.enabled,
totalQuantity,
unitReward: unitRewardYuan,
totalAmount: unitRewardYuan * totalQuantity,
})
}
function syncSharingForm(next: {
totalQuantity?: number
unitReward?: number
totalAmount?: number
}) {
const values = sharingForm.getFieldsValue()
const unitReward = Number(next.unitReward ?? values.unitReward ?? 0)
const totalQuantity = Number(next.totalQuantity ?? values.totalQuantity ?? 1)
const totalAmount = Number(next.totalAmount ?? values.totalAmount ?? 0)
if (next.totalAmount !== undefined) {
const quantity = unitReward > 0 ? Math.max(1, Math.round(totalAmount / unitReward)) : 1
sharingForm.setFieldsValue({
totalQuantity: quantity,
totalAmount: quantity * unitReward,
})
return
}
sharingForm.setFieldsValue({
totalAmount: unitReward * totalQuantity,
})
}
async function submitSharingConfig(values: {
enabled?: boolean
totalQuantity?: number
unitReward?: number
totalAmount?: number
}) {
if (!sharingOrder) return
const succeeded = await runAction(
() =>
updateAdminWorkOrderSharing(sharingOrder.workOrderId, {
enabled: values.enabled !== false,
totalQuantity: Number(values.totalQuantity || 1),
unitReward: Number(values.unitReward || 0),
totalAmount: Number(values.totalAmount || 0),
}),
'拼单配置已保存',
)
if (!succeeded) return
setSharingOrder(null)
sharingForm.resetFields()
}
async function refreshAll() {
await Promise.all([
@@ -299,6 +372,26 @@ function WorkOrdersPanel() {
width: 110,
render: (_, row) => formatMoney(row.requiredDepositAmount),
},
{
title: '拼单',
width: 190,
render: (_, row) =>
row.sharing?.enabled ? (
<div className="cell-stack">
<Tag color="purple">
{formatMoney(row.sharing.unitReward)} / {row.sharing.totalQuantity}
</Tag>
<Typography.Text type="secondary">
{row.sharingProgress?.joinedQuantity || 0} / {row.sharing.totalQuantity}
{row.sharingProgress?.pendingSubmissionCount
? ` · ${row.sharingProgress.pendingSubmissionCount} 人待提交`
: ''}
</Typography.Text>
</div>
) : (
<Typography.Text type="secondary"></Typography.Text>
),
},
{
title: '状态',
width: 130,
@@ -336,6 +429,14 @@ function WorkOrdersPanel() {
render: (_, row) => (
<Space wrap>
<Button onClick={() => openDetailDrawer(row)}></Button>
{['pending_material', 'unassigned', 'open'].includes(row.status) ? (
<Button
icon={<TeamOutlined />}
onClick={() => openSharingConfigModal(row)}
>
</Button>
) : null}
{row.status === 'pending_material' ? (
<Button
icon={<FormOutlined />}
@@ -737,6 +838,138 @@ function WorkOrdersPanel() {
))}
</Form>
</Modal>
<Modal
title={`拼单配置 · ${sharingOrder?.productName || ''}`}
open={Boolean(sharingOrder)}
onCancel={() => {
setSharingOrder(null)
sharingForm.resetFields()
}}
onOk={() => sharingForm.submit()}
destroyOnHidden
width={720}
>
{sharingOrder ? (
<Space direction="vertical" size={16} className="full-width">
<Form
form={sharingForm}
layout="vertical"
initialValues={{
enabled: false,
totalQuantity: 1,
unitReward: 0,
}}
onFinish={submitSharingConfig}
>
<Form.Item
label="启用拼单"
name="enabled"
valuePropName="checked"
tooltip="启用后打手可在抢单大厅按单价×数量参与拼单,未拼满时整单仍可被抢走"
>
<Switch />
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue }) =>
getFieldValue('enabled') ? (
<Space size={16} wrap>
<Form.Item
label="总数量"
name="totalQuantity"
rules={[{ required: true, message: '请填写总数量' }]}
>
<InputNumber
min={1}
max={100000}
addonAfter="份"
onChange={(value) =>
syncSharingForm({
totalQuantity: Number(value) || 0,
})
}
/>
</Form.Item>
<Form.Item
label="单价"
name="unitReward"
rules={[{ required: true, message: '请填写单价' }]}
tooltip="每个数量份对应的报酬,修改后总价自动重算"
>
<InputNumber
min={0.01}
step={1}
addonAfter="元"
onChange={(value) =>
syncSharingForm({ unitReward: Number(value) || 0 })
}
/>
</Form.Item>
<Form.Item
label="拼单总价"
name="totalAmount"
rules={[{ required: true, message: '请填写总价' }]}
tooltip="总价 = 单价 × 数量,修改总价会自动反推数量"
>
<InputNumber
min={0.01}
step={1}
addonAfter="元"
onChange={(value) =>
syncSharingForm({ totalAmount: Number(value) || 0 })
}
/>
</Form.Item>
</Space>
) : null
}
</Form.Item>
</Form>
<Card size="small" title="拼单明细">
{sharingQuery.isLoading ? (
<Typography.Text type="secondary">...</Typography.Text>
) : (sharingQuery.data?.data.shares || []).length === 0 ? (
<Typography.Text type="secondary"></Typography.Text>
) : (
<Table<WorkOrderShare>
rowKey="shareId"
size="small"
dataSource={sharingQuery.data?.data.shares || []}
pagination={false}
columns={[
{
title: '打手',
render: (_, share) =>
share.worker?.displayName || share.worker?.username || '-',
},
{ title: '数量', dataIndex: 'quantity' },
{
title: '份额报酬',
render: (_, share) => formatMoney(share.shareReward),
},
{
title: '状态',
render: (_, share) => (
<Tag color={resolveSharingShareStatusColor(share.status)}>
{formatSharingShareStatus(share.status)}
</Tag>
),
},
{
title: '提交时间',
render: (_, share) =>
share.submittedAt
? formatAdminDateTime(share.submittedAt)
: '-',
},
]}
/>
)}
</Card>
</Space>
) : null}
</Modal>
</section>
)
}
@@ -828,6 +1061,10 @@ function ProductRulesPanel() {
rewardAmount?: number
requiredDepositAmount?: number
depositThresholdAmount?: number
sharingEnabled?: boolean
sharingTotalQuantity?: number
sharingUnitReward?: number
sharingTotalAmount?: number
fieldsText?: string
enabled?: boolean
autoCreate?: boolean
@@ -849,6 +1086,30 @@ function ProductRulesPanel() {
}
}
function syncSharingRuleForm(next: {
sharingTotalQuantity?: number
sharingUnitReward?: number
sharingTotalAmount?: number
}) {
const values = form.getFieldsValue()
const unitReward = Number(next.sharingUnitReward ?? values.sharingUnitReward ?? 0)
const totalQuantity = Number(
next.sharingTotalQuantity ?? values.sharingTotalQuantity ?? 1,
)
const totalAmount = Number(next.sharingTotalAmount ?? values.sharingTotalAmount ?? 0)
if (next.sharingTotalAmount !== undefined) {
const quantity = unitReward > 0 ? Math.max(1, Math.round(totalAmount / unitReward)) : 1
form.setFieldsValue({
sharingTotalQuantity: quantity,
sharingTotalAmount: quantity * unitReward,
})
return
}
form.setFieldsValue({
sharingTotalAmount: unitReward * totalQuantity,
})
}
const columns: TableColumnsType<WorkProductRule> = [
{
title: '规则',
@@ -893,6 +1154,11 @@ function ProductRulesPanel() {
{row.enabled ? '启用' : '停用'}
</Tag>
{row.autoCreate ? <Tag color="blue"></Tag> : null}
{row.sharing?.enabled ? (
<Tag color="purple">
{formatMoney(row.sharing.unitReward)}/
</Tag>
) : null}
</Space>
),
},
@@ -912,6 +1178,10 @@ function ProductRulesPanel() {
depositThresholdAmount: 200,
enabled: true,
autoCreate: false,
sharingEnabled: false,
sharingTotalQuantity: 10,
sharingUnitReward: 5,
sharingTotalAmount: 50,
fieldsText:
'gameAccount:游戏账号\nserverName:区服\nroleName:角色名',
}}
@@ -997,7 +1267,73 @@ function ProductRulesPanel() {
>
<Switch />
</Form.Item>
<Form.Item
label="启用拼单"
name="sharingEnabled"
valuePropName="checked"
tooltip="开启后按下方总数量与单价生成拼单工单"
>
<Switch />
</Form.Item>
</div>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue }) =>
getFieldValue('sharingEnabled') ? (
<Space size={16} wrap style={{ marginBottom: 16 }}>
<Form.Item
label="拼单总数量"
name="sharingTotalQuantity"
rules={[{ required: true, message: '请填写总数量' }]}
>
<InputNumber
min={1}
max={100000}
addonAfter="份"
onChange={(value) =>
syncSharingRuleForm({
sharingTotalQuantity: Number(value) || 0,
})
}
/>
</Form.Item>
<Form.Item
label="拼单单价"
name="sharingUnitReward"
rules={[{ required: true, message: '请填写单价' }]}
tooltip="每个数量份对应的报酬,修改后总价自动重算"
>
<InputNumber
min={0.01}
step={1}
addonAfter="元"
onChange={(value) =>
syncSharingRuleForm({
sharingUnitReward: Number(value) || 0,
})
}
/>
</Form.Item>
<Form.Item
label="拼单总价"
name="sharingTotalAmount"
rules={[{ required: true, message: '请填写总价' }]}
tooltip="总价 = 单价 × 数量,修改总价会自动反推数量"
>
<InputNumber
min={0.01}
step={1}
addonAfter="元"
onChange={(value) =>
syncSharingRuleForm({
sharingTotalAmount: Number(value) || 0,
})
}
/>
</Form.Item>
</Space>
) : null
}
</Form.Item>
<Form.Item label="资料字段" name="fieldsText">
<Input.TextArea rows={4} />
</Form.Item>
@@ -2423,6 +2759,23 @@ function formatFinanceChannel(channel: string) {
return channel || '-'
}
function formatSharingShareStatus(status: string) {
const labels: Record<string, string> = {
joined: '已参与',
submitted: '已提交验收',
accepted: '已验收',
cancelled: '已取消',
}
return labels[status] || status
}
function resolveSharingShareStatusColor(status: string) {
if (status === 'accepted') return 'green'
if (status === 'submitted') return 'gold'
if (status === 'cancelled') return 'default'
return 'purple'
}
function maskFinanceAccount(value: string) {
const text = String(value || '').trim()
if (!text) return '-'
+132 -11
View File
@@ -9,6 +9,8 @@ import {
Drawer,
Empty,
Input,
InputNumber,
Modal,
Pagination,
Space,
Spin,
@@ -23,6 +25,7 @@ import {
fetchWorkerMyOrders,
fetchWorkerProfile,
grabWorkerOrder,
joinWorkerSharingOrder,
} from '@/services/worker'
import type { CollectField, WorkOrder, WorkerUser } from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
@@ -43,6 +46,9 @@ export default function WorkerHallPage() {
const [pageSize, setPageSize] = useState(20)
const [grabbingId, setGrabbingId] = useState(0)
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [sharingQuantity, setSharingQuantity] = useState(1)
const [submittingSharing, setSubmittingSharing] = useState(false)
const ordersQuery = useQuery({
queryKey: ['worker-hall-orders', keyword, categoryId, page, pageSize],
@@ -135,6 +141,44 @@ export default function WorkerHallPage() {
}
}
function openSharingModal(order: WorkOrder) {
setSharingOrder(order)
setSharingQuantity(1)
}
async function submitSharing() {
if (!sharingOrder) return
setSubmittingSharing(true)
try {
await joinWorkerSharingOrder(sharingOrder.workOrderId, {
quantity: sharingQuantity,
})
message.success('拼单成功,已进入我的订单')
setSharingOrder(null)
setDetailOrder(null)
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '拼单失败')
} finally {
setSubmittingSharing(false)
}
}
function resolveSharingRemaining(order: WorkOrder): number {
const sharing = order.sharing
if (!sharing?.enabled) return 0
const progress = order.sharingProgress
return Math.max(0, sharing.totalQuantity - (progress?.joinedQuantity || 0))
}
function resolveSharingDeposit(order: WorkOrder, quantity: number): number {
const sharing = order.sharing
if (!sharing?.enabled) return 0
const deposit = Number(order.requiredDepositAmount || 0)
const total = Math.max(1, sharing.totalQuantity)
return Math.round((deposit * quantity) / total)
}
function applyKeywordSearch() {
setKeyword(keywordInput.trim())
setPage(1)
@@ -293,6 +337,18 @@ export default function WorkerHallPage() {
</Typography.Text>
</div>
{order.sharing?.enabled ? (
<div className="worker-hall-card-sharing">
<Tag color="purple"></Tag>
<Typography.Text type="secondary">
{formatMoney(order.sharing.unitReward)} · {' '}
{order.sharingProgress?.joinedQuantity || 0}/
{order.sharing.totalQuantity} · {' '}
{resolveSharingRemaining(order)}
</Typography.Text>
</div>
) : null}
<div className="worker-hall-card-amount-row">
<div className="worker-hall-card-amount">
{formatMoney(order.rewardAmount)}
@@ -315,17 +371,40 @@ export default function WorkerHallPage() {
) : null}
<div className="worker-hall-card-actions">
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(order)}>
</Button>
<Button
type="primary"
disabled={Boolean(disabledReason)}
loading={grabbingId === order.workOrderId}
onClick={() => submitGrab(order)}
>
</Button>
<div className="worker-hall-card-actions-row">
<Button
size="small"
type="text"
icon={<EyeOutlined />}
onClick={() => setDetailOrder(order)}
>
</Button>
</div>
<div className="worker-hall-card-grab-row">
{order.sharing?.enabled ? (
<Button
type="primary"
size="large"
className="worker-hall-sharing-button"
disabled={Boolean(disabledReason) || Boolean(order.myShare)}
onClick={() => openSharingModal(order)}
>
{order.myShare
? `已拼 ${order.myShare.quantity}`
: `拼单 ${formatMoney(order.sharing.unitReward)}/份`}
</Button>
) : null}
<Button
type="primary"
size="large"
disabled={Boolean(disabledReason)}
loading={grabbingId === order.workOrderId}
onClick={() => submitGrab(order)}
>
{disabledReason || '立即抢单'}
</Button>
</div>
</div>
</article>
)
@@ -424,6 +503,48 @@ export default function WorkerHallPage() {
</div>
) : null}
</Drawer>
<Modal
title={`拼单参与 · ${sharingOrder?.productName || ''}`}
open={Boolean(sharingOrder)}
confirmLoading={submittingSharing}
onCancel={() => setSharingOrder(null)}
onOk={submitSharing}
okText="确认拼单"
cancelText="取消"
okButtonProps={{ disabled: sharingQuantity <= 0 }}
>
{sharingOrder ? (
<Space direction="vertical" size={16} className="full-width">
<Alert
showIcon
type="info"
message={`单价 ${formatMoney(sharingOrder.sharing?.unitReward || 0)} / 份,剩余 ${resolveSharingRemaining(sharingOrder)} 份可拼`}
/>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="拼单进度">
{sharingOrder.sharingProgress?.joinedQuantity || 0} /{' '}
{sharingOrder.sharing?.totalQuantity || 0}
</Descriptions.Item>
<Descriptions.Item label="参与数量">
<InputNumber
min={1}
max={resolveSharingRemaining(sharingOrder)}
value={sharingQuantity}
onChange={(value) => setSharingQuantity(Number(value) || 1)}
addonAfter="份"
/>
</Descriptions.Item>
<Descriptions.Item label="预估报酬">
{formatMoney((sharingOrder.sharing?.unitReward || 0) * sharingQuantity)}
</Descriptions.Item>
<Descriptions.Item label="预估冻结押金">
{formatMoney(resolveSharingDeposit(sharingOrder, sharingQuantity))}
</Descriptions.Item>
</Descriptions>
</Space>
) : null}
</Modal>
</section>
)
}
@@ -29,7 +29,11 @@ import { useState } from 'react'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import ImageUpload from '@/components/files/ImageUpload'
import { fetchWorkerMyOrders, submitWorkerAcceptance } from '@/services/worker'
import {
cancelWorkerOrder,
fetchWorkerMyOrders,
submitWorkerAcceptance,
} from '@/services/worker'
import type { CollectField, UploadedFile, WorkOrder } from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
@@ -63,7 +67,7 @@ type DetailFieldItem = {
}
export default function WorkerOrdersPage() {
const { message } = App.useApp()
const { message, modal } = App.useApp()
const queryClient = useQueryClient()
const [status, setStatus] = useState('')
const [keywordInput, setKeywordInput] = useState('')
@@ -141,6 +145,26 @@ export default function WorkerOrdersPage() {
setPage(1)
}
function openCancelModal(order: WorkOrder) {
modal.confirm({
title: '确认取消接单?',
content:
'取消后订单将重新回到抢单大厅,冻结押金全额退还。30 天内取消次数有限,请谨慎操作。',
okText: '确认取消',
cancelText: '再想想',
okButtonProps: { danger: true },
onOk: async () => {
try {
await cancelWorkerOrder(order.workOrderId)
message.success('已取消接单,押金已退还')
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '取消接单失败')
}
},
})
}
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单信息',
@@ -177,6 +201,26 @@ export default function WorkerOrdersPage() {
</div>
),
},
{
title: '我的拼单',
width: 170,
render: (_, row) =>
row.myShare ? (
<div className="cell-stack">
<Tag color="purple">
{formatSharingShareStatus(row.myShare.status)}
</Tag>
<Typography.Text>
{row.myShare.quantity} × {formatMoney(row.myShare.unitReward)}
</Typography.Text>
<Typography.Text type="secondary">
{formatMoney(row.myShare.shareReward)}
</Typography.Text>
</div>
) : (
<Typography.Text type="secondary">-</Typography.Text>
),
},
{
title: '当前状态',
width: 180,
@@ -221,7 +265,7 @@ export default function WorkerOrdersPage() {
},
{
title: '操作',
width: 200,
width: 280,
render: (_, row) => (
<Space wrap>
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(row)}>
@@ -236,6 +280,14 @@ export default function WorkerOrdersPage() {
</Button>
) : null}
{canCancelOrder(row) ? (
<Button
danger
onClick={() => openCancelModal(row)}
>
</Button>
) : null}
</Space>
),
},
@@ -380,6 +432,11 @@ export default function WorkerOrdersPage() {
<Tag color={resolveStatusColor(detailOrder.status)}>
{formatStatus(detailOrder.status)}
</Tag>
{canCancelOrder(detailOrder) ? (
<Button danger onClick={() => openCancelModal(detailOrder)}>
</Button>
) : null}
{canSubmitAcceptance(detailOrder) ? (
<Button
type="primary"
@@ -523,9 +580,16 @@ export default function WorkerOrdersPage() {
}
function canSubmitAcceptance(order: WorkOrder) {
if (order.myShare) {
return order.myShare.status === 'joined' && order.status !== 'accepted'
}
return ['in_progress', 'problem'].includes(order.status)
}
function canCancelOrder(order: WorkOrder) {
return !order.myShare && order.status === 'in_progress'
}
function getRequirementFields(order: WorkOrder | null): CollectField[] {
if (!order) return []
const rawFields = Array.isArray(order.requirement?.fields)
@@ -688,3 +752,13 @@ function resolveStatusColor(status: string) {
if (status === 'pending_acceptance') return 'gold'
return 'blue'
}
function formatSharingShareStatus(status: string) {
const labels: Record<string, string> = {
joined: '已参与',
submitted: '已提交验收',
accepted: '已验收',
cancelled: '已取消',
}
return labels[status] || status
}
@@ -2,6 +2,7 @@ import { apiDelete, apiGet, apiPost } from '@/lib/http'
import type {
WorkCategory,
WorkOrder,
WorkOrderShare,
WorkProductRule,
WorkerFinanceConfig,
WorkerFinanceRequest,
@@ -97,6 +98,10 @@ export function saveAdminWorkProductRule(payload: {
rewardAmount?: number
requiredDepositAmount?: number
depositThresholdAmount?: number
sharingEnabled?: boolean
sharingTotalQuantity?: number
sharingUnitReward?: number
sharingTotalAmount?: number
fieldsText?: string
sortOrder?: number
}) {
@@ -217,6 +222,27 @@ export function unpublishAdminWorkOrder(workOrderId: number) {
)
}
export function fetchAdminWorkOrderSharing(workOrderId: number) {
return apiGet<{ order: WorkOrder; shares: WorkOrderShare[] }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/sharing`,
)
}
export function updateAdminWorkOrderSharing(
workOrderId: number,
payload: {
enabled: boolean
totalQuantity: number
unitReward: number
totalAmount?: number
},
) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/sharing`,
payload,
)
}
export function markAdminWorkOrderProblem(workOrderId: number, note: string) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/problem`,
+14
View File
@@ -3,6 +3,7 @@ import type {
CollectLookupResponse,
UploadedFile,
WorkOrder,
WorkOrderShare,
WorkerFinanceRequest,
WorkerHallOrdersResponse,
WorkerListResponse,
@@ -107,6 +108,13 @@ export function grabWorkerOrder(workOrderId: number) {
)
}
export function joinWorkerSharingOrder(workOrderId: number, payload: { quantity: number }) {
return apiPost<{ share: WorkOrderShare }>(
`/api/v1/worker/hall/orders/${workOrderId}/join-sharing`,
payload,
)
}
export function fetchWorkerMyOrders(params?: Record<string, unknown>) {
return apiGet<WorkerListResponse<WorkOrder>>('/api/v1/worker/orders', params)
}
@@ -121,6 +129,12 @@ export function submitWorkerAcceptance(
)
}
export function cancelWorkerOrder(workOrderId: number) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/worker/orders/${workOrderId}/cancel`,
)
}
export function lookupCollectOrder(orderNo: string) {
return apiPost<CollectLookupResponse>('/api/v1/collect/lookup', { orderNo })
}
+39 -5
View File
@@ -1811,13 +1811,43 @@ select {
}
.worker-hall-card-actions {
display: flex;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
display: grid;
gap: 8px;
margin-top: auto;
}
.worker-hall-card-actions-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.worker-hall-card-actions-row .ant-btn {
padding-inline: 8px;
}
.worker-hall-card-grab-row {
display: flex;
gap: 8px;
}
.worker-hall-card-grab-row .ant-btn {
flex: 1;
min-width: 0;
}
.worker-hall-sharing-button {
background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important;
border-color: #7c3aed !important;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.28) !important;
}
.worker-hall-sharing-button:not(:disabled):hover {
background: linear-gradient(135deg, #7c3aed, #6d28d9) !important;
border-color: #6d28d9 !important;
}
.worker-hall-detail-stack {
display: grid;
gap: 16px;
@@ -1905,11 +1935,15 @@ select {
padding: 16px;
}
.worker-hall-card-actions {
.worker-hall-card-actions-row {
align-items: stretch;
flex-direction: column;
}
.worker-hall-card-grab-row {
flex-direction: column;
}
}
@media (max-width: 480px) {
@@ -142,6 +142,7 @@ export type WorkProductRule = {
rewardAmount: number
requiredDepositAmount: number
depositThresholdAmount: number
sharing?: WorkOrderSharingConfig
requirement: {
fields: CollectField[]
}
@@ -171,6 +172,33 @@ export type WorkOrderStatus =
| 'cancelled'
| string
export type WorkOrderSharingConfig = {
enabled: boolean
totalQuantity: number
unitReward: number
}
export type WorkOrderShare = {
shareId: number
workOrderId: number
workerId: number
quantity: number
unitReward: number
shareReward: number
shareDeposit: number
status: string
acceptance: Record<string, unknown>
submittedAt: string | null
acceptedAt: string | null
createdAt: string
updatedAt: string
worker: null | {
workerId: number
username: string
displayName: string
}
}
export type WorkOrder = {
workOrderId: number
orderId?: number | null
@@ -184,6 +212,12 @@ export type WorkOrder = {
requiredDepositAmount?: number
depositThresholdAmount?: number
freezeDepositAmount?: number
sharing?: WorkOrderSharingConfig
myShare?: WorkOrderShare | null
sharingProgress?: {
joinedQuantity: number
pendingSubmissionCount: number
}
material: Record<string, unknown>
requirement: Record<string, unknown>
acceptance: {