优化后端-接单平台部分逻辑
This commit is contained in:
@@ -28,16 +28,19 @@ import {
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
import JsonPreview from '@/components/admin/JsonPreview'
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import {
|
||||
acceptAdminWorkOrder,
|
||||
createAdminMockWorkOrder,
|
||||
creditAdminWorkerWallet,
|
||||
fetchAdminWorkerFinanceConfig,
|
||||
fetchAdminWorkerFinanceRequests,
|
||||
fetchAdminWorkCategories,
|
||||
fetchAdminWorkProductRules,
|
||||
fetchAdminWorkerLevels,
|
||||
@@ -46,13 +49,16 @@ import {
|
||||
fetchAdminWorkOrders,
|
||||
markAdminWorkOrderProblem,
|
||||
publishAdminWorkOrder,
|
||||
reviewAdminWorkerFinanceRequest,
|
||||
resolveAdminProblemWorkOrder,
|
||||
reviewAdminWorkerUser,
|
||||
saveAdminWorkerFinanceConfig,
|
||||
saveAdminWorkCategory,
|
||||
saveAdminWorkProductRule,
|
||||
saveAdminWorkerLevel,
|
||||
submitAdminWorkOrderMaterial,
|
||||
syncAdminWorkerOrdersFromSource,
|
||||
unpublishAdminWorkOrder,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
CollectField,
|
||||
@@ -60,6 +66,8 @@ import type {
|
||||
WorkCategory,
|
||||
WorkOrder,
|
||||
WorkProductRule,
|
||||
WorkerFinanceConfig,
|
||||
WorkerFinanceRequest,
|
||||
WorkerLevel,
|
||||
WorkerUser,
|
||||
} from '@/types/worker-platform'
|
||||
@@ -69,6 +77,23 @@ import {
|
||||
} from '@/utils/admin-pagination'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type FinanceConfigFormValues = {
|
||||
recharge?: {
|
||||
enabled?: boolean
|
||||
channelName?: string
|
||||
accountName?: string
|
||||
accountNo?: string
|
||||
qrCodeImageList?: UploadedFile[]
|
||||
instructions?: string
|
||||
}
|
||||
withdraw?: {
|
||||
enabled?: boolean
|
||||
instructions?: string
|
||||
}
|
||||
}
|
||||
|
||||
type FinanceReviewAction = 'approved' | 'rejected' | 'cancelled'
|
||||
|
||||
export default function AdminWorkerPlatformPage() {
|
||||
return (
|
||||
<section className="page-stack">
|
||||
@@ -84,6 +109,7 @@ export default function AdminWorkerPlatformPage() {
|
||||
{ key: 'rules', label: '物品规则', children: <ProductRulesPanel /> },
|
||||
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
||||
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
||||
{ key: 'finance', label: '资金', children: <FinancePanel /> },
|
||||
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
||||
]}
|
||||
/>
|
||||
@@ -123,7 +149,7 @@ function SummaryCards() {
|
||||
}
|
||||
|
||||
function WorkOrdersPanel() {
|
||||
const { message } = App.useApp()
|
||||
const { message, modal } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const [status, setStatus] = useState('')
|
||||
@@ -329,6 +355,26 @@ function WorkOrdersPanel() {
|
||||
发布
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'open' ? (
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
modal.confirm({
|
||||
title: '确认下架该订单?',
|
||||
content: '下架后订单会回到未分配状态,不再在抢单大厅展示。',
|
||||
okText: '确认下架',
|
||||
cancelText: '取消',
|
||||
onOk: () =>
|
||||
runAction(
|
||||
() => unpublishAdminWorkOrder(row.workOrderId),
|
||||
'订单已撤回到未分配',
|
||||
),
|
||||
})
|
||||
}}
|
||||
>
|
||||
下架
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'pending_acceptance' ? (
|
||||
<Button
|
||||
icon={<CheckOutlined />}
|
||||
@@ -1088,12 +1134,18 @@ function WorkersPanel() {
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null)
|
||||
const [levelWorker, setLevelWorker] = useState<WorkerUser | null>(null)
|
||||
const [creditForm] = Form.useForm()
|
||||
const [levelForm] = Form.useForm<{ levelId?: number }>()
|
||||
|
||||
const workersQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-workers', status, page, pageSize],
|
||||
queryFn: () => fetchAdminWorkerUsers({ status, page, pageSize }),
|
||||
})
|
||||
const levelsQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-levels'],
|
||||
queryFn: () => fetchAdminWorkerLevels(),
|
||||
})
|
||||
const workersPagination = workersQuery.data?.data.pagination
|
||||
|
||||
async function refreshWorkers() {
|
||||
@@ -1133,6 +1185,23 @@ function WorkersPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitLevel(values: { levelId?: number }) {
|
||||
if (!levelWorker) return
|
||||
try {
|
||||
await reviewAdminWorkerUser(levelWorker.workerId, {
|
||||
status: levelWorker.status,
|
||||
levelId: Number(values.levelId || 0),
|
||||
reviewNote: levelWorker.reviewNote,
|
||||
})
|
||||
message.success('打手等级已更新')
|
||||
setLevelWorker(null)
|
||||
levelForm.resetFields()
|
||||
await refreshWorkers()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '调整等级失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<WorkerUser> = [
|
||||
{
|
||||
title: '打手',
|
||||
@@ -1168,7 +1237,7 @@ function WorkersPanel() {
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 260,
|
||||
width: 340,
|
||||
render: (_, row) => (
|
||||
<Space wrap>
|
||||
{row.status !== 'active' ? (
|
||||
@@ -1177,6 +1246,16 @@ function WorkersPanel() {
|
||||
{row.status !== 'rejected' ? (
|
||||
<Button onClick={() => review(row, 'rejected')}>拒绝</Button>
|
||||
) : null}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLevelWorker(row)
|
||||
levelForm.setFieldsValue({
|
||||
levelId: row.level?.levelId,
|
||||
})
|
||||
}}
|
||||
>
|
||||
调等级
|
||||
</Button>
|
||||
<Button onClick={() => setCreditWorker(row)}>充值</Button>
|
||||
</Space>
|
||||
),
|
||||
@@ -1231,6 +1310,38 @@ function WorkersPanel() {
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="调整打手等级"
|
||||
open={Boolean(levelWorker)}
|
||||
onCancel={() => {
|
||||
setLevelWorker(null)
|
||||
levelForm.resetFields()
|
||||
}}
|
||||
onOk={() => levelForm.submit()}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={levelForm} layout="vertical" onFinish={submitLevel}>
|
||||
<Form.Item label="打手">
|
||||
<Typography.Text>
|
||||
{levelWorker?.displayName || levelWorker?.username || '-'}
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="目标等级"
|
||||
name="levelId"
|
||||
rules={[{ required: true, message: '请选择目标等级' }]}
|
||||
>
|
||||
<Select
|
||||
loading={levelsQuery.isLoading}
|
||||
options={(levelsQuery.data?.data.items || []).map((item) => ({
|
||||
value: item.levelId,
|
||||
label: `${item.name} / 最多同时 ${item.maxActiveOrders} 单`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="人工增加余额"
|
||||
open={Boolean(creditWorker)}
|
||||
@@ -1263,6 +1374,408 @@ function WorkersPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
function FinancePanel() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [configForm] = Form.useForm<FinanceConfigFormValues>()
|
||||
const [reviewForm] = Form.useForm<{ reviewedNote?: string }>()
|
||||
const [requestStatus, setRequestStatus] = useState('')
|
||||
const [requestType, setRequestType] = useState('')
|
||||
const [keywordInput, setKeywordInput] = useState('')
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [savingConfig, setSavingConfig] = useState(false)
|
||||
const [reviewing, setReviewing] = useState(false)
|
||||
const [reviewState, setReviewState] = useState<{
|
||||
action: FinanceReviewAction
|
||||
request: WorkerFinanceRequest
|
||||
} | null>(null)
|
||||
|
||||
const financeConfigQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-finance-config'],
|
||||
queryFn: () => fetchAdminWorkerFinanceConfig(),
|
||||
})
|
||||
const financeRequestsQuery = useQuery({
|
||||
queryKey: [
|
||||
'admin-worker-platform-finance-requests',
|
||||
requestStatus,
|
||||
requestType,
|
||||
keyword,
|
||||
page,
|
||||
pageSize,
|
||||
],
|
||||
queryFn: () =>
|
||||
fetchAdminWorkerFinanceRequests({
|
||||
status: requestStatus,
|
||||
requestType,
|
||||
keyword,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
})
|
||||
const financeRequestsPagination = financeRequestsQuery.data?.data.pagination
|
||||
|
||||
useEffect(() => {
|
||||
const config = financeConfigQuery.data?.data
|
||||
if (!config) {
|
||||
return
|
||||
}
|
||||
configForm.setFieldsValue(mapFinanceConfigToFormValues(config))
|
||||
}, [financeConfigQuery.data, configForm])
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-finance-config'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-finance-requests'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-workers'],
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
async function submitFinanceConfig(values: FinanceConfigFormValues) {
|
||||
setSavingConfig(true)
|
||||
try {
|
||||
await saveAdminWorkerFinanceConfig({
|
||||
recharge: {
|
||||
enabled: values.recharge?.enabled !== false,
|
||||
channelName: String(values.recharge?.channelName || '').trim(),
|
||||
accountName: String(values.recharge?.accountName || '').trim(),
|
||||
accountNo: String(values.recharge?.accountNo || '').trim(),
|
||||
qrCodeImage: values.recharge?.qrCodeImageList?.[0] || null,
|
||||
instructions: String(values.recharge?.instructions || '').trim(),
|
||||
},
|
||||
withdraw: {
|
||||
enabled: values.withdraw?.enabled !== false,
|
||||
instructions: String(values.withdraw?.instructions || '').trim(),
|
||||
},
|
||||
})
|
||||
message.success('资金配置已保存')
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存资金配置失败')
|
||||
} finally {
|
||||
setSavingConfig(false)
|
||||
}
|
||||
}
|
||||
|
||||
function openReviewModal(request: WorkerFinanceRequest, action: FinanceReviewAction) {
|
||||
setReviewState({ request, action })
|
||||
reviewForm.setFieldsValue({ reviewedNote: '' })
|
||||
}
|
||||
|
||||
async function submitFinanceReview(values: { reviewedNote?: string }) {
|
||||
if (!reviewState) return
|
||||
setReviewing(true)
|
||||
try {
|
||||
await reviewAdminWorkerFinanceRequest(reviewState.request.requestId, {
|
||||
status: reviewState.action,
|
||||
reviewedNote: String(values.reviewedNote || '').trim(),
|
||||
})
|
||||
message.success(`${formatFinanceReviewAction(reviewState.action)}成功`)
|
||||
setReviewState(null)
|
||||
reviewForm.resetFields()
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '处理资金申请失败')
|
||||
} finally {
|
||||
setReviewing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const requestColumns: TableColumnsType<WorkerFinanceRequest> = [
|
||||
{
|
||||
title: '打手',
|
||||
minWidth: 180,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong>
|
||||
{row.worker?.displayName || row.worker?.username || '-'}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{row.worker?.username || '-'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
width: 120,
|
||||
render: (_, row) => (
|
||||
<Tag color={row.requestType === 'withdraw' ? 'green' : 'blue'}>
|
||||
{formatFinanceRequestType(row.requestType)}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
width: 120,
|
||||
render: (_, row) => formatMoney(row.amount),
|
||||
},
|
||||
{
|
||||
title: '收款信息',
|
||||
minWidth: 240,
|
||||
render: (_, row) => renderFinanceRequestAccount(row),
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
minWidth: 260,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text>{row.note || '-'}</Typography.Text>
|
||||
{row.reviewedNote ? (
|
||||
<Typography.Text type="secondary">
|
||||
审核备注:{row.reviewedNote}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 120,
|
||||
render: (_, row) => (
|
||||
<Tag color={resolveFinanceRequestStatusColor(row.status)}>
|
||||
{formatFinanceRequestStatus(row.status)}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
width: 180,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text>{formatAdminDateTime(row.createdAt)}</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{row.reviewedAt ? `审核:${formatAdminDateTime(row.reviewedAt)}` : '待审核'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 240,
|
||||
render: (_, row) =>
|
||||
row.status === 'pending' ? (
|
||||
<Space wrap>
|
||||
<Button type="primary" onClick={() => openReviewModal(row, 'approved')}>
|
||||
通过
|
||||
</Button>
|
||||
<Button onClick={() => openReviewModal(row, 'rejected')}>拒绝</Button>
|
||||
<Button danger onClick={() => openReviewModal(row, 'cancelled')}>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Typography.Text type="secondary">已处理</Typography.Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="platform-panel-stack">
|
||||
<Card
|
||||
title="资金配置"
|
||||
bordered={false}
|
||||
loading={financeConfigQuery.isLoading}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={financeConfigQuery.isFetching}
|
||||
onClick={() => financeConfigQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{financeConfigQuery.error ? (
|
||||
<Typography.Text type="danger">
|
||||
{financeConfigQuery.error instanceof Error
|
||||
? financeConfigQuery.error.message
|
||||
: '读取资金配置失败'}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
<Form
|
||||
form={configForm}
|
||||
layout="vertical"
|
||||
onFinish={submitFinanceConfig}
|
||||
initialValues={mapFinanceConfigToFormValues()}
|
||||
>
|
||||
<Card title="充值入口" size="small" style={{ marginBottom: 16 }}>
|
||||
<Form.Item
|
||||
label="开启充值申请"
|
||||
name={['recharge', 'enabled']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="收款通道名称" name={['recharge', 'channelName']}>
|
||||
<Input placeholder="如:支付宝扫码、微信收款" />
|
||||
</Form.Item>
|
||||
<Form.Item label="收款人" name={['recharge', 'accountName']}>
|
||||
<Input placeholder="请输入收款人姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item label="收款账号" name={['recharge', 'accountNo']}>
|
||||
<Input placeholder="请输入支付宝账号、微信号或银行卡号" />
|
||||
</Form.Item>
|
||||
<Form.Item label="收款二维码" name={['recharge', 'qrCodeImageList']}>
|
||||
<ImageUpload scene="worker-finance" scope="admin" maxCount={1} />
|
||||
</Form.Item>
|
||||
<Form.Item label="充值说明" name={['recharge', 'instructions']}>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="例如:转账后请填写付款时间、付款尾号、截图说明。"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Card title="提现入口" size="small" style={{ marginBottom: 16 }}>
|
||||
<Form.Item
|
||||
label="开启提现申请"
|
||||
name={['withdraw', 'enabled']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="提现说明" name={['withdraw', 'instructions']}>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="例如:每天 18:00 前审核,当日打款;银行卡请备注开户行。"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Button type="primary" htmlType="submit" loading={savingConfig}>
|
||||
保存配置
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="资金申请"
|
||||
bordered={false}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Input.Search
|
||||
allowClear
|
||||
value={keywordInput}
|
||||
placeholder="搜索打手账号/昵称/手机号"
|
||||
style={{ width: 240 }}
|
||||
onChange={(event) => setKeywordInput(event.target.value)}
|
||||
onSearch={(value) => {
|
||||
setKeyword(String(value || '').trim())
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={requestType}
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: '', label: '全部类型' },
|
||||
{ value: 'recharge', label: '充值申请' },
|
||||
{ value: 'withdraw', label: '提现申请' },
|
||||
]}
|
||||
onChange={(nextType) => {
|
||||
setRequestType(nextType)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={requestStatus}
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'approved', label: '已通过' },
|
||||
{ value: 'rejected', label: '已拒绝' },
|
||||
{ value: 'cancelled', label: '已取消' },
|
||||
]}
|
||||
onChange={(nextStatus) => {
|
||||
setRequestStatus(nextStatus)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={financeRequestsQuery.isFetching}
|
||||
onClick={() => financeRequestsQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table<WorkerFinanceRequest>
|
||||
rowKey="requestId"
|
||||
loading={financeRequestsQuery.isLoading}
|
||||
dataSource={financeRequestsQuery.data?.data.items || []}
|
||||
columns={requestColumns}
|
||||
pagination={buildAdminTablePagination({
|
||||
current: financeRequestsPagination?.page || page,
|
||||
pageSize: financeRequestsPagination?.pageSize || pageSize,
|
||||
total: financeRequestsPagination?.total || 0,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
},
|
||||
})}
|
||||
scroll={{ x: 1380 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={reviewState ? `${formatFinanceReviewAction(reviewState.action)}资金申请` : '处理资金申请'}
|
||||
open={Boolean(reviewState)}
|
||||
destroyOnHidden
|
||||
confirmLoading={reviewing}
|
||||
onCancel={() => {
|
||||
setReviewState(null)
|
||||
reviewForm.resetFields()
|
||||
}}
|
||||
onOk={() => reviewForm.submit()}
|
||||
>
|
||||
{reviewState ? (
|
||||
<Space direction="vertical" size={16} className="full-width">
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="打手">
|
||||
{reviewState.request.worker?.displayName ||
|
||||
reviewState.request.worker?.username ||
|
||||
'-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="申请类型">
|
||||
{formatFinanceRequestType(reviewState.request.requestType)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="金额">
|
||||
{formatMoney(reviewState.request.amount)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="收款信息">
|
||||
{renderFinanceRequestAccountText(reviewState.request)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="申请备注">
|
||||
{reviewState.request.note || '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Form form={reviewForm} layout="vertical" onFinish={submitFinanceReview}>
|
||||
<Form.Item label="审核备注" name="reviewedNote">
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder={`请输入${formatFinanceReviewAction(reviewState.action)}备注`}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
) : null}
|
||||
</Modal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function LevelsPanel() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -1556,3 +2069,115 @@ function formatWorkerStatus(status: string) {
|
||||
}
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
function mapFinanceConfigToFormValues(
|
||||
config?: WorkerFinanceConfig,
|
||||
): FinanceConfigFormValues {
|
||||
return {
|
||||
recharge: {
|
||||
enabled: config?.recharge.enabled !== false,
|
||||
channelName: config?.recharge.channelName || '',
|
||||
accountName: config?.recharge.accountName || '',
|
||||
accountNo: config?.recharge.accountNo || '',
|
||||
qrCodeImageList: config?.recharge.qrCodeImage ? [config.recharge.qrCodeImage] : [],
|
||||
instructions: config?.recharge.instructions || '',
|
||||
},
|
||||
withdraw: {
|
||||
enabled: config?.withdraw.enabled !== false,
|
||||
instructions: config?.withdraw.instructions || '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function formatFinanceReviewAction(action: FinanceReviewAction) {
|
||||
if (action === 'approved') return '通过'
|
||||
if (action === 'rejected') return '拒绝'
|
||||
return '取消'
|
||||
}
|
||||
|
||||
function formatFinanceRequestType(requestType: string) {
|
||||
if (requestType === 'recharge') return '充值申请'
|
||||
if (requestType === 'withdraw') return '提现申请'
|
||||
return requestType || '-'
|
||||
}
|
||||
|
||||
function formatFinanceRequestStatus(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
return labels[status] || status || '-'
|
||||
}
|
||||
|
||||
function resolveFinanceRequestStatusColor(status: string) {
|
||||
if (status === 'approved') return 'green'
|
||||
if (status === 'pending') return 'gold'
|
||||
if (status === 'rejected') return 'red'
|
||||
if (status === 'cancelled') return 'default'
|
||||
return 'blue'
|
||||
}
|
||||
|
||||
function renderFinanceRequestAccount(request: WorkerFinanceRequest) {
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text>{renderFinanceRequestAccountText(request)}</Typography.Text>
|
||||
{request.requestType === 'withdraw' && request.accountNo ? (
|
||||
<Typography.Text type="secondary">
|
||||
{maskFinanceAccount(request.accountNo)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderFinanceRequestAccountText(request: WorkerFinanceRequest) {
|
||||
if (request.requestType === 'withdraw') {
|
||||
const channel = formatFinanceChannel(request.accountChannel)
|
||||
const accountName = request.accountName || '-'
|
||||
return `${channel} / ${accountName}`
|
||||
}
|
||||
|
||||
const rechargeSnapshot = getRechargeSnapshot(request)
|
||||
if (
|
||||
rechargeSnapshot.channelName ||
|
||||
rechargeSnapshot.accountName ||
|
||||
rechargeSnapshot.accountNo
|
||||
) {
|
||||
return [
|
||||
rechargeSnapshot.channelName,
|
||||
rechargeSnapshot.accountName,
|
||||
rechargeSnapshot.accountNo ? maskFinanceAccount(rechargeSnapshot.accountNo) : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
return '线下充值,等待管理员核对'
|
||||
}
|
||||
|
||||
function getRechargeSnapshot(request: WorkerFinanceRequest) {
|
||||
const payload = asRecord(request.payload)
|
||||
const financeConfig = asRecord(payload.financeConfig)
|
||||
return {
|
||||
channelName: String(financeConfig.channelName || '').trim(),
|
||||
accountName: String(financeConfig.accountName || '').trim(),
|
||||
accountNo: String(financeConfig.accountNo || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function formatFinanceChannel(channel: string) {
|
||||
if (channel === 'alipay') return '支付宝'
|
||||
if (channel === 'wechat') return '微信收款'
|
||||
if (channel === 'bank') return '银行卡'
|
||||
if (channel === 'manual') return '线下转账'
|
||||
return channel || '-'
|
||||
}
|
||||
|
||||
function maskFinanceAccount(value: string) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) return '-'
|
||||
if (text.length <= 8) return text
|
||||
return `${text.slice(0, 4)} **** ${text.slice(-4)}`
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Descriptions,
|
||||
Empty,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
@@ -113,6 +114,7 @@ export default function WorkerProfilePage() {
|
||||
|
||||
const worker = profileQuery.data?.data.worker
|
||||
const summary = profileQuery.data?.data.summary
|
||||
const financeConfig = profileQuery.data?.data.financeConfig
|
||||
const canUseFinanceActions = worker?.status === 'active'
|
||||
|
||||
const ledgersQuery = useQuery({
|
||||
@@ -179,6 +181,30 @@ export default function WorkerProfilePage() {
|
||||
setActiveTab('requests')
|
||||
}
|
||||
|
||||
function openRechargeModal() {
|
||||
if (!canUseFinanceActions) {
|
||||
message.warning('当前账号尚未通过审核,暂时不能发起充值申请')
|
||||
return
|
||||
}
|
||||
if (financeConfig?.recharge.enabled === false) {
|
||||
message.warning('当前充值入口暂未开放,请联系管理员')
|
||||
return
|
||||
}
|
||||
setRechargeOpen(true)
|
||||
}
|
||||
|
||||
function openWithdrawModal() {
|
||||
if (!canUseFinanceActions) {
|
||||
message.warning('当前账号尚未通过审核,暂时不能发起提现申请')
|
||||
return
|
||||
}
|
||||
if (financeConfig?.withdraw.enabled === false) {
|
||||
message.warning('当前提现入口暂未开放,请联系管理员')
|
||||
return
|
||||
}
|
||||
setWithdrawOpen(true)
|
||||
}
|
||||
|
||||
async function submitRecharge(values: RechargeFormValues) {
|
||||
setSubmittingRecharge(true)
|
||||
try {
|
||||
@@ -327,7 +353,7 @@ export default function WorkerProfilePage() {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Text type="secondary">线下充值,等待管理员入账</Typography.Text>
|
||||
renderRechargeRequestSummary(row)
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -452,7 +478,7 @@ export default function WorkerProfilePage() {
|
||||
type="primary"
|
||||
icon={<WalletOutlined />}
|
||||
disabled={!canUseFinanceActions}
|
||||
onClick={() => setRechargeOpen(true)}
|
||||
onClick={openRechargeModal}
|
||||
>
|
||||
充值申请
|
||||
</Button>
|
||||
@@ -460,7 +486,7 @@ export default function WorkerProfilePage() {
|
||||
type="primary"
|
||||
icon={<BankOutlined />}
|
||||
disabled={!canUseFinanceActions}
|
||||
onClick={() => setWithdrawOpen(true)}
|
||||
onClick={openWithdrawModal}
|
||||
>
|
||||
提现申请
|
||||
</Button>
|
||||
@@ -549,6 +575,7 @@ export default function WorkerProfilePage() {
|
||||
options={[
|
||||
{ value: '', label: '全部流水' },
|
||||
{ value: 'manual_credit', label: '人工充值' },
|
||||
{ value: 'withdraw_paid', label: '提现打款' },
|
||||
{ value: 'deposit_freeze', label: '冻结押金' },
|
||||
{ value: 'deposit_release', label: '释放押金' },
|
||||
{ value: 'deposit_deduction', label: '扣除押金' },
|
||||
@@ -697,6 +724,51 @@ export default function WorkerProfilePage() {
|
||||
initialValues={{ amount: undefined, note: '' }}
|
||||
onFinish={submitRecharge}
|
||||
>
|
||||
<Alert
|
||||
showIcon
|
||||
type={hasRechargeConfig(financeConfig) ? 'info' : 'warning'}
|
||||
message={
|
||||
hasRechargeConfig(financeConfig)
|
||||
? '请先按下方收款信息完成转账,再提交充值申请,管理员核对后会手动入账。'
|
||||
: '管理员暂未配置完整收款信息,如无法转账请先联系管理员确认。'
|
||||
}
|
||||
/>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size={12}
|
||||
className="full-width"
|
||||
style={{ marginTop: 16, marginBottom: 16 }}
|
||||
>
|
||||
{financeConfig?.recharge.qrCodeImage ? (
|
||||
<Image
|
||||
width={180}
|
||||
src={
|
||||
financeConfig.recharge.qrCodeImage.mediumUrl ||
|
||||
financeConfig.recharge.qrCodeImage.url
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="收款通道">
|
||||
{financeConfig?.recharge.channelName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="收款人">
|
||||
{financeConfig?.recharge.accountName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="收款账号">
|
||||
{financeConfig?.recharge.accountNo ? (
|
||||
<Typography.Text copyable>
|
||||
{financeConfig.recharge.accountNo}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="充值说明">
|
||||
{financeConfig?.recharge.instructions || '请转账后填写备注说明,方便管理员核对。'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Space>
|
||||
<Form.Item
|
||||
label="充值金额"
|
||||
name="amount"
|
||||
@@ -730,6 +802,14 @@ export default function WorkerProfilePage() {
|
||||
initialValues={{ accountChannel: 'alipay' }}
|
||||
onFinish={submitWithdraw}
|
||||
>
|
||||
{financeConfig?.withdraw.instructions ? (
|
||||
<Alert
|
||||
showIcon
|
||||
type="info"
|
||||
message={financeConfig.withdraw.instructions}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
) : null}
|
||||
<Form.Item label="可提现余额">
|
||||
<Typography.Text strong>
|
||||
{formatMoney(resolveAvailableForWithdraw(worker, summary))}
|
||||
@@ -962,6 +1042,7 @@ function resolveWorkerStatusColor(status: string) {
|
||||
function formatLedgerType(ledgerType: string) {
|
||||
const labels: Record<string, string> = {
|
||||
manual_credit: '人工充值',
|
||||
withdraw_paid: '提现打款',
|
||||
deposit_freeze: '冻结押金',
|
||||
deposit_release: '释放押金',
|
||||
deposit_deduction: '扣除押金',
|
||||
@@ -972,6 +1053,7 @@ function formatLedgerType(ledgerType: string) {
|
||||
|
||||
function resolveLedgerTagColor(ledgerType: string) {
|
||||
if (ledgerType === 'manual_credit') return 'blue'
|
||||
if (ledgerType === 'withdraw_paid') return 'purple'
|
||||
if (ledgerType === 'deposit_freeze') return 'gold'
|
||||
if (ledgerType === 'deposit_release') return 'green'
|
||||
if (ledgerType === 'deposit_deduction') return 'red'
|
||||
@@ -995,6 +1077,46 @@ function formatRequestStatus(status: string) {
|
||||
return labels[status] || status || '-'
|
||||
}
|
||||
|
||||
function hasRechargeConfig(
|
||||
financeConfig: {
|
||||
recharge?: { accountName?: string; accountNo?: string; qrCodeImage?: unknown }
|
||||
} | undefined,
|
||||
) {
|
||||
return Boolean(
|
||||
financeConfig?.recharge?.accountName ||
|
||||
financeConfig?.recharge?.accountNo ||
|
||||
financeConfig?.recharge?.qrCodeImage,
|
||||
)
|
||||
}
|
||||
|
||||
function renderRechargeRequestSummary(row: WorkerFinanceRequest) {
|
||||
const snapshot = getRechargeSnapshot(row)
|
||||
if (!snapshot.channelName && !snapshot.accountName && !snapshot.accountNo) {
|
||||
return <Typography.Text type="secondary">线下充值,等待管理员入账</Typography.Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text>
|
||||
{[snapshot.channelName, snapshot.accountName].filter(Boolean).join(' / ') || '线下充值'}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{snapshot.accountNo ? maskAccountNo(snapshot.accountNo) : '等待管理员核对'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getRechargeSnapshot(row: WorkerFinanceRequest) {
|
||||
const payload = asRecord(row.payload)
|
||||
const financeConfig = asRecord(payload.financeConfig)
|
||||
return {
|
||||
channelName: String(financeConfig.channelName || '').trim(),
|
||||
accountName: String(financeConfig.accountName || '').trim(),
|
||||
accountNo: String(financeConfig.accountNo || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRequestStatusColor(status: string) {
|
||||
if (status === 'approved') return 'green'
|
||||
if (status === 'pending') return 'gold'
|
||||
@@ -1016,3 +1138,9 @@ function maskAccountNo(value: string) {
|
||||
if (text.length <= 8) return text
|
||||
return `${text.slice(0, 4)} **** ${text.slice(-4)}`
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
WorkCategory,
|
||||
WorkOrder,
|
||||
WorkProductRule,
|
||||
WorkerFinanceConfig,
|
||||
WorkerFinanceRequest,
|
||||
WorkerLevel,
|
||||
WorkerListResponse,
|
||||
WorkerUser,
|
||||
@@ -110,6 +112,44 @@ export function creditAdminWorkerWallet(
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminWorkerFinanceConfig() {
|
||||
return apiGet<WorkerFinanceConfig>('/api/v1/admin/worker-platform/finance-config')
|
||||
}
|
||||
|
||||
export function saveAdminWorkerFinanceConfig(payload: {
|
||||
recharge?: {
|
||||
enabled?: boolean
|
||||
channelName?: string
|
||||
accountName?: string
|
||||
accountNo?: string
|
||||
qrCodeImage?: unknown
|
||||
instructions?: string
|
||||
}
|
||||
withdraw?: {
|
||||
enabled?: boolean
|
||||
instructions?: string
|
||||
}
|
||||
}) {
|
||||
return apiPost<WorkerFinanceConfig>('/api/v1/admin/worker-platform/finance-config', payload)
|
||||
}
|
||||
|
||||
export function fetchAdminWorkerFinanceRequests(params?: Record<string, unknown>) {
|
||||
return apiGet<WorkerListResponse<WorkerFinanceRequest>>(
|
||||
'/api/v1/admin/worker-platform/finance-requests',
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
export function reviewAdminWorkerFinanceRequest(
|
||||
requestId: number,
|
||||
payload: { status: 'approved' | 'rejected' | 'cancelled'; reviewedNote?: string; note?: string },
|
||||
) {
|
||||
return apiPost<{ request: WorkerFinanceRequest }>(
|
||||
`/api/v1/admin/worker-platform/finance-requests/${requestId}/review`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminWorkOrders(params?: Record<string, unknown>) {
|
||||
return apiGet<WorkerListResponse<WorkOrder>>(
|
||||
'/api/v1/admin/worker-platform/orders',
|
||||
@@ -157,6 +197,12 @@ export function publishAdminWorkOrder(workOrderId: number) {
|
||||
)
|
||||
}
|
||||
|
||||
export function unpublishAdminWorkOrder(workOrderId: number) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/unpublish`,
|
||||
)
|
||||
}
|
||||
|
||||
export function markAdminWorkOrderProblem(workOrderId: number, note: string) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/problem`,
|
||||
|
||||
@@ -59,12 +59,34 @@ export type WorkerFinanceRequest = {
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
reviewedAt: string | null
|
||||
worker: null | {
|
||||
workerId: number
|
||||
username: string
|
||||
displayName: string
|
||||
phone: string
|
||||
}
|
||||
}
|
||||
|
||||
export type WorkerFinanceConfig = {
|
||||
recharge: {
|
||||
enabled: boolean
|
||||
channelName: string
|
||||
accountName: string
|
||||
accountNo: string
|
||||
qrCodeImage: UploadedFile | null
|
||||
instructions: string
|
||||
}
|
||||
withdraw: {
|
||||
enabled: boolean
|
||||
instructions: string
|
||||
}
|
||||
}
|
||||
|
||||
export type WorkerProfileResponse = {
|
||||
worker: WorkerUser
|
||||
permissions: Record<string, unknown>
|
||||
summary: WorkerProfileSummary
|
||||
financeConfig: WorkerFinanceConfig
|
||||
}
|
||||
|
||||
export type WorkerLevel = {
|
||||
|
||||
Reference in New Issue
Block a user