import React, { useState, useEffect, useCallback } from 'react'; import { Table, Button, Card, Space, Tag, Modal, Form, Input, InputNumber, Select, DatePicker, message, Descriptions, Image, Divider, Tabs } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UndoOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import { useAuthStore } from '../store/authStore'; import FileUpload from '../components/FileUpload'; import useFormDraft from '../hooks/useFormDraft'; import { useLanguageStore } from '../store/languageStore'; const { Option } = Select; const { TextArea } = Input; // 收款单位类型 const PAYEE_TYPES = [ { value: 'subcontractor', label: '分包商' }, { value: 'supplier', label: '供应商' }, { value: 'customer', label: '客户' }, { value: 'other', label: '其他' } ]; // 支出类型 const EXPENSE_TYPES = [ { value: 'company', label: '公司支出' }, { value: 'project', label: '项目支出' } ]; // 项目支出分类 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 PaymentInfo { account_name: string; bank_account: string; bank_name: string; qr_code?: string; is_primary: boolean; } interface PayeeEntity { id: string; name: string; payment_infos?: PaymentInfo[]; } const PaymentRequestsPage: React.FC = () => { const { t, currentLanguage } = useLanguageStore(); const { user } = useAuthStore(); const [requests, setRequests] = useState([]); const [completedRequests, setCompletedRequests] = useState([]); const [activeTab, setActiveTab] = useState('active'); const [loading, setLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [detailModalVisible, setDetailModalVisible] = useState(false); const [editingId, setEditingId] = useState(null); const [selectedRecord, setSelectedRecord] = useState(null); const [form] = Form.useForm(); const [exchangeRates, setExchangeRates] = useState>({}); // 表单草稿保护 const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({ form, storageKey: 'payment_request_create', }); const handleFormChange = useCallback(() => { saveDraft(); }, [saveDraft]); // 数据列表 const [subcontractors, setSubcontractors] = useState([]); const [suppliers, setSuppliers] = useState([]); const [customers, setCustomers] = useState([]); const [projects, setProjects] = useState([]); useEffect(() => { fetchRequests(); fetchSubcontractors(); fetchSuppliers(); fetchCustomers(); fetchProjects(); fetchExchangeRates(); }, []); const fetchRequests = async () => { setLoading(true); try { const res = await fetch('/api/payment-requests'); const data = await res.json(); if (data.success) { // 解析JSON字符串字段 const parsedRequests = data.data.map((request: any) => ({ ...request, detail_items: typeof request.detail_items === 'string' ? JSON.parse(request.detail_items) : request.detail_items || [], attachments: typeof request.attachments === 'string' ? JSON.parse(request.attachments) : request.attachments || [] })); // 分离活跃的和已完结的付款申请 const active = parsedRequests.filter((item: any) => ['pending', 'approved', 'rejected', 'withdrawn'].includes(item.status)); const completed = parsedRequests.filter((item: any) => ['executed', 'paid'].includes(item.status)); setRequests(active); setCompletedRequests(completed); } } catch (error) { console.error('获取付款申请列表失败:', error); message.error(t('paymentRequest.getListFailed')); } finally { setLoading(false); } }; const fetchSubcontractors = async () => { try { const res = await fetch('/api/subcontractors'); const data = await res.json(); if (data.success) { setSubcontractors(data.data || []); } } catch (error) { console.error('获取分包商列表失败:', error); } }; const fetchSuppliers = async () => { try { const res = await fetch('/api/suppliers'); const data = await res.json(); if (data.success) { setSuppliers(data.data || []); } } catch (error) { console.error('获取供应商列表失败:', error); } }; const fetchCustomers = async () => { try { const res = await fetch('/api/customers'); const data = await res.json(); if (data.success) { setCustomers(data.data || []); } } catch (error) { console.error('获取客户列表失败:', error); } }; const fetchProjects = async () => { try { const res = await fetch('/api/projects'); const data = await res.json(); if (data.success) { setProjects(data.data || []); } } catch (error) { console.error('获取项目列表失败:', error); } }; const fetchExchangeRates = async () => { try { const res = await fetch('/api/exchange-rates/latest'); const data = await res.json(); if (data.success) { const rates: Record = {}; Object.keys(data.data).forEach(key => { rates[key] = parseFloat(data.data[key]) || 1; }); setExchangeRates(rates); } } catch (error) { console.error('获取汇率失败:', error); } }; // 获取主要收款信息 const getPrimaryPaymentInfo = (paymentInfos?: PaymentInfo[]): PaymentInfo | null => { if (!paymentInfos || paymentInfos.length === 0) return null; return paymentInfos.find(p => p.is_primary) || paymentInfos[0]; }; // 根据收款单位类型和ID获取收款信息 const getPayeePaymentInfo = (payeeType: string, payeeId: string): PaymentInfo | null => { let entity: PayeeEntity | undefined; switch (payeeType) { case 'subcontractor': entity = subcontractors.find(s => s.id === payeeId); break; case 'supplier': entity = suppliers.find(s => s.id === payeeId); break; case 'customer': entity = customers.find(c => c.id === payeeId); break; default: return null; } return entity ? getPrimaryPaymentInfo(entity.payment_infos) : null; }; const handleCreate = () => { setEditingId(null); form.resetFields(); form.setFieldsValue({ application_date: dayjs(), currency: 'CNY', applicant: user?.name || user?.username || t('common.currentUser'), attachments: [], payee_type: 'other', expense_type: 'company' }); setWatchedAmount(null); setWatchedCurrency('CNY'); setModalVisible(true); setTimeout(() => { if (hasDraft()) { Modal.confirm({ title: t('common.draftFound'), content: t('common.draftRestore'), okText: t('common.restoreDraft'), cancelText: t('common.reFill'), onOk: () => { restoreDraft(); }, onCancel: () => { clearDraft(); form.resetFields(); form.setFieldsValue({ application_date: dayjs(), currency: 'CNY', applicant: user?.name || user?.username || t('common.currentUser'), attachments: [], payee_type: 'other', expense_type: 'company' }); }, }); } }, 0); }; const handleEdit = (record: any) => { setEditingId(record.id); form.setFieldsValue({ ...record, application_date: record.application_date ? dayjs(record.application_date) : (record.payment_date ? dayjs(record.payment_date) : null), attachments: record.attachments || [] }); setWatchedAmount(record.amount || null); setWatchedCurrency(record.currency || 'CNY'); setModalVisible(true); }; const handleView = (record: any) => { setSelectedRecord(record); setDetailModalVisible(true); }; const handleDelete = async (id: number) => { Modal.confirm({ title: t('common.deleteConfirm'), content: t('paymentRequest.deleteConfirmMsg') || t('common.confirmDeleteMsg'), onOk: async () => { try { await fetch('/api/payment-requests/' + id, { method: 'DELETE' }); message.success(t('paymentRequest.deleteSuccess')); fetchRequests(); } catch (error) { message.error(t('paymentRequest.deleteFailed')); } } }); }; const handleWithdraw = async (id: number) => { Modal.confirm({ title: t('paymentRequest.withdrawConfirm'), content: t('paymentRequest.withdrawConfirmMsg'), onOk: async () => { try { await fetch('/api/payment-requests/' + id + '/withdraw', { method: 'POST' }); message.success(t('paymentRequest.withdrawSuccess')); fetchRequests(); } catch (error) { message.error(t('paymentRequest.withdrawFailed')); } } }); }; const handleSubmit = async () => { try { const values = await form.validateFields(); // 处理收款单位 let payee = ''; let payee_id = null; if (values.payee_type === 'subcontractor') { const sub = subcontractors.find(s => s.id === values.payee_select); payee = sub?.name || ''; payee_id = values.payee_select; } else if (values.payee_type === 'supplier') { const sup = suppliers.find(s => s.id === values.payee_select); payee = sup?.name || ''; payee_id = values.payee_select; } else if (values.payee_type === 'customer') { const cust = customers.find(c => c.id === values.payee_select); payee = cust?.name || ''; payee_id = values.payee_select; } else { payee = values.payee_input || ''; } const data = { ...values, payee, payee_id, application_date: values.application_date?.format('YYYY-MM-DD'), payment_date: values.application_date?.format('YYYY-MM-DD'), // 兼容旧字段 applicant: user?.name || user?.username }; // 删除临时字段 delete data.payee_select; delete data.payee_input; const url = editingId ? '/api/payment-requests/' + editingId : '/api/payment-requests'; const method = editingId ? 'PUT' : 'POST'; const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await res.json(); if (result.success) { message.success(editingId ? t('common.updateSuccess') : t('common.createSuccess')); clearDraft(); setModalVisible(false); fetchRequests(); } else { message.error(result.error || t('common.operationFailed')); } } catch (error) { message.error(t('common.operationFailed')); } }; const convertToCNY = (amount: number, curr: string): number => { if (curr === "CNY") return amount; // 先尝试 XXX_CNY 格式 const rateKey = curr + "_CNY"; if (exchangeRates[rateKey]) { return amount * exchangeRates[rateKey]; } // 尝试 CNY_XXX 格式的倒数 const reverseKey = "CNY_" + curr; if (exchangeRates[reverseKey]) { return amount / exchangeRates[reverseKey]; } // 尝试通过 USD 中转: XXX -> USD -> CNY const xxxUsdKey = curr + "_USD"; const usdCnyKey = "USD_CNY"; const cnyUsdKey = "CNY_USD"; if (exchangeRates[xxxUsdKey]) { const usdAmount = amount * exchangeRates[xxxUsdKey]; if (exchangeRates[usdCnyKey]) return usdAmount * exchangeRates[usdCnyKey]; if (exchangeRates[cnyUsdKey]) return usdAmount / exchangeRates[cnyUsdKey]; } // 通过 LAK 中转 const xxxLakKey = curr + "_LAK"; const cnyLakKey = "CNY_LAK"; if (exchangeRates[xxxLakKey] && exchangeRates[cnyLakKey]) { const lakAmount = amount * exchangeRates[xxxLakKey]; return lakAmount / exchangeRates[cnyLakKey]; } return amount; }; const getStatusTag = (status: string) => { const statusMap: Record = { pending: { color: 'processing', text: t('paymentRequest.pendingApproval') }, approved: { color: 'success', text: t('paymentRequest.approved') }, rejected: { color: 'error', text: t('paymentRequest.rejected') }, withdrawn: { color: 'default', text: t('paymentRequest.withdrawn') }, paid: { color: 'blue', text: t('paymentRequest.paid') }, }; const config = statusMap[status] || { color: 'default', text: status }; return {config.text}; }; 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 getExpenseCategoryLabel = (type: string, category: string) => { if (type === 'project') { return PROJECT_EXPENSE_CATEGORIES.find(c => c.value === category)?.label || category; } else { return COMPANY_EXPENSE_CATEGORIES.find(c => c.value === category)?.label || category; } }; // 获取收款单位类型标签 const getPayeeTypeLabel = (type: string) => { return PAYEE_TYPES.find(t => t.value === type)?.label || type; }; const columns = [ { title: t('paymentRequest.subject'), dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => handleView(r)}>{v} }, { title: t('paymentRequest.applicant'), dataIndex: 'applicant', key: 'applicant', width: 80 }, { title: t('paymentRequest.payee'), dataIndex: 'payee', key: 'payee', width: 150, ellipsis: true }, { title: t('paymentRequest.amount'), 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: t('paymentRequest.applicationDate'), dataIndex: 'application_date', key: 'application_date', width: 100, render: (v: string, r: any) => v || r.payment_date }, { title: t('paymentRequest.status'), dataIndex: 'status', key: 'status', width: 100, render: (status: string) => getStatusTag(status) }, { title: t('paymentRequest.code'), dataIndex: 'request_code', key: 'request_code', width: 120 }, { title: t('paymentRequest.action'), key: 'action', width: 250, render: (_: any, record: any) => ( {record.status === 'pending' && ( <> )} {(record.status === 'rejected' || record.status === 'withdrawn') && ( )} ) } ]; // 监听表单值变化 - 使用 useState + onValuesChange 替代 Form.useWatch 以确保稳定触发 const [watchedAmount, setWatchedAmount] = useState(null); const [watchedCurrency, setWatchedCurrency] = useState('CNY'); const payeeType = Form.useWatch('payee_type', form); const payeeSelect = Form.useWatch('payee_select', form); const expenseType = Form.useWatch('expense_type', form); const amountCNY = React.useMemo(() => { return watchedAmount && watchedCurrency ? convertToCNY(watchedAmount, watchedCurrency) : 0; }, [watchedAmount, watchedCurrency, exchangeRates]); // 当选择收款单位时,自动填充收款信息 useEffect(() => { if (payeeType && payeeSelect && ['subcontractor', 'supplier', 'customer'].includes(payeeType)) { const paymentInfo = getPayeePaymentInfo(payeeType, payeeSelect); if (paymentInfo) { form.setFieldsValue({ account_name: paymentInfo.account_name, bank_account: paymentInfo.bank_account, bank_name: paymentInfo.bank_name, qr_code: paymentInfo.qr_code }); } } }, [payeeType, payeeSelect]); return (

{t('paymentRequest.title')}

{t('paymentRequest.description')}

} onClick={handleCreate}>{t('paymentRequest.newRequest')}}>
{ if (form.isFieldsTouched()) { Modal.confirm({ title: t('common.closeConfirm'), content: t('common.closeConfirmMsg'), okText: t('common.close'), cancelText: t('common.continueEdit'), onOk: () => { saveDraft(); form.resetFields(); setModalVisible(false); }, }); } else { form.resetFields(); setModalVisible(false); } }} maskClosable={false} width={900} destroyOnClose>
{/* 第2项:支出类型和支出分类 */} {/* 项目支出 - 选择项目 */} {expenseType === 'project' && ( )} {/* 支出分类 */} {/* 申请日期(原付款日期,不显示) */} {/* 收款单位 - 二级选择 */} {payeeType === 'subcontractor' && ( )} {payeeType === 'supplier' && ( )} {payeeType === 'customer' && ( )} {payeeType === 'other' && ( )} {/* 收款户名 - 新增字段 */} {/* 收款码 - 新增字段 */} {/* 金额 - 直接输入 */} setWatchedAmount(value)} /> {watchedAmount && watchedCurrency !== 'CNY' && amountCNY > 0 && (
{t('paymentRequest.equivalentCNY')}{amountCNY.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
)}