备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -59,6 +60,7 @@ interface PayeeEntity {
|
||||
}
|
||||
|
||||
const PaymentRequestsPage: React.FC = () => {
|
||||
const { t, currentLanguage } = useLanguageStore();
|
||||
const { user } = useAuthStore();
|
||||
const [requests, setRequests] = useState<any[]>([]);
|
||||
const [completedRequests, setCompletedRequests] = useState<any[]>([]);
|
||||
@@ -116,7 +118,7 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取付款申请列表失败:', error);
|
||||
message.error('获取付款申请列表失败');
|
||||
message.error(t('paymentRequest.getListFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -217,19 +219,21 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
form.setFieldsValue({
|
||||
application_date: dayjs(),
|
||||
currency: 'CNY',
|
||||
applicant: user?.name || user?.username || '当前用户',
|
||||
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: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的付款申请,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
title: t('common.draftFound'),
|
||||
content: t('common.draftRestore'),
|
||||
okText: t('common.restoreDraft'),
|
||||
cancelText: t('common.reFill'),
|
||||
onOk: () => {
|
||||
restoreDraft();
|
||||
},
|
||||
@@ -239,7 +243,7 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
form.setFieldsValue({
|
||||
application_date: dayjs(),
|
||||
currency: 'CNY',
|
||||
applicant: user?.name || user?.username || '当前用户',
|
||||
applicant: user?.name || user?.username || t('common.currentUser'),
|
||||
attachments: [],
|
||||
payee_type: 'other',
|
||||
expense_type: 'company'
|
||||
@@ -257,6 +261,8 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -267,15 +273,15 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这条付款申请吗?',
|
||||
title: t('common.deleteConfirm'),
|
||||
content: t('paymentRequest.deleteConfirmMsg') || t('common.confirmDeleteMsg'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await fetch('/api/payment-requests/' + id, { method: 'DELETE' });
|
||||
message.success('删除成功');
|
||||
message.success(t('paymentRequest.deleteSuccess'));
|
||||
fetchRequests();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
message.error(t('paymentRequest.deleteFailed'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -283,15 +289,15 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
|
||||
const handleWithdraw = async (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认撤回',
|
||||
content: '撤回后可重新编辑提交,确认撤回吗?',
|
||||
title: t('paymentRequest.withdrawConfirm'),
|
||||
content: t('paymentRequest.withdrawConfirmMsg'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await fetch('/api/payment-requests/' + id + '/withdraw', { method: 'POST' });
|
||||
message.success('已撤回,可重新编辑');
|
||||
message.success(t('paymentRequest.withdrawSuccess'));
|
||||
fetchRequests();
|
||||
} catch (error) {
|
||||
message.error('撤回失败');
|
||||
message.error(t('paymentRequest.withdrawFailed'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -342,32 +348,56 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
message.success(editingId ? '更新成功' : '创建成功');
|
||||
message.success(editingId ? t('common.updateSuccess') : t('common.createSuccess'));
|
||||
clearDraft();
|
||||
setModalVisible(false);
|
||||
fetchRequests();
|
||||
} else {
|
||||
message.error(result.error || '操作失败');
|
||||
message.error(result.error || t('common.operationFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
message.error(t('common.operationFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const convertToCNY = (amount: number, curr: string): number => {
|
||||
if (curr === "CNY") return amount;
|
||||
// 先尝试 XXX_CNY 格式
|
||||
const rateKey = curr + "_CNY";
|
||||
const rate = exchangeRates[rateKey] || 1;
|
||||
return amount * rate;
|
||||
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: '待审批' },
|
||||
approved: { color: 'success', text: '已批准' },
|
||||
rejected: { color: 'error', text: '已退回' },
|
||||
withdrawn: { color: 'default', text: '已撤回' },
|
||||
paid: { color: 'blue', text: '已付款' },
|
||||
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>;
|
||||
@@ -393,48 +423,48 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '事由', dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => <a onClick={() => handleView(r)}>{v}</a> },
|
||||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant', width: 80 },
|
||||
{ title: '收款单位', dataIndex: 'payee', key: 'payee', width: 150, ellipsis: true },
|
||||
{ title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: (v: number, r: any) => (
|
||||
{ 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: '申请日期', dataIndex: 'application_date', key: 'application_date', width: 100, render: (v: string, r: any) => v || r.payment_date },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (status: string) => getStatusTag(status) },
|
||||
{ title: '编号', dataIndex: 'request_code', key: 'request_code', width: 120 },
|
||||
{ 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: '操作', key: 'action', width: 250,
|
||||
title: t('paymentRequest.action'), key: 'action', width: 250,
|
||||
render: (_: any, record: any) => (
|
||||
<Space wrap>
|
||||
<Button size="small" icon={<EyeOutlined />} onClick={() => handleView(record)}>详情</Button>
|
||||
<Button size="small" icon={<EyeOutlined />} onClick={() => handleView(record)}>{t('common.detail')}</Button>
|
||||
{record.status === 'pending' && (
|
||||
<>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Button size="small" icon={<UndoOutlined />} onClick={() => handleWithdraw(record.id)}>撤回</Button>
|
||||
<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)}>编辑重提</Button>
|
||||
<Button size="small" type="primary" icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('paymentRequest.reEdit')}</Button>
|
||||
)}
|
||||
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)}>删除</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 amount = Form.useWatch('amount', form);
|
||||
const currency = Form.useWatch('currency', form);
|
||||
|
||||
|
||||
const amountCNY = React.useMemo(() => {
|
||||
return amount && currency ? convertToCNY(amount, currency) : 0;
|
||||
}, [amount, currency, exchangeRates]);
|
||||
return watchedAmount && watchedCurrency ? convertToCNY(watchedAmount, watchedCurrency) : 0;
|
||||
}, [watchedAmount, watchedCurrency, exchangeRates]);
|
||||
|
||||
// 当选择收款单位时,自动填充收款信息
|
||||
useEffect(() => {
|
||||
@@ -454,45 +484,47 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h2 style={{ marginBottom: 8 }}>付款申请</h2>
|
||||
<p style={{ color: '#888', marginBottom: 0 }}>管理对外付款申请</p>
|
||||
<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}>新建付款申请</Button>}>
|
||||
<Card extra={<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>{t('paymentRequest.newRequest')}</Button>}>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.TabPane tab="活跃申请" key="active">
|
||||
<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="已完结" key="completed">
|
||||
<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 ? '编辑付款申请' : '新建付款申请'} open={modalVisible} onOk={handleSubmit} onCancel={() => {
|
||||
<Modal title={editingId ? t('paymentRequest.editPayment') : t('paymentRequest.newPayment')} open={modalVisible} onOk={handleSubmit} onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
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}>
|
||||
}} maskClosable={false} width={900} destroyOnClose>
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
<Form.Item name="applicant" label="申请人">
|
||||
<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="支出类型" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择支出类型">
|
||||
<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>
|
||||
))}
|
||||
@@ -501,8 +533,8 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
|
||||
{/* 项目支出 - 选择项目 */}
|
||||
{expenseType === 'project' && (
|
||||
<Form.Item name="project_id" label="关联项目" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择项目" showSearch optionFilterProp="children">
|
||||
<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>
|
||||
))}
|
||||
@@ -511,8 +543,8 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* 支出分类 */}
|
||||
<Form.Item name="expense_category" label="支出分类" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择支出分类">
|
||||
<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>
|
||||
))}
|
||||
@@ -520,13 +552,13 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
</Form.Item>
|
||||
|
||||
{/* 申请日期(原付款日期,不显示) */}
|
||||
<Form.Item name="application_date" label="申请日期" rules={[{ required: true }]} style={{ display: 'none' }}>
|
||||
<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="收款单位类型" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择收款单位类型">
|
||||
<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>
|
||||
))}
|
||||
@@ -534,8 +566,8 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
</Form.Item>
|
||||
|
||||
{payeeType === 'subcontractor' && (
|
||||
<Form.Item name="payee_select" label="选择分包商" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择分包商" showSearch optionFilterProp="children">
|
||||
<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>
|
||||
))}
|
||||
@@ -544,8 +576,8 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{payeeType === 'supplier' && (
|
||||
<Form.Item name="payee_select" label="选择供应商" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择供应商" showSearch optionFilterProp="children">
|
||||
<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>
|
||||
))}
|
||||
@@ -554,8 +586,8 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{payeeType === 'customer' && (
|
||||
<Form.Item name="payee_select" label="选择客户" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择客户" showSearch optionFilterProp="children">
|
||||
<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>
|
||||
))}
|
||||
@@ -564,102 +596,102 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{payeeType === 'other' && (
|
||||
<Form.Item name="payee_input" label="收款单位" rules={[{ required: true }]}>
|
||||
<Input placeholder="手动输入收款单位名称" />
|
||||
<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="收款户名">
|
||||
<Input placeholder="收款户名(选择分包商/供应商/客户时自动填充)" readOnly={['subcontractor', 'supplier', 'customer'].includes(payeeType)} />
|
||||
<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="银行账号">
|
||||
<Input placeholder="收款银行账号(选择分包商/供应商/客户时自动填充)" readOnly={['subcontractor', 'supplier', 'customer'].includes(payeeType)} />
|
||||
<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="开户银行">
|
||||
<Input placeholder="开户银行名称(选择分包商/供应商/客户时自动填充)" readOnly={['subcontractor', 'supplier', 'customer'].includes(payeeType)} />
|
||||
<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="收款码">
|
||||
<Form.Item name="qr_code" label={t('paymentRequest.qrCode')}>
|
||||
<FileUpload maxCount={1} accept="image/*" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="currency" label="币种" rules={[{ required: true }]}>
|
||||
<Select style={{ width: 200 }}>
|
||||
<Option value="CNY">人民币 (CNY)</Option>
|
||||
<Option value="USD">美元 (USD)</Option>
|
||||
<Option value="LAK">老挝基普 (LAK)</Option>
|
||||
<Option value="THB">泰铢 (THB)</Option>
|
||||
<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="付款金额" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} placeholder="输入付款金额" />
|
||||
{amount && currency !== 'CNY' && amountCNY > 0 && (
|
||||
<div style={{ marginTop: 8, color: '#888', fontSize: 13 }}>
|
||||
等价人民币:¥ {amountCNY.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<Form.Item name="reason" label="付款事由" rules={[{ required: true }]}>
|
||||
<TextArea rows={2} placeholder="付款原因" />
|
||||
</Form.Item>
|
||||
|
||||
<Divider>凭证附件</Divider>
|
||||
<Form.Item name="attachments" label="上传凭证附件">
|
||||
<Divider>{t('paymentRequest.proofAttachment')}</Divider>
|
||||
<Form.Item name="attachments" label={t('paymentRequest.uploadProof')}>
|
||||
<FileUpload maxCount={9} accept="image/*" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title="付款申请详情" open={detailModalVisible} onCancel={() => setDetailModalVisible(false)} footer={null} width={900}>
|
||||
<Modal title={t('paymentRequest.detailTitle')} open={detailModalVisible} onCancel={() => setDetailModalVisible(false)} footer={null} width={900}>
|
||||
{selectedRecord && (
|
||||
<>
|
||||
<Descriptions bordered column={2} size="small">
|
||||
<Descriptions.Item label="申请编号">{selectedRecord.request_code}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{getStatusTag(selectedRecord.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label="申请人">{selectedRecord.applicant}</Descriptions.Item>
|
||||
<Descriptions.Item label="申请日期">{selectedRecord.application_date || selectedRecord.payment_date}</Descriptions.Item>
|
||||
<Descriptions.Item label="收款单位类型">{getPayeeTypeLabel(selectedRecord.payee_type)}</Descriptions.Item>
|
||||
<Descriptions.Item label="收款单位">{selectedRecord.payee}</Descriptions.Item>
|
||||
<Descriptions.Item label="收款户名">{selectedRecord.account_name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="银行账号">{selectedRecord.bank_account || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="开户银行">{selectedRecord.bank_name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="支出类型">
|
||||
{selectedRecord.expense_type === 'company' ? '公司支出' : '项目支出'}
|
||||
<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="关联项目">
|
||||
<Descriptions.Item label={t('paymentRequest.relatedProject')}>
|
||||
{projects.find(p => p.id === selectedRecord.project_id)?.name || '-'}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="支出分类">
|
||||
<Descriptions.Item label={t('paymentRequest.expenseCategory')}>
|
||||
{getExpenseCategoryLabel(selectedRecord.expense_type, selectedRecord.expense_category)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="金额">
|
||||
<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="付款事由" span={2}>{selectedRecord.reason}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('paymentRequest.paymentReason')} span={2}>{selectedRecord.reason}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{selectedRecord.qr_code && (
|
||||
<>
|
||||
<Divider>收款码</Divider>
|
||||
<Divider>{t('paymentRequest.qrCode')}</Divider>
|
||||
<Image src={selectedRecord.qr_code} width={200} style={{ borderRadius: 4 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedRecord.attachments && selectedRecord.attachments.length > 0 && (
|
||||
<>
|
||||
<Divider>凭证附件</Divider>
|
||||
<Divider>{t('paymentRequest.proofAttachment')}</Divider>
|
||||
<Image.PreviewGroup>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{selectedRecord.attachments.map((url: string, index: number) => (
|
||||
@@ -676,4 +708,4 @@ const PaymentRequestsPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentRequestsPage;
|
||||
export default PaymentRequestsPage;
|
||||
Reference in New Issue
Block a user