2026-03-25 23:55:36 +07:00
|
|
|
|
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';
|
2026-06-13 12:44:48 +08:00
|
|
|
|
import { useLanguageStore } from '../../store/languageStore';
|
2026-03-25 23:55:36 +07:00
|
|
|
|
|
|
|
|
|
|
const { TextArea } = Input;
|
|
|
|
|
|
|
|
|
|
|
|
// 项目支出分类
|
|
|
|
|
|
const PROJECT_EXPENSE_CATEGORIES = [
|
|
|
|
|
|
{ value: 'material_purchase', label: '材料采购' },
|
|
|
|
|
|
{ value: 'equipment_purchase', label: '设备采购' },
|
|
|
|
|
|
{ value: 'pole_crossarm', label: '电杆横担支出' },
|
|
|
|
|
|
{ value: 'freight', label: '运费支出' },
|
|
|
|
|
|
{ value: 'construction', label: '施工费支出' },
|
|
|
|
|
|
{ value: 'other', label: '其他支出' }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 公司支出分类
|
|
|
|
|
|
const COMPANY_EXPENSE_CATEGORIES = [
|
|
|
|
|
|
{ value: 'office_operations', label: '通用运营(房租/耗材)' },
|
|
|
|
|
|
{ value: 'transportation', label: '交通通勤' },
|
|
|
|
|
|
{ value: 'marketing', label: '业扩营销' },
|
|
|
|
|
|
{ value: 'power_system', label: '电力系统关系' },
|
|
|
|
|
|
{ value: 'employee_welfare', label: '员工福利' },
|
|
|
|
|
|
{ value: 'logistics', label: '快递物流' },
|
|
|
|
|
|
{ value: 'other', label: '其他支出' }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-06-13 12:44:48 +08:00
|
|
|
|
// Type name to i18n key mapping
|
|
|
|
|
|
const typeKeyMap: Record<string, string> = {
|
|
|
|
|
|
'预支申请': 'execution.advanceApply',
|
|
|
|
|
|
'报销申请': 'execution.reimburseApply',
|
|
|
|
|
|
'付款申请': 'execution.paymentApply',
|
|
|
|
|
|
'核销申请': 'execution.verificationApply',
|
|
|
|
|
|
'采购申请': 'execution.purchaseApply',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 23:55:36 +07:00
|
|
|
|
// 执行记录类型
|
|
|
|
|
|
interface ExecutionRecord {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
applyCode: string;
|
|
|
|
|
|
applyType: string;
|
|
|
|
|
|
applicant: string;
|
|
|
|
|
|
amount: number;
|
|
|
|
|
|
currency: string;
|
|
|
|
|
|
action: 'execute' | 'reject';
|
|
|
|
|
|
operator: string;
|
|
|
|
|
|
operatorRole: string;
|
|
|
|
|
|
timestamp: string;
|
|
|
|
|
|
executeMethod?: string;
|
|
|
|
|
|
voucherNo?: string;
|
|
|
|
|
|
rejectReason?: string;
|
|
|
|
|
|
remark?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ExecutionManagement: React.FC = () => {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
const { t, currentLanguage } = useLanguageStore();
|
2026-03-25 23:55:36 +07:00
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
|
|
|
|
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
|
|
|
|
|
const [selectedRecord, setSelectedRecord] = useState<any>(null);
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const [fullDetail, setFullDetail] = useState<any>(null);
|
2026-03-25 23:55:36 +07:00
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
const [editForm] = Form.useForm();
|
|
|
|
|
|
const [voucherFiles, setVoucherFiles] = useState<any[]>([]);
|
|
|
|
|
|
const [isRejecting, setIsRejecting] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 待执行数据
|
|
|
|
|
|
const [pendingData, setPendingData] = useState([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 已执行数据
|
|
|
|
|
|
const [executedData, setExecutedData] = useState([]);
|
|
|
|
|
|
|
2026-03-28 00:34:32 +07:00
|
|
|
|
// 已执行列表筛选状态
|
|
|
|
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
|
|
|
|
|
const [filterType, setFilterType] = useState<string | null>(null);
|
|
|
|
|
|
const [sortField, setSortField] = useState<string>('executeDate');
|
|
|
|
|
|
const [sortOrder, setSortOrder] = useState<'ascend' | 'descend'>('descend');
|
|
|
|
|
|
|
2026-03-25 23:55:36 +07:00
|
|
|
|
// 项目列表
|
|
|
|
|
|
const [projects, setProjects] = useState<any[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 从后端获取待执行数据
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchPendingData = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const response = await fetch('/api/executions/pending');
|
2026-03-25 23:55:36 +07:00
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && Array.isArray(data.data)) {
|
|
|
|
|
|
setPendingData(data.data.map((item: any, index: number) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
key: item.id || index,
|
|
|
|
|
|
rawData: item
|
|
|
|
|
|
})));
|
|
|
|
|
|
} else {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.getPendingFailedFormat'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.getPendingFailed') + response.statusText);
|
2026-03-25 23:55:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取待执行数据错误:', error);
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.getPendingNetworkError'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchPendingData();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取项目列表
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchProjects = async () => {
|
|
|
|
|
|
try {
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const response = await fetch('/api/projects');
|
2026-03-25 23:55:36 +07:00
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && Array.isArray(data.data)) {
|
|
|
|
|
|
setProjects(data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取项目列表失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchProjects();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 从后端获取已执行数据
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchExecutedData = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const response = await fetch('/api/executions/executed');
|
2026-03-25 23:55:36 +07:00
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && Array.isArray(data.data)) {
|
|
|
|
|
|
setExecutedData(data.data.map((item: any, index: number) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
key: item.id || index,
|
|
|
|
|
|
rawData: item
|
|
|
|
|
|
})));
|
|
|
|
|
|
} else {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.getExecutedFailedFormat'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.getExecutedFailed') + response.statusText);
|
2026-03-25 23:55:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取已执行数据错误:', error);
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.getExecutedNetworkError'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchExecutedData();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
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 formatNumberWithSeparator = (value: number | undefined, currency: string): string => {
|
|
|
|
|
|
if (value === undefined || value === null) return '';
|
|
|
|
|
|
const symbols: Record<string, string> = { CNY: '¥', USD: '$', LAK: '₭', THB: '฿' };
|
|
|
|
|
|
return (symbols[currency] || '¥') + value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const parseFormattedNumber = (value: string): number => {
|
|
|
|
|
|
const cleaned = value.replace(/[¥$₭฿,]/g, '');
|
|
|
|
|
|
return parseFloat(cleaned) || 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getTypeTag = (type: string) => {
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const colors: Record<string, string> = { '预支申请': 'blue', '报销申请': 'green', '付款申请': 'orange', '核销申请': 'purple', '采购申请': 'cyan' };
|
2026-06-13 12:44:48 +08:00
|
|
|
|
const key = typeKeyMap[type] || type;
|
|
|
|
|
|
return <Tag color={colors[type] || 'default'}>{t(key)}</Tag>;
|
2026-03-25 23:55:36 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getStatusTag = (status: string) => {
|
|
|
|
|
|
const statusMap: Record<string, { color: string; text: string }> = {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
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') },
|
2026-03-25 23:55:36 +07:00
|
|
|
|
};
|
|
|
|
|
|
const config = statusMap[status] || { color: 'default', text: status };
|
|
|
|
|
|
return <Tag color={config.color}>{config.text}</Tag>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 根据项目ID获取项目名称
|
|
|
|
|
|
const getProjectName = (projectId: any) => {
|
|
|
|
|
|
if (!projectId) return '-';
|
|
|
|
|
|
const project = projects.find(p => p.id === projectId);
|
|
|
|
|
|
return project ? project.name : `项目ID: ${projectId}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 查看详情
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const handleViewDetail = async (record: any) => {
|
2026-03-25 23:55:36 +07:00
|
|
|
|
setSelectedRecord(record);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({ execute_date: dayjs(), execute_method: 'bank' });
|
|
|
|
|
|
setVoucherFiles([]);
|
|
|
|
|
|
setIsRejecting(false);
|
2026-03-28 00:34:32 +07:00
|
|
|
|
|
|
|
|
|
|
// 获取完整详情
|
2026-06-13 12:44:48 +08:00
|
|
|
|
if (record.type === t('execution.purchaseApply')) {
|
2026-03-28 00:34:32 +07:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/purchase-requests/${record.id}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
setFullDetail(data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取采购申请详情失败:', error);
|
|
|
|
|
|
// 如果获取失败,使用record中的数据
|
|
|
|
|
|
setFullDetail(record);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他类型使用record中的数据
|
|
|
|
|
|
setFullDetail(record);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-25 23:55:36 +07:00
|
|
|
|
setDetailModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理执行
|
|
|
|
|
|
const handleExecute = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 检查是否为核销申请
|
2026-06-13 12:44:48 +08:00
|
|
|
|
const isVerification = selectedRecord.type === t('execution.verificationApply');
|
2026-03-25 23:55:36 +07:00
|
|
|
|
// 检查是否为退款类型的核销申请
|
|
|
|
|
|
const isRefundVerification = isVerification && fullDetail.settlement && fullDetail.settlement_amount > 0;
|
|
|
|
|
|
// 检查是否为非结算核销
|
|
|
|
|
|
const isNonSettlementVerification = isVerification && !fullDetail.settlement;
|
|
|
|
|
|
|
|
|
|
|
|
// 非结算核销不需要验证执行方式和付款凭证
|
|
|
|
|
|
if (isNonSettlementVerification) {
|
|
|
|
|
|
// 直接执行,不需要验证表单
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用执行API
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const executeResponse = await fetch('/api/executions', {
|
2026-03-25 23:55:36 +07:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
apply_id: selectedRecord.id,
|
|
|
|
|
|
apply_type: 'verification',
|
|
|
|
|
|
action: 'execute',
|
|
|
|
|
|
execute_method: 'none', // 非结算核销不需要执行方式
|
|
|
|
|
|
voucher_files: [], // 非结算核销不需要付款凭证
|
|
|
|
|
|
remark: form.getFieldValue('remark') || ''
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (executeResponse.ok) {
|
|
|
|
|
|
setPendingData(pendingData.filter(item => item.key !== selectedRecord.key));
|
|
|
|
|
|
// 刷新已执行数据
|
|
|
|
|
|
const fetchExecutedData = async () => {
|
|
|
|
|
|
try {
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const response = await fetch('/api/executions/executed');
|
2026-03-25 23:55:36 +07:00
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && Array.isArray(data.data)) {
|
|
|
|
|
|
setExecutedData(data.data.map((item: any, index: number) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
key: item.id || index,
|
|
|
|
|
|
rawData: item
|
|
|
|
|
|
})));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取已执行数据错误:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
fetchExecutedData();
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.success(t('execution.executeSuccess', { code: selectedRecord.code }));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
setDetailModalVisible(false);
|
|
|
|
|
|
} else {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.executeFailed'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他类型的申请需要验证表单
|
|
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否需要上传付款凭证
|
|
|
|
|
|
if (!isRefundVerification && (!voucherFiles || voucherFiles.length === 0)) {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.proofRequired'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取已上传文件的URL列表(如果需要)
|
|
|
|
|
|
let voucherFileUrls = [];
|
|
|
|
|
|
if (!isRefundVerification) {
|
|
|
|
|
|
voucherFileUrls = voucherFiles
|
|
|
|
|
|
.map(f => f.url || f.response?.data?.url || f.response?.url)
|
|
|
|
|
|
.filter(url => url); // 过滤掉空值
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 调用执行API
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const executeResponse = await fetch('/api/executions', {
|
2026-03-25 23:55:36 +07:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
apply_id: selectedRecord.id,
|
2026-06-13 12:44:48 +08:00
|
|
|
|
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',
|
2026-03-25 23:55:36 +07:00
|
|
|
|
action: 'execute',
|
|
|
|
|
|
execute_method: isRefundVerification ? 'refund' : values.execute_method,
|
|
|
|
|
|
voucher_files: voucherFileUrls,
|
|
|
|
|
|
remark: values.remark
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (executeResponse.ok) {
|
|
|
|
|
|
setPendingData(pendingData.filter(item => item.key !== selectedRecord.key));
|
|
|
|
|
|
// 刷新已执行数据
|
|
|
|
|
|
const fetchExecutedData = async () => {
|
|
|
|
|
|
try {
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const response = await fetch('/api/executions/executed');
|
2026-03-25 23:55:36 +07:00
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success && Array.isArray(data.data)) {
|
|
|
|
|
|
setExecutedData(data.data.map((item: any, index: number) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
key: item.id || index,
|
|
|
|
|
|
rawData: item
|
|
|
|
|
|
})));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取已执行数据错误:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
fetchExecutedData();
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.success(t('execution.executeSuccess', { code: selectedRecord.code }));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
setDetailModalVisible(false);
|
|
|
|
|
|
} else {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.executeFailed'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('执行操作失败:', error);
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('common.networkError'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理退回
|
|
|
|
|
|
const handleReject = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setIsRejecting(true);
|
|
|
|
|
|
// 只验证退回原因字段
|
|
|
|
|
|
const values = await form.validateFields(['rejectReason'], { force: true });
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用退回API
|
2026-03-28 00:34:32 +07:00
|
|
|
|
const rejectResponse = await fetch('/api/executions', {
|
2026-03-25 23:55:36 +07:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
apply_id: selectedRecord.id,
|
2026-06-13 12:44:48 +08:00
|
|
|
|
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',
|
2026-03-25 23:55:36 +07:00
|
|
|
|
action: 'reject',
|
|
|
|
|
|
reject_reason: values.rejectReason
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (rejectResponse.ok) {
|
|
|
|
|
|
setPendingData(pendingData.filter(item => item.key !== selectedRecord.key));
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.success(t('execution.rejectSuccess', { code: selectedRecord.code }));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
setDetailModalVisible(false);
|
|
|
|
|
|
} else {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.rejectFailed'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('退回操作失败:', error);
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('common.networkError'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleEdit = (record: any) => {
|
|
|
|
|
|
setSelectedRecord(record);
|
|
|
|
|
|
editForm.setFieldsValue({ amount: record.amount, reason: record.reason });
|
|
|
|
|
|
setEditModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleEditSubmit = () => {
|
|
|
|
|
|
editForm.validateFields().then(values => {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.success(t('execution.editSuccess'));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
setEditModalVisible(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染附件列表
|
|
|
|
|
|
const renderAttachments = (attachments: any) => {
|
|
|
|
|
|
// 处理字符串类型的 attachments(JSON字符串)
|
|
|
|
|
|
let attachmentList = attachments;
|
|
|
|
|
|
if (typeof attachments === 'string') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
attachmentList = JSON.parse(attachments);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确保是数组
|
|
|
|
|
|
if (!Array.isArray(attachmentList) || attachmentList.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
2026-04-19 19:15:01 +08:00
|
|
|
|
{attachmentList.map((item: any, index: number) => {
|
|
|
|
|
|
// 确保item是字符串类型的URL
|
|
|
|
|
|
const url = typeof item === 'string' ? item : item?.url;
|
|
|
|
|
|
if (!url) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={index} style={{ position: 'relative' }}>
|
|
|
|
|
|
{url.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={url}
|
|
|
|
|
|
alt={`附件${index + 1}`}
|
|
|
|
|
|
style={{ width: 120, height: 120, objectFit: 'cover', borderRadius: 4, border: '1px solid #f0f0f0', cursor: 'pointer' }}
|
|
|
|
|
|
onClick={() => window.open(url, '_blank')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
|
|
|
|
|
<div style={{ width: 120, height: 120, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #f0f0f0', borderRadius: 4, background: '#f5f5f5' }}>
|
|
|
|
|
|
<FileImageOutlined style={{ fontSize: 32, color: '#999' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取支出分类的中文名称
|
|
|
|
|
|
const getCategoryName = (category: string) => {
|
|
|
|
|
|
if (!category) return '-';
|
|
|
|
|
|
// 特殊分类映射
|
|
|
|
|
|
const specialCategories: Record<string, string> = {
|
|
|
|
|
|
// Project expense categories
|
2026-06-13 12:44:48 +08:00
|
|
|
|
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'),
|
2026-03-25 23:55:36 +07:00
|
|
|
|
// Company expense categories
|
2026-06-13 12:44:48 +08:00
|
|
|
|
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')
|
2026-03-25 23:55:36 +07:00
|
|
|
|
};
|
|
|
|
|
|
// 先检查特殊分类
|
|
|
|
|
|
if (specialCategories[category]) return specialCategories[category];
|
|
|
|
|
|
// 再从项目支出分类中查找
|
|
|
|
|
|
const projectCategory = PROJECT_EXPENSE_CATEGORIES.find(c => c.value === category);
|
|
|
|
|
|
if (projectCategory) return projectCategory.label;
|
|
|
|
|
|
// 再从公司支出分类中查找
|
|
|
|
|
|
const companyCategory = COMPANY_EXPENSE_CATEGORIES.find(c => c.value === category);
|
|
|
|
|
|
if (companyCategory) return companyCategory.label;
|
|
|
|
|
|
// 如果都找不到,返回原始值
|
|
|
|
|
|
return category;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染明细清单
|
|
|
|
|
|
const renderDetailItems = (detailItems: any) => {
|
|
|
|
|
|
// 处理字符串类型的 detailItems(JSON字符串)
|
|
|
|
|
|
let itemsList = detailItems;
|
|
|
|
|
|
if (typeof detailItems === 'string') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
itemsList = JSON.parse(detailItems);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确保是数组
|
|
|
|
|
|
if (!Array.isArray(itemsList) || itemsList.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<List
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
dataSource={itemsList}
|
|
|
|
|
|
renderItem={(item: any, index: number) => (
|
|
|
|
|
|
<List.Item>
|
|
|
|
|
|
<div style={{ width: '100%' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<span><strong>{t('execution.detailLabel', { index: index + 1 })}</strong> {item.description || getCategoryName(item.category) || '-'}</span>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>{formatAmount(item.amount, item.currency)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{item.category && (
|
|
|
|
|
|
<div style={{ marginBottom: 8, fontSize: 13, color: '#666' }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<strong>{t('execution.categoryLabel')}</strong>{getCategoryName(item.category)}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{item.attachments && (
|
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<span style={{ color: '#666', fontSize: 12 }}>{t('execution.detailAttachment')}</span>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
{renderAttachments(item.attachments)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const pendingColumns = [
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{ 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 },
|
2026-03-25 23:55:36 +07:00
|
|
|
|
{
|
2026-06-13 12:44:48 +08:00
|
|
|
|
title: t('execution.action'), key: 'action', width: 200,
|
2026-03-25 23:55:36 +07:00
|
|
|
|
render: (_: any, record: any) => (
|
|
|
|
|
|
<Space wrap>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Space>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-28 00:34:32 +07:00
|
|
|
|
// 筛选和排序已执行数据
|
|
|
|
|
|
const getFilteredExecutedData = () => {
|
|
|
|
|
|
let data = [...executedData];
|
|
|
|
|
|
|
|
|
|
|
|
// 按事由搜索
|
|
|
|
|
|
if (searchKeyword) {
|
|
|
|
|
|
data = data.filter(item =>
|
|
|
|
|
|
(item.reason || '').toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
|
|
|
|
|
(item.code || '').toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
|
|
|
|
|
(item.applicant || '').toLowerCase().includes(searchKeyword.toLowerCase())
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按类型筛选
|
|
|
|
|
|
if (filterType) {
|
|
|
|
|
|
data = data.filter(item => item.type === filterType);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 排序
|
|
|
|
|
|
data.sort((a, b) => {
|
|
|
|
|
|
let aValue = a[sortField];
|
|
|
|
|
|
let bValue = b[sortField];
|
|
|
|
|
|
|
|
|
|
|
|
// 处理日期排序
|
|
|
|
|
|
if (sortField === 'executeDate') {
|
|
|
|
|
|
aValue = a.execute_date || a.executeDate || '';
|
|
|
|
|
|
bValue = b.execute_date || b.executeDate || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sortOrder === 'ascend') {
|
|
|
|
|
|
return aValue > bValue ? 1 : -1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return aValue < bValue ? 1 : -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 23:55:36 +07:00
|
|
|
|
const executedColumns = [
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{ 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) => (
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<>
|
|
|
|
|
|
<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>}
|
|
|
|
|
|
</>
|
|
|
|
|
|
) },
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{ 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 },
|
2026-03-25 23:55:36 +07:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-28 00:34:32 +07:00
|
|
|
|
// 已执行列表的筛选和排序控件
|
|
|
|
|
|
const ExecutedListControls = () => (
|
|
|
|
|
|
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
|
|
|
|
|
<Input.Search
|
2026-06-13 12:44:48 +08:00
|
|
|
|
placeholder={t('execution.searchPlaceholder')}
|
2026-03-28 00:34:32 +07:00
|
|
|
|
value={searchKeyword}
|
|
|
|
|
|
onChange={(e) => setSearchKeyword(e.target.value)}
|
|
|
|
|
|
onSearch={(value) => setSearchKeyword(value)}
|
|
|
|
|
|
style={{ width: 250 }}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
2026-06-13 12:44:48 +08:00
|
|
|
|
placeholder={t('execution.filterType')}
|
2026-03-28 00:34:32 +07:00
|
|
|
|
value={filterType}
|
|
|
|
|
|
onChange={(value) => setFilterType(value)}
|
|
|
|
|
|
style={{ width: 150 }}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
</Select>
|
|
|
|
|
|
<Select
|
2026-06-13 12:44:48 +08:00
|
|
|
|
placeholder={t('execution.sortBy')}
|
2026-03-28 00:34:32 +07:00
|
|
|
|
value={`${sortField}_${sortOrder}`}
|
|
|
|
|
|
onChange={(value) => {
|
|
|
|
|
|
const [field, order] = (value as string).split('_');
|
|
|
|
|
|
setSortField(field);
|
|
|
|
|
|
setSortOrder(order as 'ascend' | 'descend');
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ width: 180 }}
|
|
|
|
|
|
>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-25 23:55:36 +07:00
|
|
|
|
const tabItems = [
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{ 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: (
|
2026-03-28 00:34:32 +07:00
|
|
|
|
<>
|
|
|
|
|
|
<ExecutedListControls />
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={executedColumns}
|
|
|
|
|
|
dataSource={getFilteredExecutedData()}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
pagination={{ pageSize: 10 }}
|
|
|
|
|
|
scroll={{ x: 1400 }}
|
|
|
|
|
|
onChange={(pagination, filters, sorter: any) => {
|
|
|
|
|
|
if (sorter.field) {
|
|
|
|
|
|
setSortField(sorter.field);
|
|
|
|
|
|
setSortOrder(sorter.order || 'descend');
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)},
|
2026-03-25 23:55:36 +07:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 上传配置
|
2026-06-13 12:44:48 +08:00
|
|
|
|
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 {};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 23:55:36 +07:00
|
|
|
|
const uploadProps = {
|
|
|
|
|
|
name: 'file',
|
2026-03-28 00:34:32 +07:00
|
|
|
|
action: '/api/upload/single',
|
2026-06-13 12:44:48 +08:00
|
|
|
|
headers: getUploadHeaders(),
|
2026-03-25 23:55:36 +07:00
|
|
|
|
onChange(info: any) {
|
|
|
|
|
|
// 更新文件列表状态
|
|
|
|
|
|
setVoucherFiles(info.fileList);
|
|
|
|
|
|
|
|
|
|
|
|
if (info.file.status === 'done') {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.success(t('execution.uploadSuccess', { name: info.file.name }));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
// 如果上传成功,将返回的URL添加到文件对象中
|
|
|
|
|
|
const updatedFileList = info.fileList.map((file: any) => {
|
|
|
|
|
|
if (file.uid === info.file.uid && file.response) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...file,
|
|
|
|
|
|
url: file.response.data?.url || file.response.url || file.response
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return file;
|
|
|
|
|
|
});
|
|
|
|
|
|
setVoucherFiles(updatedFileList);
|
|
|
|
|
|
} else if (info.file.status === 'error') {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
message.error(t('execution.uploadFailed', { name: info.file.name }));
|
2026-03-25 23:55:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
fileList: voucherFiles,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ padding: 24 }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<div style={{ marginBottom: 24 }}><h2 style={{ marginBottom: 8 }}>{t('execution.title')}</h2><p style={{ color: '#888', marginBottom: 0 }}>{t('execution.description')}</p></div>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<Card><Tabs items={tabItems} /></Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 详情模态框 */}
|
|
|
|
|
|
<Modal
|
2026-06-13 12:44:48 +08:00
|
|
|
|
title={`${selectedRecord?.type}${t('common.detail')}`}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
open={detailModalVisible}
|
|
|
|
|
|
onCancel={() => setDetailModalVisible(false)}
|
|
|
|
|
|
width={900}
|
|
|
|
|
|
footer={
|
|
|
|
|
|
selectedRecord?.status === 'approved' || selectedRecord?.status === 'pending' ? (
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Button onClick={() => setDetailModalVisible(false)}>{t('execution.close')}</Button>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{fullDetail && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* 基本信息 */}
|
|
|
|
|
|
<Descriptions bordered column={2} size="small">
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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')}>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
{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>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.status')}>{getStatusTag(selectedRecord.status)}</Descriptions.Item>
|
|
|
|
|
|
<Descriptions.Item label={t('execution.subject')} span={2}>{selectedRecord.reason}</Descriptions.Item>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
|
|
|
|
|
|
{/* 付款申请特有字段 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{selectedRecord.type === t('execution.paymentApply') && (
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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')}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Descriptions.Item>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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')}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
{fullDetail.expense_type === 'project' && fullDetail.project_id && (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.relatedProject')}>{getProjectName(fullDetail.project_id)}</Descriptions.Item>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
)}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.expenseCategory')}>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
{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)
|
|
|
|
|
|
}
|
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 报销申请特有字段 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{selectedRecord.type === t('execution.reimburseApply') && fullDetail.expense_type && (
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.expenseType')}>
|
|
|
|
|
|
{fullDetail.expense_type === 'company' ? t('execution.companyExpense') : t('execution.projectExpense')}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
{fullDetail.project_id && (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.relatedProject')}>{getProjectName(fullDetail.project_id)}</Descriptions.Item>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 核销申请特有字段 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{selectedRecord.type === t('execution.verificationApply') && fullDetail.advance_code && (
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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')}>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<span style={{ fontWeight: 'bold', color: fullDetail.settlement ? '#52c41a' : '#fa8c16' }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{fullDetail.settlement ? t('common.is') : t('common.no')}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</span>
|
|
|
|
|
|
</Descriptions.Item>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
{fullDetail.settlement && fullDetail.settlement_amount && (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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)}`}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-03-28 00:34:32 +07:00
|
|
|
|
|
|
|
|
|
|
{/* 采购申请特有字段 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{selectedRecord.type === t('execution.purchaseApply') && (
|
2026-03-28 00:34:32 +07:00
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.purchaseType')}>
|
|
|
|
|
|
{fullDetail.purchase_type === 'project' ? t('execution.projectPurchase') : t('execution.stockPurchase')}
|
2026-03-28 00:34:32 +07:00
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
{fullDetail.purchase_type === 'project' && fullDetail.project_id && (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.relatedProject')}>{getProjectName(fullDetail.project_id)}</Descriptions.Item>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
)}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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')}
|
2026-03-28 00:34:32 +07:00
|
|
|
|
</Descriptions.Item>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
{fullDetail.remark && (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.remark')} span={2}>{fullDetail.remark}</Descriptions.Item>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Descriptions>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
|
|
|
|
|
|
{/* 采购申请供应商收款信息 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{selectedRecord.type === t('execution.purchaseApply') && fullDetail.supplier_payment_infos && fullDetail.supplier_payment_infos.length > 0 && (
|
2026-03-28 00:34:32 +07:00
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Divider>{t('execution.supplierPaymentInfo')}</Divider>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
<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}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
{payment.qr_code && (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Descriptions.Item label={t('execution.qrCode')}>
|
|
|
|
|
|
<img src={payment.qr_code} alt={t('execution.qrCode')} style={{ width: 100, height: 100, objectFit: 'contain' }} />
|
2026-03-28 00:34:32 +07:00
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 采购申请商品明细 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{selectedRecord.type === t('execution.purchaseApply') && fullDetail.items && fullDetail.items.length > 0 && (
|
2026-03-28 00:34:32 +07:00
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Divider>{t('execution.purchaseDetail')}</Divider>
|
2026-03-28 00:34:32 +07:00
|
|
|
|
<List
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
dataSource={fullDetail.items}
|
|
|
|
|
|
renderItem={(item: any, index: number) => (
|
|
|
|
|
|
<List.Item>
|
|
|
|
|
|
<div style={{ width: '100%' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
|
|
|
|
|
<span><strong>{index + 1}. {item.product_name}</strong></span>
|
|
|
|
|
|
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>
|
|
|
|
|
|
{fullDetail.currency} {item.total_price?.toLocaleString('zh-CN', {minimumFractionDigits: 2})}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: 13, color: '#666' }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{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})}
|
2026-03-28 00:34:32 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
|
|
|
|
|
|
{/* 明细清单 */}
|
|
|
|
|
|
{fullDetail.detail_items && fullDetail.detail_items.length > 0 && (
|
|
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Divider>{t('execution.detailList')}</Divider>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
{renderDetailItems(fullDetail.detail_items)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 审批意见 */}
|
|
|
|
|
|
{fullDetail.approval_remark && (
|
|
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Divider>{t('execution.approvalOpinion')}</Divider>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<div style={{ padding: '12px', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
|
|
|
|
<p style={{ margin: 0, color: '#666' }}>{fullDetail.approval_remark}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 申请凭证附件或退款凭证 */}
|
|
|
|
|
|
{fullDetail.attachments && fullDetail.attachments.length > 0 && (
|
|
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Divider>{fullDetail.settlement && fullDetail.settlement_amount > 0 ? t('execution.refundProof') : t('execution.applicationAttachment')}</Divider>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
{renderAttachments(fullDetail.attachments)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 执行表单 */}
|
|
|
|
|
|
{(selectedRecord.status === 'approved' || selectedRecord.status === 'pending') && (
|
|
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Divider>{t('execution.executionInfo')}</Divider>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<Form form={form} layout="vertical">
|
|
|
|
|
|
{/* 执行/确认日期 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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 }]}>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<DatePicker style={{ width: '100%' }} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 执行方式或收款方式 - 非结算核销不需要 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{!(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') }]} />
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
) : (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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') }]} />
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 只有退款类型的核销申请和非结算核销不需要上传付款凭证,其他类型的申请都需要 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{!(selectedRecord.type === t('execution.verificationApply') && (fullDetail.settlement && fullDetail.settlement_amount > 0 || !fullDetail.settlement)) && (
|
|
|
|
|
|
<Form.Item label={t('execution.proofOfPayment')} required>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<Upload {...uploadProps}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Button icon={<UploadOutlined />}>{t('execution.uploadProof')}</Button>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Upload>
|
|
|
|
|
|
<div style={{ marginTop: 8, color: '#666', fontSize: 12 }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{t('execution.proofUploadTip')}
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 退款的核销申请显示提示 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{selectedRecord.type === t('execution.verificationApply') && fullDetail.settlement && fullDetail.settlement_amount > 0 && (
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<div style={{ margin: '16px 0', padding: '12px', backgroundColor: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: '4px' }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<p style={{ margin: 0, color: '#389e0d' }}>{t('execution.noProofRefund')}</p>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 非结算核销的核销申请显示提示 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{selectedRecord.type === t('execution.verificationApply') && !fullDetail.settlement && (
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<div style={{ margin: '16px 0', padding: '12px', backgroundColor: '#e6f7ff', border: '1px solid #91d5ff', borderRadius: '4px' }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<p style={{ margin: 0, color: '#1890ff' }}>{t('execution.noProofNonSettlement')}</p>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 根据操作类型显示不同的字段 */}
|
|
|
|
|
|
{!isRejecting ? (
|
|
|
|
|
|
// 执行操作时显示的字段
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* 收款确认信息(仅退款类型)或备注 */}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
{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')} />
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
) : (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Form.Item name="remark" label={t('execution.remark')}>
|
|
|
|
|
|
<TextArea rows={2} placeholder={t('execution.remarkPlaceholder')} />
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
// 退回操作时显示的字段
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Form.Item name="rejectReason" label={t('execution.returnReason')} rules={[{ required: true, message: t('execution.rejectReasonRequired') }]}>
|
|
|
|
|
|
<TextArea rows={3} placeholder={t('execution.returnReason')} />
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Modal title={`${t('execution.edit')}:${selectedRecord?.code}`} open={editModalVisible} onCancel={() => setEditModalVisible(false)} onOk={handleEditSubmit} width={600}>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
<Form form={editForm} layout="vertical">
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<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>
|
2026-03-25 23:55:36 +07:00
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ExecutionManagement;
|