备份:PWA配置前的完整版本

包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
a273825743
2026-06-13 12:44:48 +08:00
parent 5fd19822a2
commit 706dcc24eb
83 changed files with 26590 additions and 13379 deletions
@@ -2,6 +2,7 @@ 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';
import { useLanguageStore } from '../../store/languageStore';
const { TextArea } = Input;
@@ -26,6 +27,15 @@ const COMPANY_EXPENSE_CATEGORIES = [
{ value: 'other', label: '其他支出' }
];
// Type name to i18n key mapping
const typeKeyMap: Record<string, string> = {
'预支申请': 'execution.advanceApply',
'报销申请': 'execution.reimburseApply',
'付款申请': 'execution.paymentApply',
'核销申请': 'execution.verificationApply',
'采购申请': 'execution.purchaseApply',
};
// 执行记录类型
interface ExecutionRecord {
id: string;
@@ -45,6 +55,7 @@ interface ExecutionRecord {
}
const ExecutionManagement: React.FC = () => {
const { t, currentLanguage } = useLanguageStore();
const [loading, setLoading] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
@@ -85,14 +96,14 @@ const ExecutionManagement: React.FC = () => {
rawData: item
})));
} else {
message.error('获取待执行数据失败:数据格式错误');
message.error(t('execution.getPendingFailedFormat'));
}
} else {
message.error('获取待执行数据失败:' + response.statusText);
message.error(t('execution.getPendingFailed') + response.statusText);
}
} catch (error) {
console.error('获取待执行数据错误:', error);
message.error('网络错误,获取待执行数据失败');
message.error(t('execution.getPendingNetworkError'));
} finally {
setLoading(false);
}
@@ -135,14 +146,14 @@ const ExecutionManagement: React.FC = () => {
rawData: item
})));
} else {
message.error('获取已执行数据失败:数据格式错误');
message.error(t('execution.getExecutedFailedFormat'));
}
} else {
message.error('获取已执行数据失败:' + response.statusText);
message.error(t('execution.getExecutedFailed') + response.statusText);
}
} catch (error) {
console.error('获取已执行数据错误:', error);
message.error('网络错误,获取已执行数据失败');
message.error(t('execution.getExecutedNetworkError'));
} finally {
setLoading(false);
}
@@ -169,15 +180,16 @@ const ExecutionManagement: React.FC = () => {
const getTypeTag = (type: string) => {
const colors: Record<string, string> = { '预支申请': 'blue', '报销申请': 'green', '付款申请': 'orange', '核销申请': 'purple', '采购申请': 'cyan' };
return <Tag color={colors[type] || 'default'}>{type}</Tag>;
const key = typeKeyMap[type] || type;
return <Tag color={colors[type] || 'default'}>{t(key)}</Tag>;
};
const getStatusTag = (status: string) => {
const statusMap: Record<string, { color: string; text: string }> = {
pending: { color: 'processing', text: '待执行' },
executed: { color: 'success', text: '已执行' },
rejected: { color: 'error', text: '已退回' },
approved: { color: 'success', text: '已批准' },
pending: { color: 'processing', text: t('execution.pendingExecution') },
executed: { color: 'success', text: t('execution.executed') },
rejected: { color: 'error', text: t('execution.rejected') },
approved: { color: 'success', text: t('execution.approved') },
};
const config = statusMap[status] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
@@ -201,7 +213,7 @@ const ExecutionManagement: React.FC = () => {
setIsRejecting(false);
// 获取完整详情
if (record.type === '采购申请') {
if (record.type === t('execution.purchaseApply')) {
try {
const response = await fetch(`/api/purchase-requests/${record.id}`);
const data = await response.json();
@@ -225,7 +237,7 @@ const ExecutionManagement: React.FC = () => {
const handleExecute = async () => {
try {
// 检查是否为核销申请
const isVerification = selectedRecord.type === '核销申请';
const isVerification = selectedRecord.type === t('execution.verificationApply');
// 检查是否为退款类型的核销申请
const isRefundVerification = isVerification && fullDetail.settlement && fullDetail.settlement_amount > 0;
// 检查是否为非结算核销
@@ -271,10 +283,10 @@ const ExecutionManagement: React.FC = () => {
}
};
fetchExecutedData();
message.success(`执行成功:${selectedRecord.code}`);
message.success(t('execution.executeSuccess', { code: selectedRecord.code }));
setDetailModalVisible(false);
} else {
message.error('执行操作失败,请重试');
message.error(t('execution.executeFailed'));
}
} else {
// 其他类型的申请需要验证表单
@@ -282,7 +294,7 @@ const ExecutionManagement: React.FC = () => {
// 检查是否需要上传付款凭证
if (!isRefundVerification && (!voucherFiles || voucherFiles.length === 0)) {
message.error('请上传付款凭证');
message.error(t('execution.proofRequired'));
return;
}
@@ -303,7 +315,7 @@ const ExecutionManagement: React.FC = () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apply_id: selectedRecord.id,
apply_type: selectedRecord.type === '预支申请' ? 'advance' : selectedRecord.type === '报销申请' ? 'reimbursement' : selectedRecord.type === '付款申请' ? 'payment' : selectedRecord.type === '采购申请' ? 'purchase' : 'verification',
apply_type: selectedRecord.type === t('execution.advanceApply') ? 'advance' : selectedRecord.type === t('execution.reimburseApply') ? 'reimbursement' : selectedRecord.type === t('execution.paymentApply') ? 'payment' : selectedRecord.type === t('execution.purchaseApply') ? 'purchase' : 'verification',
action: 'execute',
execute_method: isRefundVerification ? 'refund' : values.execute_method,
voucher_files: voucherFileUrls,
@@ -332,15 +344,15 @@ const ExecutionManagement: React.FC = () => {
}
};
fetchExecutedData();
message.success(`执行成功:${selectedRecord.code}`);
message.success(t('execution.executeSuccess', { code: selectedRecord.code }));
setDetailModalVisible(false);
} else {
message.error('执行操作失败,请重试');
message.error(t('execution.executeFailed'));
}
}
} catch (error) {
console.error('执行操作失败:', error);
message.error('网络错误,操作失败');
message.error(t('common.networkError'));
} finally {
setLoading(false);
}
@@ -361,7 +373,7 @@ const ExecutionManagement: React.FC = () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apply_id: selectedRecord.id,
apply_type: selectedRecord.type === '预支申请' ? 'advance' : selectedRecord.type === '报销申请' ? 'reimbursement' : selectedRecord.type === '付款申请' ? 'payment' : selectedRecord.type === '采购申请' ? 'purchase' : 'verification',
apply_type: selectedRecord.type === t('execution.advanceApply') ? 'advance' : selectedRecord.type === t('execution.reimburseApply') ? 'reimbursement' : selectedRecord.type === t('execution.paymentApply') ? 'payment' : selectedRecord.type === t('execution.purchaseApply') ? 'purchase' : 'verification',
action: 'reject',
reject_reason: values.rejectReason
})
@@ -369,14 +381,14 @@ const ExecutionManagement: React.FC = () => {
if (rejectResponse.ok) {
setPendingData(pendingData.filter(item => item.key !== selectedRecord.key));
message.success(`已退回:${selectedRecord.code},申请人可编辑后重新提交`);
message.success(t('execution.rejectSuccess', { code: selectedRecord.code }));
setDetailModalVisible(false);
} else {
message.error('退回操作失败,请重试');
message.error(t('execution.rejectFailed'));
}
} catch (error) {
console.error('退回操作失败:', error);
message.error('网络错误,操作失败');
message.error(t('common.networkError'));
} finally {
setLoading(false);
}
@@ -392,7 +404,7 @@ const ExecutionManagement: React.FC = () => {
const handleEditSubmit = () => {
editForm.validateFields().then(values => {
message.success('修改成功,已重新提交审批');
message.success(t('execution.editSuccess'));
setEditModalVisible(false);
});
};
@@ -448,22 +460,22 @@ const ExecutionManagement: React.FC = () => {
// 特殊分类映射
const specialCategories: Record<string, string> = {
// Project expense categories
accommodation: '住宿',
food: '餐饮',
fuel: '加油',
materials: '零散材料',
customer_relations: '客户关系',
subcontract_relations: '分包关系',
edl_relations: 'EDL关系',
extra_construction: '额外施工',
accommodation: t('execution.accommodation'),
food: t('execution.catering'),
fuel: t('execution.fuel'),
materials: t('execution.scatteredMaterial'),
customer_relations: t('execution.customerRelation'),
subcontract_relations: t('execution.subcontractorRelation'),
edl_relations: t('execution.EDLRelation'),
extra_construction: t('execution.extraConstruction'),
// Company expense categories
general_operations: '通用运营(房租/耗材)',
transportation: '交通通勤',
business_expansion: '业扩营销',
power_system_relations: '电力系统关系',
employee_benefits: '员工福利',
express_logistics: '快递物流',
other: '其他'
general_operations: t('execution.generalOperation'),
transportation: t('execution.commute'),
business_expansion: t('execution.marketing'),
power_system_relations: t('execution.powerSystem'),
employee_benefits: t('execution.employeeBenefit'),
express_logistics: t('execution.expressLogistics'),
other: t('execution.otherCategory')
};
// 先检查特殊分类
if (specialCategories[category]) return specialCategories[category];
@@ -501,17 +513,17 @@ const ExecutionManagement: React.FC = () => {
<List.Item>
<div style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span><strong> {index + 1}:</strong> {item.description || getCategoryName(item.category) || '-'}</span>
<span><strong>{t('execution.detailLabel', { index: index + 1 })}</strong> {item.description || getCategoryName(item.category) || '-'}</span>
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>{formatAmount(item.amount, item.currency)}</span>
</div>
{item.category && (
<div style={{ marginBottom: 8, fontSize: 13, color: '#666' }}>
<strong></strong>{getCategoryName(item.category)}
<strong>{t('execution.categoryLabel')}</strong>{getCategoryName(item.category)}
</div>
)}
{item.attachments && (
<div style={{ marginTop: 8 }}>
<span style={{ color: '#666', fontSize: 12 }}></span>
<span style={{ color: '#666', fontSize: 12 }}>{t('execution.detailAttachment')}</span>
{renderAttachments(item.attachments)}
</div>
)}
@@ -525,19 +537,19 @@ const ExecutionManagement: React.FC = () => {
const pendingColumns = [
{ title: '事由', dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => <a onClick={() => handleViewDetail(r)}>{v}</a> },
{ 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) => <span style={{ fontWeight: 'bold', color: '#1890ff' }}>{formatAmount(v, r.currency)}</span> },
{ 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: t('execution.subject'), dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => <a onClick={() => handleViewDetail(r)}>{v}</a> },
{ title: t('execution.type'), dataIndex: 'type', key: 'type', width: 100, render: (v: string) => getTypeTag(v) },
{ title: t('execution.applicant'), dataIndex: 'applicant', key: 'applicant', width: 80 },
{ title: t('execution.amount'), dataIndex: 'amount', key: 'amount', width: 120, render: (v: number, r: any) => <span style={{ fontWeight: 'bold', color: '#1890ff' }}>{formatAmount(v, r.currency)}</span> },
{ title: t('execution.payee'), dataIndex: 'payee', key: 'payee', ellipsis: true, render: (v: string, r: any) => v || r.applicant },
{ title: t('execution.approvalDate'), dataIndex: 'approveDate', key: 'approveDate', width: 100 },
{ title: t('execution.code'), dataIndex: 'code', key: 'code', width: 140 },
{
title: '操作', key: 'action', width: 200,
title: t('execution.action'), key: 'action', width: 200,
render: (_: any, record: any) => (
<Space wrap>
<Button size="small" type="primary" icon={<DollarOutlined />} onClick={() => handleViewDetail(record)}></Button>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}></Button>
<Button size="small" type="primary" icon={<DollarOutlined />} onClick={() => handleViewDetail(record)}>{t('execution.execute')}</Button>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('execution.edit')}</Button>
</Space>
)
}
@@ -583,26 +595,26 @@ const ExecutionManagement: React.FC = () => {
};
const executedColumns = [
{ title: '事由', dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => <a onClick={() => handleViewDetail(r)}>{v || '-'}</a> },
{ 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) => (
{ title: t('execution.subject'), dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => <a onClick={() => handleViewDetail(r)}>{v || '-'}</a> },
{ title: t('execution.type'), dataIndex: 'type', key: 'type', width: 100, render: (v: string) => getTypeTag(v) },
{ title: t('execution.applicant'), dataIndex: 'applicant', key: 'applicant', width: 80 },
{ title: t('execution.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: 'executeDate', key: 'executeDate', width: 100, sorter: true, render: (v: string) => v || '-' },
{ title: '执行方式', dataIndex: 'executeMethod', key: 'executeMethod', width: 100, render: (v: string) => v || '-' },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (v: string) => getStatusTag(v) },
{ title: '编号', dataIndex: 'code', key: 'code', width: 140 },
{ title: t('execution.executionDate'), dataIndex: 'executeDate', key: 'executeDate', width: 100, sorter: true, render: (v: string) => v || '-' },
{ title: t('execution.executionMethod'), dataIndex: 'executeMethod', key: 'executeMethod', width: 100, render: (v: string) => v || '-' },
{ title: t('execution.status'), dataIndex: 'status', key: 'status', width: 100, render: (v: string) => getStatusTag(v) },
{ title: t('execution.code'), dataIndex: 'code', key: 'code', width: 140 },
];
// 已执行列表的筛选和排序控件
const ExecutedListControls = () => (
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Input.Search
placeholder="搜索事由、编号或申请人"
placeholder={t('execution.searchPlaceholder')}
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onSearch={(value) => setSearchKeyword(value)}
@@ -610,20 +622,20 @@ const ExecutionManagement: React.FC = () => {
allowClear
/>
<Select
placeholder="筛选类型"
placeholder={t('execution.filterType')}
value={filterType}
onChange={(value) => setFilterType(value)}
style={{ width: 150 }}
allowClear
>
<Select.Option value="预支申请"></Select.Option>
<Select.Option value="报销申请"></Select.Option>
<Select.Option value="付款申请"></Select.Option>
<Select.Option value="核销申请"></Select.Option>
<Select.Option value="采购申请"></Select.Option>
<Select.Option value={t('execution.advanceApply')}>{t('execution.advanceApply')}</Select.Option>
<Select.Option value={t('execution.reimburseApply')}>{t('execution.reimburseApply')}</Select.Option>
<Select.Option value={t('execution.paymentApply')}>{t('execution.paymentApply')}</Select.Option>
<Select.Option value={t('execution.verificationApply')}>{t('execution.verificationApply')}</Select.Option>
<Select.Option value={t('execution.purchaseApply')}>{t('execution.purchaseApply')}</Select.Option>
</Select>
<Select
placeholder="排序方式"
placeholder={t('execution.sortBy')}
value={`${sortField}_${sortOrder}`}
onChange={(value) => {
const [field, order] = (value as string).split('_');
@@ -632,17 +644,17 @@ const ExecutionManagement: React.FC = () => {
}}
style={{ width: 180 }}
>
<Select.Option value="executeDate_descend"></Select.Option>
<Select.Option value="executeDate_ascend"></Select.Option>
<Select.Option value="amount_descend"></Select.Option>
<Select.Option value="amount_ascend"></Select.Option>
<Select.Option value="executeDate_descend">{t('execution.sortDateNew')}</Select.Option>
<Select.Option value="executeDate_ascend">{t('execution.sortDateOld')}</Select.Option>
<Select.Option value="amount_descend">{t('execution.sortAmountHigh')}</Select.Option>
<Select.Option value="amount_ascend">{t('execution.sortAmountLow')}</Select.Option>
</Select>
</div>
);
const tabItems = [
{ key: 'pending', label: <span> <Badge count={pendingData.length} style={{ marginLeft: 8 }} /></span>, children: <Table columns={pendingColumns} dataSource={pendingData} loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 1300 }} /> },
{ key: 'executed', label: '已执行', children: (
{ key: 'pending', label: <span>{t('execution.pendingTab')} <Badge count={pendingData.length} style={{ marginLeft: 8 }} /></span>, children: <Table columns={pendingColumns} dataSource={pendingData} loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 1300 }} /> },
{ key: 'executed', label: t('execution.executedTab'), children: (
<>
<ExecutedListControls />
<Table
@@ -663,18 +675,28 @@ const ExecutionManagement: React.FC = () => {
];
// 上传配置
const getUploadHeaders = () => {
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
const token = parsed?.state?.token;
if (token) return { authorization: `Bearer ${token}` };
}
} catch (e) {}
return {};
};
const uploadProps = {
name: 'file',
action: '/api/upload/single',
headers: {
authorization: 'authorization-text',
},
headers: getUploadHeaders(),
onChange(info: any) {
// 更新文件列表状态
setVoucherFiles(info.fileList);
if (info.file.status === 'done') {
message.success(`${info.file.name} 上传成功`);
message.success(t('execution.uploadSuccess', { name: info.file.name }));
// 如果上传成功,将返回的URL添加到文件对象中
const updatedFileList = info.fileList.map((file: any) => {
if (file.uid === info.file.uid && file.response) {
@@ -687,7 +709,7 @@ const ExecutionManagement: React.FC = () => {
});
setVoucherFiles(updatedFileList);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 上传失败`);
message.error(t('execution.uploadFailed', { name: info.file.name }));
}
},
fileList: voucherFiles,
@@ -695,24 +717,24 @@ const ExecutionManagement: React.FC = () => {
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 24 }}><h2 style={{ marginBottom: 8 }}></h2><p style={{ color: '#888', marginBottom: 0 }}></p></div>
<div style={{ marginBottom: 24 }}><h2 style={{ marginBottom: 8 }}>{t('execution.title')}</h2><p style={{ color: '#888', marginBottom: 0 }}>{t('execution.description')}</p></div>
<Card><Tabs items={tabItems} /></Card>
{/* 详情模态框 */}
<Modal
title={`${selectedRecord?.type}详情`}
title={`${selectedRecord?.type}${t('common.detail')}`}
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
width={900}
footer={
selectedRecord?.status === 'approved' || selectedRecord?.status === 'pending' ? (
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<Button onClick={() => setDetailModalVisible(false)}></Button>
<Button danger icon={<CloseOutlined />} onClick={handleReject}>退</Button>
<Button type="primary" icon={<CheckOutlined />} onClick={handleExecute}></Button>
<Button onClick={() => setDetailModalVisible(false)}>{t('execution.cancel')}</Button>
<Button danger icon={<CloseOutlined />} onClick={handleReject}>{t('execution.pass')}</Button>
<Button type="primary" icon={<CheckOutlined />} onClick={handleExecute}>{t('execution.execute')}</Button>
</div>
) : (
<Button onClick={() => setDetailModalVisible(false)}></Button>
<Button onClick={() => setDetailModalVisible(false)}>{t('execution.close')}</Button>
)
}
>
@@ -720,37 +742,37 @@ const ExecutionManagement: React.FC = () => {
<>
{/* 基本信息 */}
<Descriptions bordered column={2} size="small">
<Descriptions.Item label="申请类型">{getTypeTag(selectedRecord.type)}</Descriptions.Item>
<Descriptions.Item label="申请编号">{selectedRecord.code}</Descriptions.Item>
<Descriptions.Item label="申请人">{selectedRecord.applicant}</Descriptions.Item>
<Descriptions.Item label="申请日期">{selectedRecord.date || fullDetail.advance_date || fullDetail.reimbursement_date || fullDetail.payment_date || fullDetail.verification_date}</Descriptions.Item>
<Descriptions.Item label="金额">
<Descriptions.Item label={t('execution.applicationType')}>{getTypeTag(selectedRecord.type)}</Descriptions.Item>
<Descriptions.Item label={t('execution.applicationCode')}>{selectedRecord.code}</Descriptions.Item>
<Descriptions.Item label={t('execution.applicant')}>{selectedRecord.applicant}</Descriptions.Item>
<Descriptions.Item label={t('execution.approvalDate')}>{selectedRecord.date || fullDetail.advance_date || fullDetail.reimbursement_date || fullDetail.payment_date || fullDetail.verification_date}</Descriptions.Item>
<Descriptions.Item label={t('execution.amount')}>
{formatAmount(selectedRecord.amount, selectedRecord.currency)}
{selectedRecord.currency !== 'CNY' && fullDetail.amount_cny > 0 && (
<span style={{ color: '#999', marginLeft: 8 }}> ¥{fullDetail.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}</span>
)}
</Descriptions.Item>
<Descriptions.Item label="状态">{getStatusTag(selectedRecord.status)}</Descriptions.Item>
<Descriptions.Item label="事由" span={2}>{selectedRecord.reason}</Descriptions.Item>
<Descriptions.Item label={t('execution.status')}>{getStatusTag(selectedRecord.status)}</Descriptions.Item>
<Descriptions.Item label={t('execution.subject')} span={2}>{selectedRecord.reason}</Descriptions.Item>
{/* 付款申请特有字段 */}
{selectedRecord.type === '付款申请' && (
{selectedRecord.type === t('execution.paymentApply') && (
<>
<Descriptions.Item label="收款单位类型">
{fullDetail.payee_type === 'subcontractor' ? '分包商' :
fullDetail.payee_type === 'supplier' ? '供应商' :
fullDetail.payee_type === 'customer' ? '客户' : '其他'}
<Descriptions.Item label={t('execution.payeeType')}>
{fullDetail.payee_type === 'subcontractor' ? t('execution.counterpartySubcontractor') :
fullDetail.payee_type === 'supplier' ? t('execution.counterpartySupplier') :
fullDetail.payee_type === 'customer' ? t('execution.counterpartyCustomer') : t('execution.counterpartyOther')}
</Descriptions.Item>
<Descriptions.Item label="收款方">{fullDetail.payee || '-'}</Descriptions.Item>
<Descriptions.Item label="银行名称">{fullDetail.bank_name || '-'}</Descriptions.Item>
<Descriptions.Item label="银行账号">{fullDetail.bank_account || '-'}</Descriptions.Item>
<Descriptions.Item label="支出类型">
{fullDetail.expense_type === 'company' ? '公司支出' : '项目支出'}
<Descriptions.Item label={t('execution.payee')}>{fullDetail.payee || '-'}</Descriptions.Item>
<Descriptions.Item label={t('execution.bankName')}>{fullDetail.bank_name || '-'}</Descriptions.Item>
<Descriptions.Item label={t('execution.bankAccount')}>{fullDetail.bank_account || '-'}</Descriptions.Item>
<Descriptions.Item label={t('execution.expenseType')}>
{fullDetail.expense_type === 'company' ? t('execution.companyExpense') : t('execution.projectExpense')}
</Descriptions.Item>
{fullDetail.expense_type === 'project' && fullDetail.project_id && (
<Descriptions.Item label="关联项目">{getProjectName(fullDetail.project_id)}</Descriptions.Item>
<Descriptions.Item label={t('execution.relatedProject')}>{getProjectName(fullDetail.project_id)}</Descriptions.Item>
)}
<Descriptions.Item label="支出分类">
<Descriptions.Item label={t('execution.expenseCategory')}>
{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)
@@ -760,75 +782,75 @@ const ExecutionManagement: React.FC = () => {
)}
{/* 报销申请特有字段 */}
{selectedRecord.type === '报销申请' && fullDetail.expense_type && (
{selectedRecord.type === t('execution.reimburseApply') && fullDetail.expense_type && (
<>
<Descriptions.Item label="支出类型">
{fullDetail.expense_type === 'company' ? '公司支出' : '项目支出'}
<Descriptions.Item label={t('execution.expenseType')}>
{fullDetail.expense_type === 'company' ? t('execution.companyExpense') : t('execution.projectExpense')}
</Descriptions.Item>
{fullDetail.project_id && (
<Descriptions.Item label="关联项目">{getProjectName(fullDetail.project_id)}</Descriptions.Item>
<Descriptions.Item label={t('execution.relatedProject')}>{getProjectName(fullDetail.project_id)}</Descriptions.Item>
)}
</>
)}
{/* 核销申请特有字段 */}
{selectedRecord.type === '核销申请' && fullDetail.advance_code && (
{selectedRecord.type === t('execution.verificationApply') && fullDetail.advance_code && (
<>
<Descriptions.Item label="关联预支单">{fullDetail.advance_code}</Descriptions.Item>
<Descriptions.Item label="预支金额">{formatAmount(fullDetail.advance_amount, fullDetail.currency)}</Descriptions.Item>
<Descriptions.Item label="结算核销">
<Descriptions.Item label={t('execution.relatedAdvance')}>{fullDetail.advance_code}</Descriptions.Item>
<Descriptions.Item label={t('execution.advanceAmount')}>{formatAmount(fullDetail.advance_amount, fullDetail.currency)}</Descriptions.Item>
<Descriptions.Item label={t('execution.settlement')}>
<span style={{ fontWeight: 'bold', color: fullDetail.settlement ? '#52c41a' : '#fa8c16' }}>
{fullDetail.settlement ? '是' : '否'}
{fullDetail.settlement ? t('common.is') : t('common.no')}
</span>
</Descriptions.Item>
<Descriptions.Item label="已核销金额">{formatAmount(fullDetail.total_reimbursed || 0, fullDetail.currency)}</Descriptions.Item>
<Descriptions.Item label="剩余核销金额">{formatAmount((fullDetail.advance_amount || 0) - (fullDetail.total_reimbursed || 0), fullDetail.currency)}</Descriptions.Item>
<Descriptions.Item label={t('execution.verifiedAmount')}>{formatAmount(fullDetail.total_reimbursed || 0, fullDetail.currency)}</Descriptions.Item>
<Descriptions.Item label={t('execution.remainingAmount')}>{formatAmount((fullDetail.advance_amount || 0) - (fullDetail.total_reimbursed || 0), fullDetail.currency)}</Descriptions.Item>
{fullDetail.settlement && fullDetail.settlement_amount && (
<Descriptions.Item label="核销结算金额" span={2}>
{fullDetail.settlement_amount > 0 ? `退款 ${formatAmount(fullDetail.settlement_amount, fullDetail.currency)}` : `补款 ${formatAmount(Math.abs(fullDetail.settlement_amount), fullDetail.currency)}`}
<Descriptions.Item label={t('execution.settlementAmount')} span={2}>
{fullDetail.settlement_amount > 0 ? `${t('execution.refundText')}${formatAmount(fullDetail.settlement_amount, fullDetail.currency)}` : `${t('execution.supplementText')}${formatAmount(Math.abs(fullDetail.settlement_amount), fullDetail.currency)}`}
</Descriptions.Item>
)}
</>
)}
{/* 采购申请特有字段 */}
{selectedRecord.type === '采购申请' && (
{selectedRecord.type === t('execution.purchaseApply') && (
<>
<Descriptions.Item label="采购类型">
{fullDetail.purchase_type === 'project' ? '项目采购' : '库存采购'}
<Descriptions.Item label={t('execution.purchaseType')}>
{fullDetail.purchase_type === 'project' ? t('execution.projectPurchase') : t('execution.stockPurchase')}
</Descriptions.Item>
{fullDetail.purchase_type === 'project' && fullDetail.project_id && (
<Descriptions.Item label="关联项目">{getProjectName(fullDetail.project_id)}</Descriptions.Item>
<Descriptions.Item label={t('execution.relatedProject')}>{getProjectName(fullDetail.project_id)}</Descriptions.Item>
)}
<Descriptions.Item label="供应商">{fullDetail.supplier_name || '-'}</Descriptions.Item>
<Descriptions.Item label="支出分类">
{fullDetail.expense_category === 'material' ? '材料' :
fullDetail.expense_category === 'equipment' ? '设备' :
fullDetail.expense_category === 'pole' ? '电杆' : '其他'}
<Descriptions.Item label={t('execution.supplier')}>{fullDetail.supplier_name || '-'}</Descriptions.Item>
<Descriptions.Item label={t('execution.expenseCategory')}>
{fullDetail.expense_category === 'material' ? t('execution.material') :
fullDetail.expense_category === 'equipment' ? t('execution.equipment') :
fullDetail.expense_category === 'pole' ? t('execution.pole') : t('execution.otherCategory')}
</Descriptions.Item>
<Descriptions.Item label="币种">{fullDetail.currency}</Descriptions.Item>
<Descriptions.Item label="申请日期">{fullDetail.request_date}</Descriptions.Item>
<Descriptions.Item label="事由" span={2}>{fullDetail.brief_description || '-'}</Descriptions.Item>
<Descriptions.Item label={t('execution.currency')}>{fullDetail.currency}</Descriptions.Item>
<Descriptions.Item label={t('execution.approvalDate')}>{fullDetail.request_date}</Descriptions.Item>
<Descriptions.Item label={t('execution.subject')} span={2}>{fullDetail.brief_description || '-'}</Descriptions.Item>
{fullDetail.remark && (
<Descriptions.Item label="备注" span={2}>{fullDetail.remark}</Descriptions.Item>
<Descriptions.Item label={t('execution.remark')} span={2}>{fullDetail.remark}</Descriptions.Item>
)}
</>
)}
</Descriptions>
{/* 采购申请供应商收款信息 */}
{selectedRecord.type === '采购申请' && fullDetail.supplier_payment_infos && fullDetail.supplier_payment_infos.length > 0 && (
{selectedRecord.type === t('execution.purchaseApply') && fullDetail.supplier_payment_infos && fullDetail.supplier_payment_infos.length > 0 && (
<>
<Divider></Divider>
<Divider>{t('execution.supplierPaymentInfo')}</Divider>
<Descriptions bordered column={2} size="small">
{fullDetail.supplier_payment_infos.filter((p: any) => p.is_primary).map((payment: any, index: number) => (
<React.Fragment key={index}>
<Descriptions.Item label="收款户名">{payment.account_name || '-'}</Descriptions.Item>
<Descriptions.Item label="银行账号">{payment.bank_account || '-'}</Descriptions.Item>
<Descriptions.Item label="开户银行">{payment.bank_name || '-'}</Descriptions.Item>
<Descriptions.Item label={t('execution.accountName')}>{payment.account_name || '-'}</Descriptions.Item>
<Descriptions.Item label={t('execution.bankAccount')}>{payment.bank_account || '-'}</Descriptions.Item>
<Descriptions.Item label={t('execution.bankName')}>{payment.bank_name || '-'}</Descriptions.Item>
{payment.qr_code && (
<Descriptions.Item label="收款码">
<img src={payment.qr_code} alt="收款码" style={{ width: 100, height: 100, objectFit: 'contain' }} />
<Descriptions.Item label={t('execution.qrCode')}>
<img src={payment.qr_code} alt={t('execution.qrCode')} style={{ width: 100, height: 100, objectFit: 'contain' }} />
</Descriptions.Item>
)}
</React.Fragment>
@@ -838,9 +860,9 @@ const ExecutionManagement: React.FC = () => {
)}
{/* 采购申请商品明细 */}
{selectedRecord.type === '采购申请' && fullDetail.items && fullDetail.items.length > 0 && (
{selectedRecord.type === t('execution.purchaseApply') && fullDetail.items && fullDetail.items.length > 0 && (
<>
<Divider></Divider>
<Divider>{t('execution.purchaseDetail')}</Divider>
<List
size="small"
bordered
@@ -855,8 +877,8 @@ const ExecutionManagement: React.FC = () => {
</span>
</div>
<div style={{ fontSize: 13, color: '#666' }}>
: {item.specification || '-'} | : {item.unit || '-'} |
: {item.quantity} | : {fullDetail.currency} {item.unit_price?.toLocaleString('zh-CN', {minimumFractionDigits: 2})}
{t('execution.specLabel')}{item.specification || '-'} | {t('execution.unitLabel')}{item.unit || '-'} |
{t('execution.qtyLabel')}{item.quantity} | {t('execution.priceLabel')}{fullDetail.currency} {item.unit_price?.toLocaleString('zh-CN', {minimumFractionDigits: 2})}
</div>
</div>
</List.Item>
@@ -868,7 +890,7 @@ const ExecutionManagement: React.FC = () => {
{/* 明细清单 */}
{fullDetail.detail_items && fullDetail.detail_items.length > 0 && (
<>
<Divider></Divider>
<Divider>{t('execution.detailList')}</Divider>
{renderDetailItems(fullDetail.detail_items)}
</>
)}
@@ -876,7 +898,7 @@ const ExecutionManagement: React.FC = () => {
{/* 审批意见 */}
{fullDetail.approval_remark && (
<>
<Divider></Divider>
<Divider>{t('execution.approvalOpinion')}</Divider>
<div style={{ padding: '12px', background: '#f5f5f5', borderRadius: '4px' }}>
<p style={{ margin: 0, color: '#666' }}>{fullDetail.approval_remark}</p>
</div>
@@ -886,7 +908,7 @@ const ExecutionManagement: React.FC = () => {
{/* 申请凭证附件或退款凭证 */}
{fullDetail.attachments && fullDetail.attachments.length > 0 && (
<>
<Divider>{fullDetail.settlement && fullDetail.settlement_amount > 0 ? '退款凭证' : '申请凭证附件'}</Divider>
<Divider>{fullDetail.settlement && fullDetail.settlement_amount > 0 ? t('execution.refundProof') : t('execution.applicationAttachment')}</Divider>
{renderAttachments(fullDetail.attachments)}
</>
)}
@@ -894,49 +916,49 @@ const ExecutionManagement: React.FC = () => {
{/* 执行表单 */}
{(selectedRecord.status === 'approved' || selectedRecord.status === 'pending') && (
<>
<Divider></Divider>
<Divider>{t('execution.executionInfo')}</Divider>
<Form form={form} layout="vertical">
{/* 执行/确认日期 */}
<Form.Item name="execute_date" label={selectedRecord.type === '核销申请' && fullDetail.settlement && fullDetail.settlement_amount > 0 ? "确认日期" : "执行日期"} rules={[{ required: true }]}>
<Form.Item name="execute_date" label={selectedRecord.type === t('execution.verificationApply') && fullDetail.settlement && fullDetail.settlement_amount > 0 ? t('execution.confirmationDate') : t('execution.executionDate')} rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
{/* 执行方式或收款方式 - 非结算核销不需要 */}
{!(selectedRecord.type === '核销申请' && !fullDetail.settlement) && (
selectedRecord.type === '核销申请' && fullDetail.settlement && fullDetail.settlement_amount > 0 ? (
<Form.Item name="execute_method" label="收款方式" rules={[{ required: true }]}>
<Select options={[{ value: 'bank', label: '银行转账' }, { value: 'cash', label: '现金' }, { value: 'wechat', label: '微信' }, { value: 'other', label: '其他' }]} />
{!(selectedRecord.type === t('execution.verificationApply') && !fullDetail.settlement) && (
selectedRecord.type === t('execution.verificationApply') && fullDetail.settlement && fullDetail.settlement_amount > 0 ? (
<Form.Item name="execute_method" label={t('execution.paymentMethod')} rules={[{ required: true }]}>
<Select options={[{ value: 'bank', label: t('execution.bankTransfer') }, { value: 'cash', label: t('execution.cash') }, { value: 'wechat', label: t('execution.wechat') }, { value: 'other', label: t('execution.other') }]} />
</Form.Item>
) : (
<Form.Item name="execute_method" label="执行方式" rules={[{ required: true }]}>
<Select options={[{ value: 'bank', label: '银行转账' }, { value: 'cash', label: '现金' }, { value: 'wechat', label: '微信' }, { value: 'other', label: '其他' }]} />
<Form.Item name="execute_method" label={t('execution.executionMethodLabel')} rules={[{ required: true }]}>
<Select options={[{ value: 'bank', label: t('execution.bankTransfer') }, { value: 'cash', label: t('execution.cash') }, { value: 'wechat', label: t('execution.wechat') }, { value: 'other', label: t('execution.other') }]} />
</Form.Item>
)
)}
{/* 只有退款类型的核销申请和非结算核销不需要上传付款凭证,其他类型的申请都需要 */}
{!(selectedRecord.type === '核销申请' && (fullDetail.settlement && fullDetail.settlement_amount > 0 || !fullDetail.settlement)) && (
<Form.Item label="付款凭证" required>
{!(selectedRecord.type === t('execution.verificationApply') && (fullDetail.settlement && fullDetail.settlement_amount > 0 || !fullDetail.settlement)) && (
<Form.Item label={t('execution.proofOfPayment')} required>
<Upload {...uploadProps}>
<Button icon={<UploadOutlined />}></Button>
<Button icon={<UploadOutlined />}>{t('execution.uploadProof')}</Button>
</Upload>
<div style={{ marginTop: 8, color: '#666', fontSize: 12 }}>
PDF格式
{t('execution.proofUploadTip')}
</div>
</Form.Item>
)}
{/* 退款的核销申请显示提示 */}
{selectedRecord.type === '核销申请' && fullDetail.settlement && fullDetail.settlement_amount > 0 && (
{selectedRecord.type === t('execution.verificationApply') && fullDetail.settlement && fullDetail.settlement_amount > 0 && (
<div style={{ margin: '16px 0', padding: '12px', backgroundColor: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: '4px' }}>
<p style={{ margin: 0, color: '#389e0d' }}>退</p>
<p style={{ margin: 0, color: '#389e0d' }}>{t('execution.noProofRefund')}</p>
</div>
)}
{/* 非结算核销的核销申请显示提示 */}
{selectedRecord.type === '核销申请' && !fullDetail.settlement && (
{selectedRecord.type === t('execution.verificationApply') && !fullDetail.settlement && (
<div style={{ margin: '16px 0', padding: '12px', backgroundColor: '#e6f7ff', border: '1px solid #91d5ff', borderRadius: '4px' }}>
<p style={{ margin: 0, color: '#1890ff' }}></p>
<p style={{ margin: 0, color: '#1890ff' }}>{t('execution.noProofNonSettlement')}</p>
</div>
)}
@@ -945,20 +967,20 @@ const ExecutionManagement: React.FC = () => {
// 执行操作时显示的字段
<>
{/* 收款确认信息(仅退款类型)或备注 */}
{selectedRecord.type === '核销申请' && fullDetail.settlement && fullDetail.settlement_amount > 0 ? (
<Form.Item name="remark" label="收款确认信息" rules={[{ required: true, message: '请填写收款确认信息' }]}>
<TextArea rows={3} placeholder="请填写收款确认信息,如收款账号、收款时间等" />
{selectedRecord.type === t('execution.verificationApply') && fullDetail.settlement && fullDetail.settlement_amount > 0 ? (
<Form.Item name="remark" label={t('execution.paymentConfirmation')} rules={[{ required: true, message: t('execution.confirmRequired') }]}>
<TextArea rows={3} placeholder={t('execution.confirmationPlaceholder')} />
</Form.Item>
) : (
<Form.Item name="remark" label="备注">
<TextArea rows={2} placeholder="可选:填写执行备注" />
<Form.Item name="remark" label={t('execution.remark')}>
<TextArea rows={2} placeholder={t('execution.remarkPlaceholder')} />
</Form.Item>
)}
</>
) : (
// 退回操作时显示的字段
<Form.Item name="rejectReason" label="退回原因" rules={[{ required: true, message: '请填写退回原因' }]}>
<TextArea rows={3} placeholder="请填写退回原因" />
<Form.Item name="rejectReason" label={t('execution.returnReason')} rules={[{ required: true, message: t('execution.rejectReasonRequired') }]}>
<TextArea rows={3} placeholder={t('execution.returnReason')} />
</Form.Item>
)}
</Form>
@@ -968,12 +990,12 @@ const ExecutionManagement: React.FC = () => {
)}
</Modal>
<Modal title={`编辑申请${selectedRecord?.code}`} open={editModalVisible} onCancel={() => setEditModalVisible(false)} onOk={handleEditSubmit} width={600}>
<Modal title={`${t('execution.edit')}${selectedRecord?.code}`} open={editModalVisible} onCancel={() => setEditModalVisible(false)} onOk={handleEditSubmit} width={600}>
<Form form={editForm} layout="vertical">
<Form.Item label="申请类型"><Input value={selectedRecord?.type} disabled /></Form.Item>
<Form.Item label="申请人"><Input value={selectedRecord?.applicant} disabled /></Form.Item>
<Form.Item name="amount" label="金额" rules={[{ required: true }]}><Input type="number" style={{ width: '100%' }} /></Form.Item>
<Form.Item name="reason" label="事由" rules={[{ required: true }]}><TextArea rows={3} /></Form.Item>
<Form.Item label={t('execution.applicationType')}><Input value={selectedRecord?.type} disabled /></Form.Item>
<Form.Item label={t('execution.applicant')}><Input value={selectedRecord?.applicant} disabled /></Form.Item>
<Form.Item name="amount" label={t('execution.amount')} rules={[{ required: true }]}><Input type="number" style={{ width: '100%' }} /></Form.Item>
<Form.Item name="reason" label={t('execution.subject')} rules={[{ required: true }]}><TextArea rows={3} /></Form.Item>
</Form>
</Modal>