import React, { useState, useEffect } from 'react'; import { Card, Table, Tag, Button, Space, Modal, Form, Input, Select, DatePicker, message, Tabs, Badge, Descriptions, Divider, List, Upload } from 'antd'; import { CheckOutlined, CloseOutlined, EyeOutlined, DollarOutlined, EditOutlined, UndoOutlined, ClockCircleOutlined, FileImageOutlined, UploadOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; const { TextArea } = Input; // 项目支出分类 const PROJECT_EXPENSE_CATEGORIES = [ { value: 'material_purchase', label: '材料采购' }, { value: 'equipment_purchase', label: '设备采购' }, { value: 'pole_crossarm', label: '电杆横担支出' }, { value: 'freight', label: '运费支出' }, { value: 'construction', label: '施工费支出' }, { value: 'other', label: '其他支出' } ]; // 公司支出分类 const COMPANY_EXPENSE_CATEGORIES = [ { value: 'office_operations', label: '通用运营(房租/耗材)' }, { value: 'transportation', label: '交通通勤' }, { value: 'marketing', label: '业扩营销' }, { value: 'power_system', label: '电力系统关系' }, { value: 'employee_welfare', label: '员工福利' }, { value: 'logistics', label: '快递物流' }, { value: 'other', label: '其他支出' } ]; // 执行记录类型 interface ExecutionRecord { id: string; applyCode: string; applyType: string; applicant: string; amount: number; currency: string; action: 'execute' | 'reject'; operator: string; operatorRole: string; timestamp: string; executeMethod?: string; voucherNo?: string; rejectReason?: string; remark?: string; } const ExecutionManagement: React.FC = () => { const [loading, setLoading] = useState(false); const [detailModalVisible, setDetailModalVisible] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false); const [historyModalVisible, setHistoryModalVisible] = useState(false); const [selectedRecord, setSelectedRecord] = useState(null); const [executionType, setExecutionType] = useState<'execute' | 'reject'>('execute'); const [form] = Form.useForm(); const [editForm] = Form.useForm(); const [voucherFiles, setVoucherFiles] = useState([]); // 执行记录 const [executionHistory, setExecutionHistory] = useState([]); // 待执行数据 const [pendingData, setPendingData] = useState([]); // 已执行数据 const [executedData, setExecutedData] = useState([]); // 从后端获取待执行数据 useEffect(() => { const fetchPendingData = async () => { setLoading(true); try { const response = await fetch('http://localhost:3005/api/executions/pending'); if (response.ok) { const data = await response.json(); if (data.success && Array.isArray(data.data)) { setPendingData(data.data.map((item: any, index: number) => ({ ...item, key: item.id || index, rawData: item }))); } else { message.error('获取待执行数据失败:数据格式错误'); } } else { message.error('获取待执行数据失败:' + response.statusText); } } catch (error) { console.error('获取待执行数据错误:', error); message.error('网络错误,获取待执行数据失败'); } finally { setLoading(false); } }; fetchPendingData(); }, []); // 从后端获取已执行数据 useEffect(() => { const fetchExecutedData = async () => { setLoading(true); try { const response = await fetch('http://localhost:3005/api/executions/executed'); if (response.ok) { const data = await response.json(); if (data.success && Array.isArray(data.data)) { setExecutedData(data.data.map((item: any, index: number) => ({ ...item, key: item.id || index, rawData: item }))); } else { message.error('获取已执行数据失败:数据格式错误'); } } else { message.error('获取已执行数据失败:' + response.statusText); } } catch (error) { console.error('获取已执行数据错误:', error); message.error('网络错误,获取已执行数据失败'); } finally { setLoading(false); } }; fetchExecutedData(); }, []); const formatAmount = (amount: number, currency: string = 'CNY') => { const symbols: Record = { CNY: '¥', USD: '$', LAK: '₭', THB: '฿' }; return (symbols[currency] || '¥') + (amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; const formatNumberWithSeparator = (value: number | undefined, currency: string): string => { if (value === undefined || value === null) return ''; const symbols: Record = { CNY: '¥', USD: '$', LAK: '₭', THB: '฿' }; return (symbols[currency] || '¥') + value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; const parseFormattedNumber = (value: string): number => { const cleaned = value.replace(/[¥$₭฿,]/g, ''); return parseFloat(cleaned) || 0; }; const getTypeTag = (type: string) => { const colors: Record = { '预支申请': 'blue', '报销申请': 'green', '付款申请': 'orange', '核销申请': 'purple' }; return {type}; }; const getStatusTag = (status: string) => { const statusMap: Record = { pending: { color: 'processing', text: '待执行' }, executed: { color: 'success', text: '已执行' }, rejected: { color: 'error', text: '已退回' }, }; const config = statusMap[status] || { color: 'default', text: status }; return {config.text}; }; // 添加执行记录 const addExecutionRecord = (record: any, action: 'execute' | 'reject', operator: string, operatorRole: string, data?: any) => { const newRecord: ExecutionRecord = { id: Date.now().toString(), applyCode: record.code, applyType: record.type, applicant: record.applicant, amount: record.amount, currency: record.currency, action, operator, operatorRole, timestamp: dayjs().format('YYYY-MM-DD HH:mm'), executeMethod: data?.executeMethod, voucherNo: data?.voucherNo, rejectReason: data?.rejectReason, remark: data?.remark }; setExecutionHistory([newRecord, ...executionHistory]); }; // 查看详情 const handleViewDetail = (record: any) => { setSelectedRecord(record); setExecutionType('execute'); form.resetFields(); form.setFieldsValue({ execute_date: dayjs(), execute_method: 'bank' }); setVoucherFiles([]); setDetailModalVisible(true); }; // 处理执行 const handleExecute = async () => { try { const values = await form.validateFields(); // 检查是否上传了付款凭证 if (!voucherFiles || voucherFiles.length === 0) { message.error('请上传付款凭证'); return; } setLoading(true); // 获取已上传文件的URL列表 const voucherFileUrls = voucherFiles .map(f => f.url || f.response?.data?.url || f.response?.url) .filter(url => url); // 过滤掉空值 console.log('上传的凭证文件:', voucherFiles); console.log('凭证文件URL列表:', voucherFileUrls); // 调用执行API const executeResponse = await fetch('http://localhost:3005/api/executions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apply_id: selectedRecord.id, apply_type: selectedRecord.type === '预支申请' ? 'advance' : selectedRecord.type === '报销申请' ? 'reimbursement' : selectedRecord.type === '付款申请' ? 'payment' : 'verification', action: 'execute', execute_method: values.execute_method, voucher_files: voucherFileUrls, remark: values.remark }) }); if (executeResponse.ok) { addExecutionRecord(selectedRecord, 'execute', '系统管理员', '管理员', { executeMethod: values.execute_method === 'bank' ? '银行转账' : values.execute_method === 'cash' ? '现金' : '其他', remark: values.remark }); setPendingData(pendingData.filter(item => item.key !== selectedRecord.key)); message.success(`执行成功:${selectedRecord.code}`); setDetailModalVisible(false); } else { message.error('执行操作失败,请重试'); } } catch (error) { console.error('执行操作失败:', error); message.error('网络错误,操作失败'); } finally { setLoading(false); } }; // 处理退回 const handleReject = async () => { try { const values = await form.validateFields(); setLoading(true); // 调用退回API const rejectResponse = await fetch('http://localhost:3005/api/executions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apply_id: selectedRecord.id, apply_type: selectedRecord.type === '预支申请' ? 'advance' : selectedRecord.type === '报销申请' ? 'reimbursement' : selectedRecord.type === '付款申请' ? 'payment' : 'verification', action: 'reject', reject_reason: values.rejectReason }) }); if (rejectResponse.ok) { addExecutionRecord(selectedRecord, 'reject', '系统管理员', '管理员', { rejectReason: values.rejectReason }); setPendingData(pendingData.filter(item => item.key !== selectedRecord.key)); message.success(`已退回:${selectedRecord.code},申请人可编辑后重新提交`); setDetailModalVisible(false); } else { message.error('退回操作失败,请重试'); } } catch (error) { console.error('退回操作失败:', error); message.error('网络错误,操作失败'); } finally { setLoading(false); } }; const handleViewHistory = (record: any) => { setSelectedRecord(record); setHistoryModalVisible(true); }; const handleEdit = (record: any) => { setSelectedRecord(record); editForm.setFieldsValue({ amount: record.amount, reason: record.reason }); setEditModalVisible(true); }; const handleEditSubmit = () => { editForm.validateFields().then(values => { message.success('修改成功,已重新提交审批'); setEditModalVisible(false); }); }; // 渲染附件列表 const renderAttachments = (attachments: any) => { // 处理字符串类型的 attachments(JSON字符串) let attachmentList = attachments; if (typeof attachments === 'string') { try { attachmentList = JSON.parse(attachments); } catch (e) { return null; } } // 确保是数组 if (!Array.isArray(attachmentList) || attachmentList.length === 0) return null; return (
{attachmentList.map((url: string, index: number) => (
{url && url.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? ( {`附件${index window.open(url, '_blank')} /> ) : (
)}
))}
); }; // 渲染明细清单 const renderDetailItems = (detailItems: any) => { // 处理字符串类型的 detailItems(JSON字符串) let itemsList = detailItems; if (typeof detailItems === 'string') { try { itemsList = JSON.parse(detailItems); } catch (e) { return null; } } // 确保是数组 if (!Array.isArray(itemsList) || itemsList.length === 0) return null; return ( (
明细 {index + 1}: {item.description || item.category || '-'} {formatAmount(item.amount, item.currency)}
{item.attachments && (
明细附件: {renderAttachments(item.attachments)}
)}
)} /> ); }; // 执行记录列 const historyColumns = [ { title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 140 }, { title: '操作', dataIndex: 'action', key: 'action', width: 100, render: (v: string) => { const map: Record = { execute: { color: 'green', icon: , text: '执行' }, reject: { color: 'red', icon: , text: '退回' } }; const m = map[v] || { color: 'default', icon: null, text: v }; return {m.text}; }}, { title: '申请编号', dataIndex: 'applyCode', key: 'applyCode', width: 140 }, { title: '类型', dataIndex: 'applyType', key: 'applyType', width: 100, render: (v: string) => getTypeTag(v) }, { title: '申请人', dataIndex: 'applicant', key: 'applicant', width: 80 }, { title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: (v: number, r: ExecutionRecord) => ( <>
{formatAmount(v, r.currency)}
{r.currency !== 'CNY' && r.amount_cny &&
≈ ¥{r.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}
} ) }, { title: '执行方式', dataIndex: 'executeMethod', key: 'executeMethod', width: 100 }, { title: '操作人', dataIndex: 'operator', key: 'operator', width: 100 }, { title: '角色', dataIndex: 'operatorRole', key: 'operatorRole', width: 80 }, { title: '退回原因', dataIndex: 'rejectReason', key: 'rejectReason', ellipsis: true }, ]; const pendingColumns = [ { title: '事由', dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => handleViewDetail(r)}>{v} }, { title: '类型', dataIndex: 'type', key: 'type', width: 100, render: (v: string) => getTypeTag(v) }, { title: '申请人', dataIndex: 'applicant', key: 'applicant', width: 80 }, { title: '金额', dataIndex: 'amount', key: 'amount', width: 120, render: (v: number, r: any) => {formatAmount(v, r.currency)} }, { title: '收款方', dataIndex: 'payee', key: 'payee', ellipsis: true, render: (v: string, r: any) => v || r.applicant }, { title: '审批日期', dataIndex: 'approveDate', key: 'approveDate', width: 100 }, { title: '编号', dataIndex: 'code', key: 'code', width: 140 }, { title: '操作', key: 'action', width: 200, render: (_: any, record: any) => ( ) } ]; const executedColumns = [ { title: '事由', dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => handleViewDetail(r)}>{v || '-'} }, { title: '类型', dataIndex: 'type', key: 'type', width: 100, render: (v: string) => getTypeTag(v) }, { title: '申请人', dataIndex: 'applicant', key: 'applicant', width: 80 }, { title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: (v: number, r: any) => ( <>
{formatAmount(v, r.currency)}
{r.currency !== 'CNY' && r.amount_cny &&
≈ ¥{r.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}
} ) }, { title: '执行日期', dataIndex: 'executeDate', key: 'executeDate', width: 100 }, { title: '执行方式', dataIndex: 'executeMethod', key: 'executeMethod', width: 100 }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (v: string) => getStatusTag(v) }, { title: '编号', dataIndex: 'code', key: 'code', width: 140 }, { title: '操作', key: 'action', width: 100, render: (_: any, record: any) => ( ) } ]; const tabItems = [ { key: 'pending', label: 待执行 , children: }, { key: 'executed', label: '已执行', children:
}, { key: 'history', label: 执行记录 , children:
}, ]; // 获取完整的申请详情 const getFullDetail = () => { if (!selectedRecord || !selectedRecord.rawData) return selectedRecord; return selectedRecord.rawData; }; const fullDetail = getFullDetail(); // 上传配置 const uploadProps = { name: 'file', action: 'http://localhost:3005/api/upload/single', headers: { authorization: 'authorization-text', }, onChange(info: any) { // 更新文件列表状态 setVoucherFiles(info.fileList); if (info.file.status === 'done') { message.success(`${info.file.name} 上传成功`); // 如果上传成功,将返回的URL添加到文件对象中 const updatedFileList = info.fileList.map((file: any) => { if (file.uid === info.file.uid && file.response) { return { ...file, url: file.response.data?.url || file.response.url || file.response }; } return file; }); setVoucherFiles(updatedFileList); } else if (info.file.status === 'error') { message.error(`${info.file.name} 上传失败`); } }, fileList: voucherFiles, }; return (

执行管理

执行已审批通过的付款申请

{/* 详情模态框 */} setDetailModalVisible(false)} width={900} footer={ selectedRecord?.status === 'approved' || selectedRecord?.status === 'pending' ? (
) : ( ) } > {fullDetail && ( <> {/* 基本信息 */} {getTypeTag(selectedRecord.type)} {selectedRecord.code} {selectedRecord.applicant} {selectedRecord.date || fullDetail.advance_date || fullDetail.reimbursement_date || fullDetail.payment_date || fullDetail.verification_date} {formatAmount(selectedRecord.amount, selectedRecord.currency)} {selectedRecord.currency !== 'CNY' && fullDetail.amount_cny > 0 && ( ≈ ¥{fullDetail.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})} )} {getStatusTag(selectedRecord.status)} {selectedRecord.reason} {/* 付款申请特有字段 */} {selectedRecord.type === '付款申请' && ( <> {fullDetail.payee_type === 'subcontractor' ? '分包商' : fullDetail.payee_type === 'supplier' ? '供应商' : fullDetail.payee_type === 'customer' ? '客户' : '其他'} {fullDetail.payee || '-'} {fullDetail.bank_name || '-'} {fullDetail.bank_account || '-'} {fullDetail.expense_type === 'company' ? '公司支出' : '项目支出'} {fullDetail.expense_type === 'project' && fullDetail.project_id && ( 项目ID: {fullDetail.project_id} )} {fullDetail.expense_type === 'project' ? (PROJECT_EXPENSE_CATEGORIES.find(c => c.value === fullDetail.expense_category)?.label || fullDetail.expense_category) : (COMPANY_EXPENSE_CATEGORIES.find(c => c.value === fullDetail.expense_category)?.label || fullDetail.expense_category) } )} {/* 报销申请特有字段 */} {selectedRecord.type === '报销申请' && fullDetail.expense_type && ( <> {fullDetail.expense_type === 'company' ? '公司支出' : '项目支出'} {fullDetail.project_id && ( 项目ID: {fullDetail.project_id} )} )} {/* 核销申请特有字段 */} {selectedRecord.type === '核销申请' && fullDetail.advance_code && ( <> {fullDetail.advance_code} {formatAmount(fullDetail.advance_amount, fullDetail.currency)} )} {/* 明细清单 */} {fullDetail.detail_items && fullDetail.detail_items.length > 0 && ( <> 明细清单 {renderDetailItems(fullDetail.detail_items)} )} {/* 凭证附件 */} {fullDetail.attachments && fullDetail.attachments.length > 0 && ( <> 申请凭证附件 {renderAttachments(fullDetail.attachments)} )} {/* 执行表单 */} {(selectedRecord.status === 'approved' || selectedRecord.status === 'pending') && ( <> 执行信息