import { ReloadOutlined } from '@ant-design/icons' import { useQuery, useQueryClient } from '@tanstack/react-query' import { App, Alert, 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 { useSearchParams } from 'react-router' 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 { AdminRangePicker } from '@/components/admin/AdminDatePicker' import dayjs, { ADMIN_DATE_TIME_FORMAT } from '@/lib/dayjs' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' import { getAdminRole } from '@/utils/admin-auth' import { asRecord, formatMoney } from './shared' type FinanceConfigFormValues = { depositUnfreezeDays?: number rewardUnfreezeEnabled?: boolean consignmentPublishDepositYuan?: number consignmentWhitelistEnabled?: boolean 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' type FinanceRequestDateType = 'created' | 'reviewed' export default function FinancePanel() { const { message } = App.useApp() const canEditConfig = getAdminRole() !== 'finance' const queryClient = useQueryClient() const [searchParams, setSearchParams] = useSearchParams() const [configForm] = Form.useForm() const [reviewForm] = Form.useForm<{ reviewedNote?: string }>() const [requestStatus, setRequestStatus] = useState('') const [targetRequestId, setTargetRequestId] = useState() const [requestType, setRequestType] = useState('') const [keywordInput, setKeywordInput] = useState('') const [keyword, setKeyword] = useState('') const [requestDateType, setRequestDateType] = useState('created') const [requestDateRange, setRequestDateRange] = useState<[string, string] | null>(null) const [accountChannel, setAccountChannel] = 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(), }) useEffect(() => { const requestId = Number(searchParams.get('financeRequestId') || 0) if (!Number.isInteger(requestId) || requestId <= 0) return setTargetRequestId(requestId) setRequestStatus('') setRequestType('') setKeyword('') setKeywordInput('') setPage(1) setSearchParams( (current) => { current.delete('financeRequestId') return current }, { replace: true }, ) }, [searchParams, setSearchParams]) const financeRequestsQuery = useQuery({ queryKey: [ 'admin-worker-platform-finance-requests', requestStatus, requestType, targetRequestId, keyword, page, pageSize, requestDateType, requestDateRange ? requestDateRange[0] : '', requestDateRange ? requestDateRange[1] : '', accountChannel, ], queryFn: () => fetchAdminWorkerFinanceRequests({ status: requestStatus, requestType, requestId: targetRequestId, keyword, page, pageSize, accountChannel, ...(requestDateRange ? requestDateType === 'created' ? { createdFrom: requestDateRange[0], createdTo: requestDateRange[1] } : { reviewedFrom: requestDateRange[0], reviewedTo: requestDateRange[1] } : {}), }), }) 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), rewardUnfreezeEnabled: values.rewardUnfreezeEnabled !== false, consignmentPublishDepositFen: Math.round( Number(values.consignmentPublishDepositYuan ?? 20) * 100, ), consignmentWhitelistEnabled: values.consignmentWhitelistEnabled !== false, 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: 250, render: (_, row) => (
{row.worker?.displayName || row.worker?.username || '-'} {row.worker?.username || '-'} {row.worker ? ( {row.worker.workerType === 'internal' ? '内部打手' : '外部打手'} {row.worker.levelName || row.worker.levelKey || '未分级'} 提现 {row.worker.withdrawRequestCount} 次,累计 {formatMoney(row.worker.totalWithdrawAmount)} ) : null}
), }, { 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)}${resolveFinanceReviewer(row) ? `(${resolveFinanceReviewer(row)})` : ''}` : '待审核'}
), }, { title: '操作', width: 240, render: (_, row) => row.status === 'pending' ? ( ) : ( 已处理 ), }, ] return (
{targetRequestId ? ( setTargetRequestId(undefined)}> 取消定位 } /> ) : null} } > {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) }} /> { setRequestDateType(nextType) setPage(1) }} /> { const [start, end] = dates || [] setRequestDateRange( start && end ? [ // 起止各占满整分钟:开始 xx:00、结束 xx:59 start.second(0).format(ADMIN_DATE_TIME_FORMAT), end.second(59).format(ADMIN_DATE_TIME_FORMAT), ] : null, ) 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.requestType === 'withdraw' ? '提现信息' : '充值信息'} {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.accountNo ? ( {reviewState.request.accountNo} ) : ( '-' )} ) : null} {reviewState.request.requestType === 'recharge' ? ( {getRechargePaidAt(reviewState.request) ? formatAdminDateTime(getRechargePaidAt(reviewState.request)) : '-'} ) : null} {reviewState.request.note || '-'}
{reviewState.request.requestType === 'withdraw' ? `${formatFinanceChannel(reviewState.request.accountChannel)}收款二维码` : '付款凭证'}
) : null}
) } function mapFinanceConfigToFormValues(config?: WorkerFinanceConfig): FinanceConfigFormValues { return { depositUnfreezeDays: Number(config?.depositUnfreezeDays ?? 3), rewardUnfreezeEnabled: config?.rewardUnfreezeEnabled !== false, consignmentPublishDepositYuan: Number(config?.consignmentPublishDepositFen ?? 2000) / 100, consignmentWhitelistEnabled: config?.consignmentWhitelistEnabled !== false, 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) { return action === 'approved' ? '通过' : '拒绝' } 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 getWithdrawQrCode(request: WorkerFinanceRequest): UploadedFile[] { if (request.accountChannel === 'alipay') return getAlipayWithdrawQrCode(request) return getWechatWithdrawQrCode(request) } function getAlipayWithdrawQrCode(request: WorkerFinanceRequest): UploadedFile[] { if (request.requestType !== 'withdraw' || request.accountChannel !== 'alipay') return [] const payload = asRecord(request.payload) const file = asRecord(payload.alipayQrCodeImage) 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)}` } type FinanceSummaryChannel = { channel: string; count: number; amount: number } /** 当前筛选条件下的渠道合计条:支付宝 / 微信 / 其他分列展示笔数与金额。 */ function FinanceSummaryBar({ channels, totalCount, totalAmount, }: { channels?: FinanceSummaryChannel[] totalCount?: number totalAmount?: number }) { const items = channels || [] const alipay = items.find((item) => item.channel === 'alipay') const wechat = items.find((item) => item.channel === 'wechat') const others = items.filter((item) => item.channel !== 'alipay' && item.channel !== 'wechat') const otherCount = others.reduce((sum, item) => sum + item.count, 0) const otherAmount = others.reduce((sum, item) => sum + item.amount, 0) return ( 支付宝 {alipay ? alipay.count : 0} 笔 /{' '} {formatMoney(alipay ? alipay.amount : 0)} 微信 {wechat ? wechat.count : 0} 笔 /{' '} {formatMoney(wechat ? wechat.amount : 0)} 其他 {otherCount} 笔 / {formatMoney(otherAmount)} 合计 {totalCount || 0} 笔 /{' '} {formatMoney(totalAmount || 0)} 按当前筛选条件(含时间区间)统计 ) } function resolveFinanceReviewer(request: WorkerFinanceRequest): string { const review = asRecord(asRecord(request.payload).review) return String(review.by || '').trim() }