import { ReloadOutlined } from '@ant-design/icons' import { useQuery, useQueryClient } from '@tanstack/react-query' import { App, Button, Card, Descriptions, Form, Input, InputNumber, Modal, Select, Space, Switch, Table, Tag, Typography, } from 'antd' import type { TableColumnsType } from 'antd' import { useEffect, useState } from 'react' import ImageUpload from '@/components/files/ImageUpload' import ImagePreviewList from '@/components/files/ImagePreviewList' import { fetchAdminWorkerFinanceConfig, fetchAdminWorkerFinanceRequests, reviewAdminWorkerFinanceRequest, saveAdminWorkerFinanceConfig, } from '@/services/admin' import type { UploadedFile, WorkerFinanceConfig, WorkerFinanceRequest, } from '@/types/worker-platform' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' import { asRecord, formatMoney } from './shared' type FinanceConfigFormValues = { depositUnfreezeDays?: number recharge?: { enabled?: boolean channelName?: string accountName?: string accountNo?: string qrCodeImageList?: UploadedFile[] instructions?: string } adminContact?: { wechatQrCodeImageList?: UploadedFile[] } withdraw?: { enabled?: boolean instructions?: string } } type FinanceReviewAction = 'approved' | 'rejected' | 'cancelled' export default function FinancePanel() { const { message } = App.useApp() const queryClient = useQueryClient() const [configForm] = Form.useForm() 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 [configExpanded, setConfigExpanded] = 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({ depositUnfreezeDays: Number(values.depositUnfreezeDays ?? 3), 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(), }, adminContact: { wechatQrCodeImage: values.adminContact?.wechatQrCodeImageList?.[0] || null, }, 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 = [ { title: '打手', minWidth: 180, render: (_, row) => (
{row.worker?.displayName || row.worker?.username || '-'} {row.worker?.username || '-'}
), }, { title: '类型', width: 120, render: (_, row) => ( {formatFinanceRequestType(row.requestType)} ), }, { title: '金额', width: 120, render: (_, row) => formatMoney(row.amount), }, { title: '收款信息', minWidth: 240, render: (_, row) => renderFinanceRequestAccount(row), }, { title: '凭证/收款码', minWidth: 140, render: (_, row) => ( ), }, { title: '备注', minWidth: 260, render: (_, row) => (
{row.note || '-'} {row.reviewedNote ? ( 审核备注:{row.reviewedNote} ) : null}
), }, { title: '状态', width: 120, render: (_, row) => ( {formatFinanceRequestStatus(row.status)} ), }, { title: '时间', width: 180, render: (_, row) => (
{formatAdminDateTime(row.createdAt)} {row.reviewedAt ? `审核:${formatAdminDateTime(row.reviewedAt)}` : '待审核'}
), }, { title: '操作', width: 240, render: (_, row) => row.status === 'pending' ? ( ) : ( 已处理 ), }, ] return (
} > {configExpanded && financeConfigQuery.error ? ( {financeConfigQuery.error instanceof Error ? financeConfigQuery.error.message : '读取资金配置失败'} ) : null} {configExpanded ? (
解冻期间如订单出现问题,可在"接单工单"中对已验收工单扣减待解冻押金(全额或部分)。
) : null}
setKeywordInput(event.target.value)} onSearch={(value) => { setKeyword(String(value || '').trim()) setPage(1) }} /> { setRequestStatus(nextStatus) setPage(1) }} /> } > 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 }} /> { setReviewState(null) reviewForm.resetFields() }} onOk={() => reviewForm.submit()} > {reviewState ? ( {reviewState.request.worker?.displayName || reviewState.request.worker?.username || '-'} {formatFinanceRequestType(reviewState.request.requestType)} {formatMoney(reviewState.request.amount)} {renderFinanceRequestAccountText(reviewState.request)} {reviewState.request.requestType === 'withdraw' && reviewState.request.accountChannel === 'alipay' ? ( {reviewState.request.accountNo ? ( {reviewState.request.accountNo} ) : ( '-' )} ) : null} {reviewState.request.requestType === 'recharge' ? ( <> {getRechargePaidAt(reviewState.request) ? formatAdminDateTime(getRechargePaidAt(reviewState.request)) : '-'} ) : null} {reviewState.request.requestType === 'withdraw' && reviewState.request.accountChannel === 'wechat' ? ( ) : null} {reviewState.request.note || '-'}
) : null}
) } function mapFinanceConfigToFormValues(config?: WorkerFinanceConfig): FinanceConfigFormValues { return { depositUnfreezeDays: Number(config?.depositUnfreezeDays ?? 3), 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 || '', }, adminContact: { wechatQrCodeImageList: config?.adminContact.wechatQrCodeImage ? [config.adminContact.wechatQrCodeImage] : [], }, 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 = { 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 (
{renderFinanceRequestAccountText(request)} {request.requestType === 'withdraw' && request.accountNo ? ( {request.accountNo} ) : null}
) } 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 getRechargeProofs(request: WorkerFinanceRequest): UploadedFile[] { if (request.requestType !== 'recharge') return [] const payload = asRecord(request.payload) const rawFiles = Array.isArray(payload.proofFiles) ? payload.proofFiles : [] return rawFiles .map((item) => { const file = asRecord(item) const url = String(file.url || '').trim() if (!url) return null return { url, mediumUrl: String(file.mediumUrl || '').trim(), thumbnailUrl: String(file.thumbnailUrl || '').trim(), } as UploadedFile }) .filter((item): item is UploadedFile => Boolean(item)) } function getWechatWithdrawQrCode(request: WorkerFinanceRequest): UploadedFile[] { if (request.requestType !== 'withdraw' || request.accountChannel !== 'wechat') return [] const payload = asRecord(request.payload) const file = asRecord(payload.wechatQrCodeImage) const url = String(file.url || '').trim() if (!url) return [] return [ { url, mediumUrl: String(file.mediumUrl || '').trim(), thumbnailUrl: String(file.thumbnailUrl || '').trim(), objectKey: String(file.objectKey || '').trim(), filename: String(file.filename || '').trim(), contentType: String(file.contentType || '').trim(), size: Number(file.size || 0), }, ] } function getRechargePaidAt(request: WorkerFinanceRequest): string { const payload = asRecord(request.payload) return String(payload.paidAt || '').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)}` }