Files
yunhaifinance/frontend/src/pages/PaymentRequestsPage.tsx
T
a273825743 706dcc24eb 备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
2026-06-13 12:44:48 +08:00

711 lines
29 KiB
TypeScript

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<any[]>([]);
const [completedRequests, setCompletedRequests] = useState<any[]>([]);
const [activeTab, setActiveTab] = useState('active');
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [selectedRecord, setSelectedRecord] = useState<any>(null);
const [form] = Form.useForm();
const [exchangeRates, setExchangeRates] = useState<Record<string, number>>({});
// 表单草稿保护
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
form,
storageKey: 'payment_request_create',
});
const handleFormChange = useCallback(() => {
saveDraft();
}, [saveDraft]);
// 数据列表
const [subcontractors, setSubcontractors] = useState<PayeeEntity[]>([]);
const [suppliers, setSuppliers] = useState<PayeeEntity[]>([]);
const [customers, setCustomers] = useState<PayeeEntity[]>([]);
const [projects, setProjects] = useState<any[]>([]);
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<string, number> = {};
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<string, { color: string; text: string }> = {
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 <Tag color={config.color}>{config.text}</Tag>;
};
const formatAmount = (amount: number, currency: string = 'CNY') => {
const symbols: Record<string, string> = { 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) => <a onClick={() => handleView(r)}>{v}</a> },
{ 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) => (
<>
<div>{formatAmount(v, r.currency)}</div>
{r.currency !== 'CNY' && r.amount_cny && <div style={{ color: '#999', fontSize: 12 }}> ¥{r.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}</div>}
</>
) },
{ 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) => (
<Space wrap>
<Button size="small" icon={<EyeOutlined />} onClick={() => handleView(record)}>{t('common.detail')}</Button>
{record.status === 'pending' && (
<>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('paymentRequest.edit')}</Button>
<Button size="small" icon={<UndoOutlined />} onClick={() => handleWithdraw(record.id)}>{t('paymentRequest.withdraw')}</Button>
</>
)}
{(record.status === 'rejected' || record.status === 'withdrawn') && (
<Button size="small" type="primary" icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('paymentRequest.reEdit')}</Button>
)}
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)}>{t('paymentRequest.delete')}</Button>
</Space>
)
}
];
// 监听表单值变化 - 使用 useState + onValuesChange 替代 Form.useWatch 以确保稳定触发
const [watchedAmount, setWatchedAmount] = useState<number | null>(null);
const [watchedCurrency, setWatchedCurrency] = useState<string>('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 (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 24 }}>
<h2 style={{ marginBottom: 8 }}>{t('paymentRequest.title')}</h2>
<p style={{ color: '#888', marginBottom: 0 }}>{t('paymentRequest.description')}</p>
</div>
<Card extra={<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>{t('paymentRequest.newRequest')}</Button>}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<Tabs.TabPane tab={t('paymentRequest.activeApplications')} key="active">
<Table dataSource={requests} columns={columns} rowKey="id" loading={loading} pagination={{ pageSize: 20 }} size="middle" scroll={{ x: 1200 }} />
</Tabs.TabPane>
<Tabs.TabPane tab={t('paymentRequest.completed')} key="completed">
<Table dataSource={completedRequests} columns={columns} rowKey="id" loading={loading} pagination={{ pageSize: 20 }} size="middle" scroll={{ x: 1200 }} />
</Tabs.TabPane>
</Tabs>
</Card>
<Modal title={editingId ? t('paymentRequest.editPayment') : t('paymentRequest.newPayment')} open={modalVisible} onOk={handleSubmit} onCancel={() => {
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>
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
<Form.Item name="applicant" label={t('paymentRequest.applicant')}>
<Input disabled style={{ color: 'rgba(0,0,0,0.85)', backgroundColor: '#f5f5f5' }} />
</Form.Item>
{/* 第2项:支出类型和支出分类 */}
<Form.Item name="expense_type" label={t('paymentRequest.expenseType')} rules={[{ required: true }]}>
<Select placeholder={t('paymentRequest.selectExpenseType')}>
{EXPENSE_TYPES.map(type => (
<Option key={type.value} value={type.value}>{type.label}</Option>
))}
</Select>
</Form.Item>
{/* 项目支出 - 选择项目 */}
{expenseType === 'project' && (
<Form.Item name="project_id" label={t('paymentRequest.relatedProject')} rules={[{ required: true }]}>
<Select placeholder={t('paymentRequest.selectProject')} showSearch optionFilterProp="children">
{projects.map(proj => (
<Option key={proj.id} value={proj.id}>{proj.name}</Option>
))}
</Select>
</Form.Item>
)}
{/* 支出分类 */}
<Form.Item name="expense_category" label={t('paymentRequest.expenseCategory')} rules={[{ required: true }]}>
<Select placeholder={t('paymentRequest.selectCategory')}>
{(expenseType === 'project' ? PROJECT_EXPENSE_CATEGORIES : COMPANY_EXPENSE_CATEGORIES).map(cat => (
<Option key={cat.value} value={cat.value}>{cat.label}</Option>
))}
</Select>
</Form.Item>
{/* 申请日期(原付款日期,不显示) */}
<Form.Item name="application_date" label={t('paymentRequest.applicationDate')} rules={[{ required: true }]} style={{ display: 'none' }}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
{/* 收款单位 - 二级选择 */}
<Form.Item name="payee_type" label={t('paymentRequest.payeeType')} rules={[{ required: true }]}>
<Select placeholder={t('paymentRequest.selectPayeeType')}>
{PAYEE_TYPES.map(type => (
<Option key={type.value} value={type.value}>{type.label}</Option>
))}
</Select>
</Form.Item>
{payeeType === 'subcontractor' && (
<Form.Item name="payee_select" label={t('paymentRequest.selectSubcontractor')} rules={[{ required: true }]}>
<Select placeholder={t('paymentRequest.selectSubcontractor')} showSearch optionFilterProp="children">
{subcontractors.map(sub => (
<Option key={sub.id} value={sub.id}>{sub.name}</Option>
))}
</Select>
</Form.Item>
)}
{payeeType === 'supplier' && (
<Form.Item name="payee_select" label={t('paymentRequest.selectSupplier')} rules={[{ required: true }]}>
<Select placeholder={t('paymentRequest.selectSupplier')} showSearch optionFilterProp="children">
{suppliers.map(sup => (
<Option key={sup.id} value={sup.id}>{sup.name}</Option>
))}
</Select>
</Form.Item>
)}
{payeeType === 'customer' && (
<Form.Item name="payee_select" label={t('paymentRequest.selectCustomer')} rules={[{ required: true }]}>
<Select placeholder={t('paymentRequest.selectCustomer')} showSearch optionFilterProp="children">
{customers.map(cust => (
<Option key={cust.id} value={cust.id}>{cust.name}</Option>
))}
</Select>
</Form.Item>
)}
{payeeType === 'other' && (
<Form.Item name="payee_input" label={t('paymentRequest.payee')} rules={[{ required: true }]}>
<Input placeholder={t('paymentRequest.payeeNamePlaceholder')} />
</Form.Item>
)}
{/* 收款户名 - 新增字段 */}
<Form.Item name="account_name" label={t('paymentRequest.accountName')}>
<Input placeholder={t('paymentRequest.accountNamePlaceholder')} readOnly={['subcontractor', 'supplier', 'customer'].includes(payeeType)} />
</Form.Item>
<Form.Item name="bank_account" label={t('paymentRequest.bankAccount')}>
<Input placeholder={t('paymentRequest.bankAccountPlaceholder')} readOnly={['subcontractor', 'supplier', 'customer'].includes(payeeType)} />
</Form.Item>
<Form.Item name="bank_name" label={t('paymentRequest.bankName')}>
<Input placeholder={t('paymentRequest.bankNamePlaceholder')} readOnly={['subcontractor', 'supplier', 'customer'].includes(payeeType)} />
</Form.Item>
{/* 收款码 - 新增字段 */}
<Form.Item name="qr_code" label={t('paymentRequest.qrCode')}>
<FileUpload maxCount={1} accept="image/*" />
</Form.Item>
<Form.Item name="currency" label={t('common.currency')} rules={[{ required: true }]}>
<Select style={{ width: 200 }} onChange={(value: string) => setWatchedCurrency(value)}>
<Option value="CNY">{t('paymentRequest.currencyCNY')}</Option>
<Option value="USD">{t('paymentRequest.currencyUSD')}</Option>
<Option value="LAK">{t('paymentRequest.currencyLAK')}</Option>
<Option value="THB">{t('paymentRequest.currencyTHB')}</Option>
</Select>
</Form.Item>
{/* 金额 - 直接输入 */}
<Form.Item name="amount" label={t('paymentRequest.paymentAmount')} rules={[{ required: true }]}>
<InputNumber min={0} precision={2} style={{ width: '100%' }} placeholder={t('paymentRequest.paymentAmountPlaceholder')} onChange={(value) => setWatchedAmount(value)} />
</Form.Item>
{watchedAmount && watchedCurrency !== 'CNY' && amountCNY > 0 && (
<div style={{ marginTop: -20, marginBottom: 24, color: '#888', fontSize: 13 }}>
{t('paymentRequest.equivalentCNY')}{amountCNY.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
)}
<Form.Item name="reason" label={t('paymentRequest.paymentReason')} rules={[{ required: true }]}>
<TextArea rows={2} placeholder={t('paymentRequest.paymentReasonPlaceholder')} />
</Form.Item>
<Divider>{t('paymentRequest.proofAttachment')}</Divider>
<Form.Item name="attachments" label={t('paymentRequest.uploadProof')}>
<FileUpload maxCount={9} accept="image/*" />
</Form.Item>
</Form>
</Modal>
<Modal title={t('paymentRequest.detailTitle')} open={detailModalVisible} onCancel={() => setDetailModalVisible(false)} footer={null} width={900}>
{selectedRecord && (
<>
<Descriptions bordered column={2} size="small">
<Descriptions.Item label={t('paymentRequest.applicationCode')}>{selectedRecord.request_code}</Descriptions.Item>
<Descriptions.Item label={t('common.status')}>{getStatusTag(selectedRecord.status)}</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.applicant')}>{selectedRecord.applicant}</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.applicationDate')}>{selectedRecord.application_date || selectedRecord.payment_date}</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.payeeType')}>{getPayeeTypeLabel(selectedRecord.payee_type)}</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.payee')}>{selectedRecord.payee}</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.accountName')}>{selectedRecord.account_name || '-'}</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.bankAccount')}>{selectedRecord.bank_account || '-'}</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.bankName')}>{selectedRecord.bank_name || '-'}</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.expenseType')}>
{selectedRecord.expense_type === 'company' ? t('paymentRequest.companyExpense') : t('paymentRequest.projectExpense')}
</Descriptions.Item>
{selectedRecord.expense_type === 'project' && (
<Descriptions.Item label={t('paymentRequest.relatedProject')}>
{projects.find(p => p.id === selectedRecord.project_id)?.name || '-'}
</Descriptions.Item>
)}
<Descriptions.Item label={t('paymentRequest.expenseCategory')}>
{getExpenseCategoryLabel(selectedRecord.expense_type, selectedRecord.expense_category)}
</Descriptions.Item>
<Descriptions.Item label={t('common.amount')}>
{formatAmount(selectedRecord.amount, selectedRecord.currency)}
{selectedRecord.currency !== 'CNY' && selectedRecord.amount_cny && (
<span style={{ color: '#999', marginLeft: 8 }}> ¥{selectedRecord.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}</span>
)}
</Descriptions.Item>
<Descriptions.Item label={t('paymentRequest.paymentReason')} span={2}>{selectedRecord.reason}</Descriptions.Item>
</Descriptions>
{selectedRecord.qr_code && (
<>
<Divider>{t('paymentRequest.qrCode')}</Divider>
<Image src={selectedRecord.qr_code} width={200} style={{ borderRadius: 4 }} />
</>
)}
{selectedRecord.attachments && selectedRecord.attachments.length > 0 && (
<>
<Divider>{t('paymentRequest.proofAttachment')}</Divider>
<Image.PreviewGroup>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{selectedRecord.attachments.map((url: string, index: number) => (
<Image key={index} src={url} width={100} height={100} style={{ objectFit: 'cover', borderRadius: 4 }} />
))}
</div>
</Image.PreviewGroup>
</>
)}
</>
)}
</Modal>
</div>
);
};
export default PaymentRequestsPage;