1853 lines
76 KiB
TypeScript
1853 lines
76 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
import { Tabs, Card, Descriptions, Button, Table, Tag, Progress, Space, message, Spin, Select, Menu, Dropdown, Modal, Form, Input, InputNumber, Switch, Upload, DatePicker } from 'antd'
|
|
import dayjs from 'dayjs'
|
|
import { DownOutlined, UploadOutlined, ToolOutlined } from '@ant-design/icons'
|
|
import {
|
|
ArrowLeftOutlined,
|
|
EditOutlined,
|
|
InfoCircleOutlined,
|
|
FileTextOutlined,
|
|
TeamOutlined,
|
|
DatabaseOutlined,
|
|
CheckCircleOutlined,
|
|
FileSearchOutlined,
|
|
DollarOutlined,
|
|
SafetyOutlined
|
|
} from '@ant-design/icons'
|
|
import apiClient from '../../utils/request'
|
|
|
|
const { TabPane } = Tabs
|
|
|
|
interface Project {
|
|
id: number
|
|
project_code: string
|
|
name: string
|
|
customer_id: number
|
|
customer_name: string
|
|
status: string
|
|
budget: string
|
|
spent: string
|
|
start_date: string
|
|
end_date: string
|
|
description: string
|
|
contract_type: string
|
|
contract_amount: string
|
|
currency: string
|
|
contract_days: number
|
|
project_manager_id: number
|
|
manager_name: string
|
|
location: string
|
|
work_quantity: string
|
|
project_situation: string
|
|
settlement_type: string
|
|
has_warranty: boolean
|
|
warranty_amount: string
|
|
warranty_percent: string
|
|
warranty_months: number
|
|
warranty_start_date: string
|
|
warranty_end_date: string
|
|
warranty_status: string
|
|
}
|
|
|
|
const ProjectDetail: React.FC = () => {
|
|
const { id } = useParams<{ id: string }>()
|
|
const navigate = useNavigate()
|
|
const [project, setProject] = useState<Project | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [activeTab, setActiveTab] = useState('basic')
|
|
const [isMobile, setIsMobile] = useState(false)
|
|
const [contractEditModalVisible, setContractEditModalVisible] = useState(false)
|
|
const [basicInfoEditModalVisible, setBasicInfoEditModalVisible] = useState(false)
|
|
const [contractForm] = Form.useForm()
|
|
const [basicInfoForm] = Form.useForm()
|
|
const [unitPriceItems, setUnitPriceItems] = useState([
|
|
{ key: '1', name: '项目1', unit: '个', quantity: 10, price: 100, total: 1000 },
|
|
{ key: '2', name: '项目2', unit: '米', quantity: 50, price: 20, total: 1000 }
|
|
])
|
|
const [contractTotal, setContractTotal] = useState(0)
|
|
const [settlementType, setSettlementType] = useState('lump_sum')
|
|
const [paymentNodes, setPaymentNodes] = useState([
|
|
{ key: '1', name: '预付款', condition: '合同签订后7天内', percentage: 40, amount: 0, status: 'pending' },
|
|
{ key: '2', name: '进度款', condition: '所有设备材料达到现场', percentage: 30, amount: 0, status: 'pending' },
|
|
{ key: '3', name: '尾款', condition: '通电完工7天内', percentage: 27, amount: 0, status: 'pending' },
|
|
{ key: '4', name: '质保金', condition: '', percentage: 3, amount: 0, status: 'pending' }
|
|
])
|
|
const [contractFile, setContractFile] = useState<string>('')
|
|
const [contracts, setContracts] = useState<any[]>([])
|
|
const [subcontracts, setSubcontracts] = useState<any[]>([])
|
|
const [materials, setMaterials] = useState<any[]>([])
|
|
const [milestones, setMilestones] = useState<any[]>([])
|
|
const [finances, setFinances] = useState<any[]>([])
|
|
const [warrantyDeposits, setWarrantyDeposits] = useState<any[]>([])
|
|
const [constructionLogs, setConstructionLogs] = useState<any[]>([])
|
|
const [subcontractModalVisible, setSubcontractModalVisible] = useState(false)
|
|
const [subcontractForm] = Form.useForm()
|
|
const [subcontractors, setSubcontractors] = useState<any[]>([])
|
|
const [users, setUsers] = useState<any[]>([])
|
|
const [usersLoading, setUsersLoading] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const checkMobile = () => {
|
|
setIsMobile(window.innerWidth <= 768)
|
|
}
|
|
|
|
checkMobile()
|
|
window.addEventListener('resize', checkMobile)
|
|
return () => window.removeEventListener('resize', checkMobile)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetchProject()
|
|
}, [id])
|
|
|
|
// 获取用户列表
|
|
useEffect(() => {
|
|
const fetchUsers = async () => {
|
|
setUsersLoading(true)
|
|
try {
|
|
const response = await apiClient.get('/users')
|
|
const data = response.data
|
|
if (data.success) {
|
|
setUsers(data.data)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取用户列表失败:', error)
|
|
} finally {
|
|
setUsersLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchUsers()
|
|
}, [])
|
|
|
|
// 当project加载完成后,设置合同相关的默认值
|
|
useEffect(() => {
|
|
if (project) {
|
|
setSettlementType(project.settlement_type || 'lump_sum')
|
|
setContractTotal(parseFloat(project.contract_amount || '0'))
|
|
|
|
// 初始化付款节点金额
|
|
const initialNodes = paymentNodes.map(node => ({
|
|
...node,
|
|
amount: Math.round((parseFloat(project.contract_amount || '0') * node.percentage) / 100)
|
|
}))
|
|
setPaymentNodes(initialNodes)
|
|
}
|
|
}, [project])
|
|
|
|
// 当contracts数据加载完成后,设置合同表单默认值
|
|
useEffect(() => {
|
|
if (project && contracts.length === 0) {
|
|
// 使用setTimeout确保Form组件已经渲染
|
|
setTimeout(() => {
|
|
contractForm.setFieldsValue({
|
|
project_overview: project?.project_situation || project?.description || '无',
|
|
settlement_type: project.settlement_type || 'lump_sum',
|
|
contract_total: parseFloat(project.contract_amount || '0'),
|
|
tax_included: false,
|
|
other_info: ''
|
|
})
|
|
}, 0)
|
|
}
|
|
}, [project, contractForm, contracts.length])
|
|
|
|
// 当project加载完成后,设置基本信息表单默认值
|
|
useEffect(() => {
|
|
if (project) {
|
|
// 使用setTimeout确保Form组件已经渲染
|
|
setTimeout(() => {
|
|
// 优先使用project.contract_days作为工期天数
|
|
let workDays = project.contract_days || 0;
|
|
|
|
basicInfoForm.setFieldsValue({
|
|
name: project.name,
|
|
manager_id: Number(project.manager_id || project.project_manager_id || 0) || undefined,
|
|
location: project.location,
|
|
start_date: project.start_date ? dayjs(project.start_date) : null,
|
|
end_date: project.end_date ? dayjs(project.end_date) : null,
|
|
work_days: workDays,
|
|
description: project.project_situation || project.description || ''
|
|
})
|
|
}, 0)
|
|
}
|
|
}, [project, basicInfoForm])
|
|
|
|
// 当contracts数据加载完成后,更新表单默认值
|
|
useEffect(() => {
|
|
if (contracts.length > 0) {
|
|
const contract = contracts[0]
|
|
setSettlementType(contract.settlement_method || 'lump_sum')
|
|
setContractTotal(parseFloat(contract.contract_amount || '0'))
|
|
setContractFile(contract.contract_file || '')
|
|
|
|
// 使用setTimeout确保Form组件已经渲染
|
|
setTimeout(() => {
|
|
// 更新表单默认值
|
|
contractForm.setFieldsValue({
|
|
project_overview: project?.project_situation || project?.description || '无',
|
|
settlement_type: contract.settlement_method || 'lump_sum',
|
|
contract_total: parseFloat(contract.contract_amount || '0'),
|
|
tax_included: contract.tax_included || false,
|
|
other_info: contract.other_info || ''
|
|
})
|
|
}, 0)
|
|
|
|
// 更新付款节点金额
|
|
const updatedNodes = paymentNodes.map(node => ({
|
|
...node,
|
|
amount: Math.round((parseFloat(contract.contract_amount || '0') * node.percentage) / 100)
|
|
}))
|
|
setPaymentNodes(updatedNodes)
|
|
}
|
|
}, [contracts, project, contractForm])
|
|
|
|
// 处理文件上传
|
|
const handleFileUpload = async (options: any) => {
|
|
const { file, onSuccess, onError } = options
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
const response = await apiClient.post('/upload/single', formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
}
|
|
})
|
|
|
|
if (response.data.success) {
|
|
setContractFile(response.data.data.url)
|
|
onSuccess(response.data.data)
|
|
} else {
|
|
onError(new Error('文件上传失败'))
|
|
}
|
|
} catch (error) {
|
|
console.error('文件上传失败:', error)
|
|
onError(error)
|
|
}
|
|
}
|
|
|
|
// 打开新增分包模态框
|
|
const openSubcontractModal = () => {
|
|
subcontractForm.resetFields()
|
|
// 设置默认的单价项目列表
|
|
setUnitPriceItems([
|
|
{ key: '1', name: '线路架设', unit: 'km', quantity: 1, price: 0, total: 0 },
|
|
{ key: '2', name: '手续办理', unit: '项', quantity: 1, price: 0, total: 0 }
|
|
]);
|
|
// 设置开始日期为当天
|
|
subcontractForm.setFieldsValue({ start_date: dayjs() });
|
|
setSubcontractModalVisible(true)
|
|
}
|
|
|
|
// 关闭新增分包模态框
|
|
const closeSubcontractModal = () => {
|
|
setSubcontractModalVisible(false)
|
|
}
|
|
|
|
// 添加单价项目
|
|
const addUnitPriceItem = () => {
|
|
setUnitPriceItems(prevItems => {
|
|
const newItem = {
|
|
key: (prevItems.length + 1).toString(),
|
|
name: '',
|
|
unit: '',
|
|
quantity: 0,
|
|
price: 0,
|
|
total: 0
|
|
};
|
|
return [...prevItems, newItem];
|
|
});
|
|
};
|
|
|
|
// 删除单价项目
|
|
const removeUnitPriceItem = (index: number) => {
|
|
setUnitPriceItems(prevItems => {
|
|
const newItems = [...prevItems];
|
|
newItems.splice(index, 1);
|
|
// 更新key值
|
|
newItems.forEach((item, i) => {
|
|
item.key = (i + 1).toString();
|
|
});
|
|
return newItems;
|
|
});
|
|
};
|
|
|
|
// 提交新增分包表单
|
|
const handleSubcontractSubmit = async () => {
|
|
try {
|
|
const values = await subcontractForm.validateFields()
|
|
|
|
let contractAmount = values.contract_amount;
|
|
|
|
// 如果是单价结算,自动计算合同金额
|
|
if (values.settlement_type === 'unit_price') {
|
|
const totalAmount = unitPriceItems.reduce((sum, item) => sum + (item.total || 0), 0);
|
|
contractAmount = totalAmount;
|
|
}
|
|
|
|
const response = await apiClient.post(`/projects/${id}/subcontracts`, {
|
|
subcontractor_id: values.subcontractor_id,
|
|
subcontractor_name: values.subcontractor_name,
|
|
contract_amount: contractAmount,
|
|
currency: values.currency,
|
|
settlement_type: values.settlement_type,
|
|
other_terms: values.other_terms,
|
|
payment_description: values.payment_description,
|
|
unit_price_items: values.settlement_type === 'unit_price' ? unitPriceItems : [],
|
|
start_date: values.start_date ? values.start_date.toISOString() : null,
|
|
end_date: values.end_date ? values.end_date.toISOString() : null,
|
|
work_days: values.work_days,
|
|
status: values.status
|
|
})
|
|
|
|
if (response.data.success) {
|
|
message.success('新增分包成功')
|
|
// 重新获取分包列表
|
|
fetchProjectData()
|
|
closeSubcontractModal()
|
|
// 重置单价项目列表
|
|
setUnitPriceItems([
|
|
{ key: '1', name: '项目1', unit: '个', quantity: 10, price: 100, total: 1000 },
|
|
{ key: '2', name: '项目2', unit: '米', quantity: 50, price: 20, total: 1000 }
|
|
]);
|
|
} else {
|
|
message.error('新增分包失败:' + response.data.message)
|
|
}
|
|
} catch (error) {
|
|
console.error('新增分包失败:', error)
|
|
message.error('新增分包失败,请检查表单数据')
|
|
}
|
|
}
|
|
|
|
const fetchProject = async () => {
|
|
try {
|
|
const response = await apiClient.get(`/projects/${id}`)
|
|
if (response.data.success) {
|
|
setProject(response.data.data)
|
|
}
|
|
} catch (error) {
|
|
message.error('获取项目信息失败')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const fetchProjectData = async () => {
|
|
try {
|
|
// 获取合同信息
|
|
const contractsResponse = await apiClient.get(`/projects/${id}/contracts`)
|
|
if (contractsResponse.data.success) {
|
|
setContracts(contractsResponse.data.data)
|
|
}
|
|
|
|
// 获取分包信息
|
|
const subcontractsResponse = await apiClient.get(`/projects/${id}/subcontracts`)
|
|
if (subcontractsResponse.data.success) {
|
|
setSubcontracts(subcontractsResponse.data.data)
|
|
}
|
|
|
|
// 获取材料信息
|
|
const materialsResponse = await apiClient.get(`/projects/${id}/materials`)
|
|
if (materialsResponse.data.success) {
|
|
setMaterials(materialsResponse.data.data)
|
|
}
|
|
|
|
// 获取施工节点
|
|
const milestonesResponse = await apiClient.get(`/projects/${id}/milestones`)
|
|
if (milestonesResponse.data.success) {
|
|
setMilestones(milestonesResponse.data.data)
|
|
}
|
|
|
|
// 获取财务信息
|
|
const financesResponse = await apiClient.get(`/projects/${id}/finances`)
|
|
if (financesResponse.data.success) {
|
|
setFinances(financesResponse.data.data)
|
|
}
|
|
|
|
// 获取质保金信息
|
|
const warrantyDepositsResponse = await apiClient.get(`/projects/${id}/warranty-deposits`)
|
|
if (warrantyDepositsResponse.data.success) {
|
|
setWarrantyDeposits(warrantyDepositsResponse.data.data)
|
|
}
|
|
|
|
// 获取施工日志
|
|
const constructionLogsResponse = await apiClient.get(`/projects/${id}/construction-logs`)
|
|
if (constructionLogsResponse.data.success) {
|
|
setConstructionLogs(constructionLogsResponse.data.data)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取项目数据失败:', error)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (id) {
|
|
fetchProject()
|
|
fetchSubcontractors()
|
|
fetchProjectData()
|
|
}
|
|
}, [id])
|
|
|
|
// 获取分包商列表
|
|
const fetchSubcontractors = async () => {
|
|
try {
|
|
const response = await apiClient.get('/subcontractors')
|
|
if (response.data.success) {
|
|
setSubcontractors(response.data.data)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取分包商列表失败:', error)
|
|
}
|
|
}
|
|
|
|
// 监听单价项目变更,更新合同总价
|
|
useEffect(() => {
|
|
calculateContractTotal()
|
|
}, [unitPriceItems])
|
|
|
|
// 监听结算方式变更,更新合同总价字段状态
|
|
useEffect(() => {
|
|
if (settlementType === 'unit_price') {
|
|
calculateContractTotal()
|
|
}
|
|
}, [settlementType])
|
|
|
|
const getStatusTag = (status: string) => {
|
|
const statusMap: Record<string, { color: string; text: string }> = {
|
|
planning: { color: 'blue', text: '规划中' },
|
|
in_progress: { color: 'processing', text: '进行中' },
|
|
active: { color: 'processing', text: '进行中' },
|
|
completed: { color: 'success', text: '已完成' },
|
|
suspended: { color: 'warning', text: '已暂停' },
|
|
}
|
|
const config = statusMap[status] || { color: 'default', text: status }
|
|
return <Tag color={config.color}>{config.text}</Tag>
|
|
}
|
|
|
|
// 计算合同总价
|
|
const calculateContractTotal = () => {
|
|
const total = unitPriceItems.reduce((sum, item) => sum + (item.total || 0), 0)
|
|
setContractTotal(total)
|
|
}
|
|
|
|
// 处理单价项目变更
|
|
const handleUnitPriceItemChange = (index: number, field: string, value: any) => {
|
|
const newItems = [...unitPriceItems]
|
|
newItems[index] = { ...newItems[index], [field]: value }
|
|
|
|
// 计算单项总价
|
|
if (field === 'quantity' || field === 'price') {
|
|
newItems[index].total = (newItems[index].quantity || 0) * (newItems[index].price || 0)
|
|
}
|
|
|
|
setUnitPriceItems(newItems)
|
|
calculateContractTotal()
|
|
|
|
// 当单价项目变化时,自动更新合同金额字段
|
|
const settlementType = subcontractForm.getFieldValue('settlement_type');
|
|
if (settlementType === 'unit_price') {
|
|
const totalAmount = newItems.reduce((sum, item) => sum + (item.total || 0), 0);
|
|
subcontractForm.setFieldsValue({ contract_amount: totalAmount });
|
|
}
|
|
}
|
|
|
|
// 处理结算方式变更
|
|
const handleSettlementTypeChange = (value: string) => {
|
|
setSettlementType(value)
|
|
contractForm.setFieldsValue({ settlement_type: value })
|
|
}
|
|
|
|
// 处理付款节点比例变化
|
|
const handlePaymentNodePercentageChange = (index: number, value: number) => {
|
|
const newNodes = [...paymentNodes]
|
|
newNodes[index].percentage = value
|
|
newNodes[index].amount = Math.round((contractTotal * value) / 100)
|
|
setPaymentNodes(newNodes)
|
|
const total = newNodes.reduce((sum, n) => sum + (n.percentage || 0), 0)
|
|
if (total > 100) {
|
|
message.warning(`付款比例合计为 ${total}%,已超过 100%,请调整`)
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 400 }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!project) {
|
|
return (
|
|
<Card>
|
|
<div style={{ textAlign: 'center', padding: 40 }}>
|
|
项目不存在或已被删除
|
|
<Button type="link" onClick={() => navigate('/projects')}>返回列表</Button>
|
|
</div>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// 基本信息 Tab
|
|
const BasicInfoTab = () => {
|
|
// 查找项目经理名称
|
|
const getManagerName = () => {
|
|
// 优先使用manager_name
|
|
if (project.manager_name) return project.manager_name;
|
|
// 尝试使用project_manager_id
|
|
if (project.project_manager_id) {
|
|
const user = users.find(u => Number(u.id) === Number(project.project_manager_id));
|
|
return user ? user.name : '未知经理';
|
|
}
|
|
// 尝试使用manager_id(后端使用的字段)
|
|
if (project.manager_id) {
|
|
const user = users.find(u => Number(u.id) === Number(project.manager_id));
|
|
return user ? user.name : '未知经理';
|
|
}
|
|
return '未知经理';
|
|
};
|
|
|
|
return (
|
|
<Card title="项目基本信息" extra={<Button icon={<EditOutlined />} onClick={() => setBasicInfoEditModalVisible(true)}>编辑</Button>}>
|
|
<Descriptions column={2} bordered>
|
|
<Descriptions.Item label="项目编号">{project.project_code}</Descriptions.Item>
|
|
<Descriptions.Item label="项目名称">{project.name}</Descriptions.Item>
|
|
<Descriptions.Item label="客户">{project.customer_name}</Descriptions.Item>
|
|
<Descriptions.Item label="项目经理">{getManagerName()}</Descriptions.Item>
|
|
<Descriptions.Item label="项目地点">{project.location || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="项目状态">{getStatusTag(project.status)}</Descriptions.Item>
|
|
<Descriptions.Item label="开工日期">{project.start_date?.split('T')[0] || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="完工日期">{project.end_date?.split('T')[0] || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="合同工期">{project.contract_days} 天</Descriptions.Item>
|
|
<Descriptions.Item label="结算方式">
|
|
{project.settlement_type === 'lump_sum' ? '总价包干' : '单价结算'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="工程概况" span={2}>
|
|
{project.project_situation || project.description || '-'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="创建时间" span={2}>
|
|
{new Date().toLocaleDateString()}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// 合同详情 Tab
|
|
const ContractTab = () => {
|
|
// 获取最新的合同信息
|
|
const latestContract = contracts.length > 0 ? contracts[0] : null;
|
|
|
|
return (
|
|
<Card title="合同详情" extra={<Button icon={<EditOutlined />} onClick={() => {
|
|
const latestContract = contracts.length > 0 ? contracts[0] : null;
|
|
const savedSettlement = latestContract?.settlement_method || 'lump_sum';
|
|
const savedTotal = parseFloat(latestContract?.contract_amount || project?.contract_amount || '0');
|
|
setSettlementType(savedSettlement);
|
|
setContractTotal(savedTotal);
|
|
setContractFile(latestContract?.contract_file || '');
|
|
if (milestones.length > 0) {
|
|
setPaymentNodes(milestones.map((m: any) => ({
|
|
key: String(m.id),
|
|
name: m.milestone_name || '',
|
|
condition: m.condition || '',
|
|
percentage: m.percentage || 0,
|
|
amount: m.amount || 0,
|
|
status: m.status || 'pending'
|
|
})));
|
|
} else {
|
|
setPaymentNodes([
|
|
{ key: '1', name: '预付款', condition: '合同签订后7天内', percentage: 40, amount: Math.round(savedTotal * 0.4), status: 'pending' },
|
|
{ key: '2', name: '进度款', condition: '所有设备材料达到现场', percentage: 30, amount: Math.round(savedTotal * 0.3), status: 'pending' },
|
|
{ key: '3', name: '尾款', condition: '通电完工7天内', percentage: 27, amount: Math.round(savedTotal * 0.27), status: 'pending' },
|
|
{ key: '4', name: '质保金', condition: '', percentage: 3, amount: Math.round(savedTotal * 0.03), status: 'pending' }
|
|
]);
|
|
}
|
|
setContractEditModalVisible(true);
|
|
setTimeout(() => {
|
|
contractForm.setFieldsValue({
|
|
project_overview: project?.project_situation || project?.description || '',
|
|
settlement_type: savedSettlement,
|
|
contract_total: savedTotal,
|
|
tax_included: latestContract?.tax_included || false,
|
|
other_info: latestContract?.other_info || ''
|
|
});
|
|
}, 100);
|
|
}}>合同细节录入</Button>}>
|
|
<Descriptions column={2} bordered style={{ marginBottom: 24 }}>
|
|
<Descriptions.Item label="合同金额">
|
|
{project.currency} {parseFloat(latestContract?.contract_amount || project.contract_amount || '0').toLocaleString()}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="币种">{project.currency}</Descriptions.Item>
|
|
<Descriptions.Item label="合同类型">
|
|
{project.contract_type === 'lump_sum' ? '总价包干' : '单价合同'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="结算方式">
|
|
{latestContract?.settlement_method === 'lump_sum' ? '总价包干' : '单价结算'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="是否含税">
|
|
{latestContract?.tax_included ? '是' : '否'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="合同编号">
|
|
{latestContract?.contract_code || '-'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
|
|
<Card type="inner" title="付款节点" style={{ marginBottom: 24 }}>
|
|
<Table
|
|
dataSource={milestones.map(item => ({
|
|
...item,
|
|
key: item.id
|
|
}))}
|
|
columns={[
|
|
{ title: '节点名称', dataIndex: 'milestone_name', key: 'milestone_name' },
|
|
{ title: '节点条件', dataIndex: 'condition', key: 'condition', render: (v: string) => v || '-' },
|
|
{ title: '比例', dataIndex: 'percentage', key: 'percentage', render: (v: number) => `${v}%` },
|
|
{ title: '金额', dataIndex: 'amount', key: 'amount', render: (v: number) => `¥${v.toLocaleString()}` },
|
|
{ title: '完成进度', dataIndex: 'completion_progress', key: 'completion_progress', render: (v: number) => <Progress percent={v} size="small" /> },
|
|
{ title: '状态', dataIndex: 'status', key: 'status', render: (v: string) => <Tag color={v === 'completed' ? 'success' : v === 'in_progress' ? 'processing' : 'default'}>{v === 'completed' ? '已完成' : v === 'in_progress' ? '进行中' : '待开始'}</Tag> },
|
|
]}
|
|
locale={{ emptyText: '暂无节点记录' }}
|
|
/>
|
|
</Card>
|
|
|
|
<Card type="inner" title="合同附件" style={{ marginBottom: 24 }}>
|
|
<div>
|
|
{latestContract ? (
|
|
<div style={{ marginBottom: 16 }}>
|
|
<Descriptions column={1}>
|
|
<Descriptions.Item label="合同编号">{latestContract.contract_code}</Descriptions.Item>
|
|
<Descriptions.Item label="合同文件">
|
|
{latestContract.contract_file ? (
|
|
<a href={latestContract.contract_file} target="_blank" rel="noopener noreferrer">
|
|
查看合同文件
|
|
</a>
|
|
) : (
|
|
'-'
|
|
)}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</div>
|
|
) : (
|
|
<div>暂无合同附件</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card type="inner" title="其他合同信息" style={{ marginBottom: 24 }}>
|
|
<Descriptions column={1}>
|
|
<Descriptions.Item label="其他信息">
|
|
{latestContract?.other_info || '-'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Card>
|
|
|
|
<Card type="inner" title="质保金设置">
|
|
<Descriptions column={2}>
|
|
<Descriptions.Item label="是否有质保金">
|
|
{milestones.find(m => m.milestone_name === '质保金') ? '是' : '否'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="质保金比例">
|
|
{milestones.find(m => m.milestone_name === '质保金')?.percentage || 0}%
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="质保金金额">
|
|
¥{(milestones.find(m => m.milestone_name === '质保金')?.amount || 0).toLocaleString()}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="质保期限">{project.warranty_months} 个月</Descriptions.Item>
|
|
<Descriptions.Item label="到期日期">{project.warranty_end_date?.split('T')[0] || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="质保金状态">
|
|
<Tag color={project.warranty_status === 'released' ? 'success' : 'default'}>
|
|
{project.warranty_status === 'released' ? '已释放' : '待释放'}
|
|
</Tag>
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Card>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// 分包管理 Tab
|
|
const SubcontractTab = () => {
|
|
const [subcontractDetailModalVisible, setSubcontractDetailModalVisible] = useState(false);
|
|
const [selectedSubcontract, setSelectedSubcontract] = useState<any>(null);
|
|
|
|
const openSubcontractDetail = (subcontract: any) => {
|
|
setSelectedSubcontract(subcontract);
|
|
setSubcontractDetailModalVisible(true);
|
|
};
|
|
|
|
const closeSubcontractDetail = () => {
|
|
setSubcontractDetailModalVisible(false);
|
|
setSelectedSubcontract(null);
|
|
};
|
|
|
|
const getCurrencySymbol = (currency: string) => {
|
|
const symbols: Record<string, string> = {
|
|
CNY: '¥',
|
|
USD: '$',
|
|
LAK: '₭',
|
|
THB: '฿'
|
|
};
|
|
return symbols[currency] || '';
|
|
};
|
|
|
|
return (
|
|
<Card title="分包管理" extra={<Button type="primary" onClick={openSubcontractModal}>新增分包</Button>}>
|
|
<Table
|
|
dataSource={subcontracts.map(item => ({
|
|
...item,
|
|
key: item.id
|
|
}))}
|
|
columns={[
|
|
{
|
|
title: '分包商',
|
|
dataIndex: 'subcontractor_name',
|
|
key: 'subcontractor_name',
|
|
render: (text: string, record: any) => (
|
|
<a onClick={() => openSubcontractDetail(record)}>{text}</a>
|
|
)
|
|
},
|
|
{
|
|
title: '合同金额',
|
|
dataIndex: 'contract_amount',
|
|
key: 'contract_amount',
|
|
render: (v: number, record: any) => {
|
|
const symbol = getCurrencySymbol(record.currency);
|
|
return `${symbol}${v.toLocaleString()}`;
|
|
}
|
|
},
|
|
{
|
|
title: '已付款',
|
|
dataIndex: 'paid_amount',
|
|
key: 'paid_amount',
|
|
render: (v: number, record: any) => {
|
|
const symbol = getCurrencySymbol(record.currency);
|
|
return `${symbol}${v.toLocaleString()}`;
|
|
}
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
render: (v: string) => <Tag color={v === 'completed' ? 'success' : 'default'}>{v === 'completed' ? '已完成' : '进行中'}</Tag>
|
|
},
|
|
]}
|
|
locale={{ emptyText: '暂无分包记录' }}
|
|
/>
|
|
|
|
{/* 分包详情模态框 */}
|
|
<Modal
|
|
title="分包详情"
|
|
open={subcontractDetailModalVisible}
|
|
onCancel={closeSubcontractDetail}
|
|
width={800}
|
|
footer={[
|
|
<Button key="close" onClick={closeSubcontractDetail}>关闭</Button>
|
|
]}
|
|
>
|
|
{selectedSubcontract && (
|
|
<div style={{ padding: 20 }}>
|
|
<Descriptions column={2} bordered>
|
|
<Descriptions.Item label="分包商">{selectedSubcontract.subcontractor_name}</Descriptions.Item>
|
|
<Descriptions.Item label="合同金额">
|
|
{getCurrencySymbol(selectedSubcontract.currency)}{selectedSubcontract.contract_amount.toLocaleString()}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="币种">{selectedSubcontract.currency}</Descriptions.Item>
|
|
<Descriptions.Item label="结算方式">
|
|
{selectedSubcontract.settlement_type === 'lump_sum' ? '总价包干' : '单价结算'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="已付款">
|
|
{getCurrencySymbol(selectedSubcontract.currency)}{selectedSubcontract.paid_amount.toLocaleString()}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="状态">
|
|
<Tag color={selectedSubcontract.status === 'completed' ? 'success' : 'default'}>
|
|
{selectedSubcontract.status === 'completed' ? '已完成' : '进行中'}
|
|
</Tag>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="开始日期">
|
|
{selectedSubcontract.start_date ? selectedSubcontract.start_date.split('T')[0] : '-'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="结束日期">
|
|
{selectedSubcontract.end_date ? selectedSubcontract.end_date.split('T')[0] : '-'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="其他约定" span={2}>
|
|
{selectedSubcontract.other_terms || '-'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="付款说明" span={2}>
|
|
{selectedSubcontract.payment_description || '-'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
|
|
{/* 单价项目列表 */}
|
|
{selectedSubcontract.settlement_type === 'unit_price' && selectedSubcontract.unit_price_items && selectedSubcontract.unit_price_items.length > 0 && (
|
|
<Card type="inner" title="项目单项价" style={{ marginTop: 20 }}>
|
|
<Table
|
|
dataSource={selectedSubcontract.unit_price_items.map((item: any, index: number) => ({
|
|
...item,
|
|
key: index
|
|
}))}
|
|
columns={[
|
|
{ title: '项目名称', dataIndex: 'name', key: 'name' },
|
|
{ title: '单位', dataIndex: 'unit', key: 'unit' },
|
|
{ title: '数量', dataIndex: 'quantity', key: 'quantity' },
|
|
{
|
|
title: '单价',
|
|
dataIndex: 'price',
|
|
key: 'price',
|
|
render: (v: number) => {
|
|
const symbol = getCurrencySymbol(selectedSubcontract.currency);
|
|
return `${symbol}${v.toLocaleString()}`;
|
|
}
|
|
},
|
|
{
|
|
title: '总价',
|
|
dataIndex: 'total',
|
|
key: 'total',
|
|
render: (v: number) => {
|
|
const symbol = getCurrencySymbol(selectedSubcontract.currency);
|
|
return `${symbol}${v.toLocaleString()}`;
|
|
}
|
|
},
|
|
]}
|
|
pagination={false}
|
|
/>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// 材料管理 Tab
|
|
const MaterialTab = () => (
|
|
<Card title="材料管理">
|
|
<Table
|
|
dataSource={materials.map(item => ({
|
|
...item,
|
|
key: item.id
|
|
}))}
|
|
columns={[
|
|
{ title: '材料名称', dataIndex: 'product_name', key: 'product_name' },
|
|
{ title: '单位', dataIndex: 'unit', key: 'unit' },
|
|
{ title: '预算量', dataIndex: 'budget_quantity', key: 'budget_quantity' },
|
|
{ title: '采购量', dataIndex: 'purchase_quantity', key: 'purchase_quantity' },
|
|
{ title: '使用量', dataIndex: 'used_quantity', key: 'used_quantity' },
|
|
{ title: '均价', dataIndex: 'average_price', key: 'average_price', render: (v: number) => `¥${v.toLocaleString()}` },
|
|
{ title: '总价', dataIndex: 'total_amount', key: 'total_amount', render: (v: number) => `¥${v.toLocaleString()}` },
|
|
]}
|
|
locale={{ emptyText: '暂无材料记录' }}
|
|
/>
|
|
</Card>
|
|
)
|
|
|
|
// 施工节点 Tab
|
|
const MilestoneTab = () => (
|
|
<Card title="施工节点">
|
|
<Card type="inner" title="合同付款节点" style={{ marginBottom: 16 }}>
|
|
<Table
|
|
dataSource={milestones.map(item => ({
|
|
...item,
|
|
key: item.id
|
|
}))}
|
|
columns={[
|
|
{ title: '节点名称', dataIndex: 'milestone_name', key: 'milestone_name' },
|
|
{ title: '节点条件', dataIndex: 'condition', key: 'condition', render: (v: string) => v || '-' },
|
|
{ title: '比例', dataIndex: 'percentage', key: 'percentage', render: (v: number) => `${v}%` },
|
|
{ title: '金额', dataIndex: 'amount', key: 'amount', render: (v: number) => `¥${v.toLocaleString()}` },
|
|
{ title: '完成进度', dataIndex: 'completion_progress', key: 'completion_progress', render: (v: number) => <Progress percent={v} size="small" /> },
|
|
{ title: '状态', dataIndex: 'status', key: 'status', render: (v: string) => <Tag color={v === 'completed' ? 'success' : v === 'in_progress' ? 'processing' : 'default'}>{v === 'completed' ? '已完成' : v === 'in_progress' ? '进行中' : '待开始'}</Tag> },
|
|
]}
|
|
locale={{ emptyText: '暂无节点记录' }}
|
|
/>
|
|
</Card>
|
|
<Card type="inner" title="重要节点完成情况">
|
|
<Table
|
|
dataSource={milestones.map(item => ({
|
|
...item,
|
|
key: item.id
|
|
}))}
|
|
columns={[
|
|
{ title: '节点名称', dataIndex: 'milestone_name', key: 'milestone_name' },
|
|
{ title: '计划日期', dataIndex: 'expected_date', key: 'expected_date' },
|
|
{ title: '实际日期', dataIndex: 'actual_date', key: 'actual_date' },
|
|
{ title: '状态', dataIndex: 'status', key: 'status', render: (v: string) => <Tag color={v === 'completed' ? 'success' : v === 'in_progress' ? 'processing' : 'default'}>{v === 'completed' ? '已完成' : v === 'in_progress' ? '进行中' : '待开始'}</Tag> },
|
|
{ title: '操作', key: 'action', render: () => <Button size="small">上传凭证</Button> },
|
|
]}
|
|
locale={{ emptyText: '暂无记录' }}
|
|
/>
|
|
</Card>
|
|
</Card>
|
|
)
|
|
|
|
// 施工日志 Tab
|
|
const LogTab = () => (
|
|
<Card title="施工日志" extra={<Button type="primary">新增日志</Button>}>
|
|
<Table
|
|
dataSource={constructionLogs.map(item => ({
|
|
...item,
|
|
key: item.id
|
|
}))}
|
|
columns={[
|
|
{ title: '日期', dataIndex: 'log_date', key: 'log_date' },
|
|
{ title: '天气', dataIndex: 'weather', key: 'weather' },
|
|
{ title: '记录人', dataIndex: 'recorder', key: 'recorder', render: () => '系统管理员' },
|
|
{ title: '今日工作', dataIndex: 'work_content', key: 'work_content' },
|
|
{ title: '照片', dataIndex: 'photos', key: 'photos', render: (v: string) => v ? '查看照片' : '-' },
|
|
]}
|
|
locale={{ emptyText: '暂无日志记录' }}
|
|
/>
|
|
</Card>
|
|
)
|
|
|
|
const FinanceTab = () => {
|
|
const [profitData, setProfitData] = React.useState<any>(null);
|
|
const [profitLoading, setProfitLoading] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
if (!project?.id) return;
|
|
setProfitLoading(true);
|
|
apiClient.get(`/financial-records/project-profit/${project.id}`)
|
|
.then(res => { if (res.data.success) setProfitData(res.data.data); })
|
|
.catch(() => {})
|
|
.finally(() => setProfitLoading(false));
|
|
}, [project?.id]);
|
|
|
|
const contractAmount = parseFloat(project?.contract_amount || '0');
|
|
const totalIncome = profitData?.total_income || 0;
|
|
const totalExpense = profitData?.total_expense || 0;
|
|
const grossProfit = profitData?.gross_profit || 0;
|
|
const grossMargin = profitData?.gross_margin || 0;
|
|
|
|
const LEVEL2_LABELS: Record<string, string> = {
|
|
material: '材料采购', equipment: '设备采购', subcontract: '施工分包', labor: '人工工资',
|
|
travel: '差旅交通', accommodation: '食宿费用', freight: '运输物流', design: '勘测设计',
|
|
tools: '小型工具', client_relations: '客户/EDL关系', other_project: '其他项目支出',
|
|
contract_payment: '合同收款', deposit_refund: '质保金退回'
|
|
};
|
|
|
|
return (
|
|
<Spin spinning={profitLoading}>
|
|
<Card title="财务信息">
|
|
<Descriptions column={2} bordered>
|
|
<Descriptions.Item label="合同金额">¥{contractAmount.toLocaleString()}</Descriptions.Item>
|
|
<Descriptions.Item label="已收款">
|
|
<span style={{ color: '#3f8600', fontWeight: 'bold' }}>¥{totalIncome.toLocaleString()}</span>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="支出合计">
|
|
<span style={{ color: '#cf1322' }}>¥{totalExpense.toLocaleString()}</span>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="毛利润">
|
|
<span style={{ color: grossProfit >= 0 ? '#52c41a' : '#ff4d4f', fontWeight: 'bold', fontSize: 16 }}>
|
|
¥{grossProfit.toLocaleString()}
|
|
</span>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="毛利率" span={2}>
|
|
<span style={{ color: grossProfit >= 0 ? '#52c41a' : '#ff4d4f', fontWeight: 'bold' }}>
|
|
{grossMargin}%
|
|
</span>
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Card>
|
|
|
|
{profitData?.expense_by_category?.length > 0 && (
|
|
<Card title="支出分类明细" size="small" style={{ marginTop: 16 }}>
|
|
<Table
|
|
dataSource={profitData.expense_by_category}
|
|
rowKey="category_level2"
|
|
pagination={false}
|
|
size="small"
|
|
columns={[
|
|
{ title: '分类', dataIndex: 'category_level2', render: (v: string) => LEVEL2_LABELS[v] || v },
|
|
{ title: '金额(¥)', dataIndex: 'total_amount', render: (v: number) => parseFloat(v).toLocaleString(), align: 'right' as const },
|
|
{ title: '笔数', dataIndex: 'count', align: 'center' as const },
|
|
{
|
|
title: '占比', render: (_: unknown, r: any) => {
|
|
const pct = totalExpense > 0 ? (parseFloat(r.total_amount) / totalExpense * 100).toFixed(1) : '0';
|
|
return `${pct}%`;
|
|
}
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
)}
|
|
|
|
{profitData?.expense_by_user?.length > 0 && (
|
|
<Card title="人员支出明细" size="small" style={{ marginTop: 16 }}>
|
|
<Table
|
|
dataSource={profitData.expense_by_user}
|
|
rowKey="user_name"
|
|
pagination={false}
|
|
size="small"
|
|
columns={[
|
|
{ title: '人员', dataIndex: 'user_name' },
|
|
{ title: '金额(¥)', dataIndex: 'total_amount', render: (v: number) => parseFloat(v).toLocaleString(), align: 'right' as const },
|
|
{ title: '笔数', dataIndex: 'count', align: 'center' as const },
|
|
]}
|
|
/>
|
|
</Card>
|
|
)}
|
|
</Spin>
|
|
)
|
|
}
|
|
|
|
// 质保金 Tab
|
|
const WarrantyTab = () => {
|
|
// 从付款节点中查找质保金节点
|
|
const warrantyMilestone = milestones.find(m => m.milestone_name === '质保金');
|
|
|
|
// 如果找到质保金节点,使用该节点的数据;否则使用0
|
|
const warrantyAmount = warrantyMilestone?.amount || 0;
|
|
const warrantyPercent = warrantyMilestone?.percentage || 0;
|
|
|
|
return (
|
|
<Card title="质保金管理">
|
|
<Descriptions column={2} bordered>
|
|
<Descriptions.Item label="质保金金额">
|
|
¥{warrantyAmount.toLocaleString()}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="质保比例">{warrantyPercent}%</Descriptions.Item>
|
|
<Descriptions.Item label="质保期限">{project.warranty_months} 个月</Descriptions.Item>
|
|
<Descriptions.Item label="起算日期">{project.warranty_start_date?.split('T')[0] || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="到期日期">{project.warranty_end_date?.split('T')[0] || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="质保金状态">
|
|
<Tag color={project.warranty_status === 'released' ? 'success' : 'default'}>
|
|
{project.warranty_status === 'released' ? '已释放' : '待释放'}
|
|
</Tag>
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
|
<Space>
|
|
<Button type="primary">标记已释放</Button>
|
|
<Button>延期</Button>
|
|
</Space>
|
|
</div>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ marginBottom: 16 }}>
|
|
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/projects')}>
|
|
返回列表
|
|
</Button>
|
|
</div>
|
|
|
|
<Card style={{ marginBottom: 16 }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<div>
|
|
<h2 style={{ margin: 0 }}>{project.name}</h2>
|
|
<Space style={{ marginTop: 8 }}>
|
|
{getStatusTag(project.status)}
|
|
{project.current_phase && <Tag color="blue">当前: {project.current_phase}</Tag>}
|
|
{project.phase_progress > 0 && <span>进度: {project.phase_progress}%</span>}
|
|
<span>质保金: ¥{(milestones.find(m => m.milestone_name === '质保金')?.amount || 0).toLocaleString()}</span>
|
|
</Space>
|
|
</div>
|
|
<Space>
|
|
<Button icon={<ToolOutlined />} type="primary" onClick={() => navigate(`/construction/progress/${id}`)}>进入施工管理</Button>
|
|
<Button icon={<EditOutlined />} onClick={() => setBasicInfoEditModalVisible(true)}>编辑项目</Button>
|
|
</Space>
|
|
</div>
|
|
{project.phase_progress > 0 && (
|
|
<Progress percent={project.phase_progress} style={{ marginTop: 12 }} strokeColor="#1890ff" />
|
|
)}
|
|
</Card>
|
|
|
|
{isMobile ? (
|
|
<div style={{ marginBottom: 16 }}>
|
|
<Select
|
|
style={{ width: '100%' }}
|
|
value={activeTab}
|
|
onChange={setActiveTab}
|
|
options={[
|
|
{ value: 'basic', label: '基本信息' },
|
|
{ value: 'contract', label: '合同与收款' },
|
|
{ value: 'subcontract', label: '分包管理' },
|
|
{ value: 'material', label: '材料管理' },
|
|
{ value: 'finance', label: '财务收支' },
|
|
{ value: 'warranty', label: '质保金' },
|
|
{ value: 'log', label: '施工日志' }
|
|
]}
|
|
/>
|
|
<div style={{ marginTop: 16 }}>
|
|
{activeTab === 'basic' && <BasicInfoTab />}
|
|
{activeTab === 'contract' && <ContractTab />}
|
|
{activeTab === 'subcontract' && <SubcontractTab />}
|
|
{activeTab === 'material' && <MaterialTab />}
|
|
{activeTab === 'finance' && <FinanceTab />}
|
|
{activeTab === 'warranty' && <WarrantyTab />}
|
|
{activeTab === 'log' && <LogTab />}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Tabs
|
|
activeKey={activeTab}
|
|
onChange={setActiveTab}
|
|
type="card"
|
|
items={[
|
|
{
|
|
key: 'basic',
|
|
label: <span><InfoCircleOutlined /> 基本信息</span>,
|
|
children: <BasicInfoTab />
|
|
},
|
|
{
|
|
key: 'contract',
|
|
label: <span><FileTextOutlined /> 合同与收款</span>,
|
|
children: <ContractTab />
|
|
},
|
|
{
|
|
key: 'subcontract',
|
|
label: <span><TeamOutlined /> 分包管理</span>,
|
|
children: <SubcontractTab />
|
|
},
|
|
{
|
|
key: 'material',
|
|
label: <span><DatabaseOutlined /> 材料管理</span>,
|
|
children: <MaterialTab />
|
|
},
|
|
{
|
|
key: 'finance',
|
|
label: <span><DollarOutlined /> 财务收支</span>,
|
|
children: <FinanceTab />
|
|
},
|
|
{
|
|
key: 'warranty',
|
|
label: <span><SafetyOutlined /> 质保金</span>,
|
|
children: <WarrantyTab />
|
|
},
|
|
{
|
|
key: 'log',
|
|
label: <span><FileSearchOutlined /> 施工日志</span>,
|
|
children: <LogTab />
|
|
}
|
|
]}
|
|
/>
|
|
)}
|
|
|
|
{/* 基本信息编辑模态框 */}
|
|
<Modal
|
|
title="编辑项目基本信息"
|
|
open={basicInfoEditModalVisible}
|
|
onCancel={() => setBasicInfoEditModalVisible(false)}
|
|
onOk={async () => {
|
|
try {
|
|
const values = await basicInfoForm.validateFields();
|
|
// 构建保存数据
|
|
const saveData = {
|
|
name: values.name,
|
|
manager_id: values.manager_id,
|
|
location: values.location,
|
|
start_date: values.start_date ? values.start_date.toISOString() : null,
|
|
end_date: values.end_date ? values.end_date.toISOString() : null,
|
|
description: values.description
|
|
};
|
|
|
|
|
|
// 发送保存请求
|
|
const response = await apiClient.put(`/projects/${id}`, saveData);
|
|
if (response.data.success) {
|
|
message.success('基本信息保存成功');
|
|
// 重新获取项目信息
|
|
fetchProject();
|
|
setBasicInfoEditModalVisible(false);
|
|
} else {
|
|
message.error('保存失败:' + response.data.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('保存失败:', error);
|
|
message.error('保存失败,请检查表单数据');
|
|
}
|
|
}}
|
|
width={600}
|
|
okText="保存"
|
|
cancelText="取消"
|
|
>
|
|
<div style={{ padding: 20 }}>
|
|
<Form layout="vertical" form={basicInfoForm}>
|
|
<Form.Item
|
|
label="项目名称"
|
|
name="name"
|
|
rules={[{ required: true, message: '请输入项目名称' }]}
|
|
>
|
|
<Input placeholder="请输入项目名称" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="项目经理"
|
|
name="manager_id"
|
|
rules={[{ required: true, message: '请选择项目经理' }]}
|
|
>
|
|
<Select placeholder="请选择项目经理" loading={usersLoading}>
|
|
{users.map(user => (
|
|
<Select.Option key={user.id} value={user.id}>
|
|
{user.name} ({user.username})
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="项目地点"
|
|
name="location"
|
|
>
|
|
<Input placeholder="请输入项目地点" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="开工日期"
|
|
name="start_date"
|
|
rules={[{ required: true, message: '请选择开工日期' }]}
|
|
>
|
|
<DatePicker
|
|
style={{ width: '100%' }}
|
|
onChange={(date) => {
|
|
const values = basicInfoForm.getFieldsValue();
|
|
if (date && values.work_days) {
|
|
// 计算完工日期(减去1天,因为后端计算时会加1天)
|
|
const endDate = dayjs(date).add(values.work_days - 1, 'day');
|
|
basicInfoForm.setFieldsValue({ end_date: endDate });
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="工期天数"
|
|
name="work_days"
|
|
rules={[{ required: true, message: '请输入工期天数' }]}
|
|
>
|
|
<InputNumber
|
|
min={1}
|
|
placeholder="请输入工期天数"
|
|
onChange={(value) => {
|
|
const values = basicInfoForm.getFieldsValue();
|
|
if (value && values.start_date) {
|
|
// 计算完工日期(减去1天,因为后端计算时会加1天)
|
|
const endDate = dayjs(values.start_date).add(value - 1, 'day');
|
|
basicInfoForm.setFieldsValue({ end_date: endDate });
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="完工日期"
|
|
name="end_date"
|
|
rules={[{ required: true, message: '请选择完工日期' }]}
|
|
>
|
|
<DatePicker
|
|
style={{ width: '100%' }}
|
|
onChange={(date) => {
|
|
const values = basicInfoForm.getFieldsValue();
|
|
if (date && values.start_date) {
|
|
// 计算工期天数(加上1天,与后端计算逻辑一致)
|
|
const startDate = dayjs(values.start_date);
|
|
const endDate = dayjs(date);
|
|
const diffDays = endDate.diff(startDate, 'day') + 1;
|
|
basicInfoForm.setFieldsValue({ work_days: diffDays });
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="工程概况"
|
|
name="description"
|
|
>
|
|
<Input.TextArea rows={4} placeholder="请输入工程概况" />
|
|
</Form.Item>
|
|
</Form>
|
|
</div>
|
|
</Modal>
|
|
|
|
{/* 新增分包模态框 */}
|
|
<Modal
|
|
title="新增分包"
|
|
open={subcontractModalVisible}
|
|
onCancel={closeSubcontractModal}
|
|
onOk={handleSubcontractSubmit}
|
|
width={800}
|
|
okText="保存"
|
|
cancelText="取消"
|
|
>
|
|
<div style={{ padding: 20 }}>
|
|
<Form layout="vertical" form={subcontractForm}>
|
|
<Form.Item
|
|
label="分包商"
|
|
name="subcontractor_id"
|
|
rules={[{ required: true, message: '请选择分包商' }]}
|
|
>
|
|
<Select
|
|
placeholder="请选择分包商"
|
|
onChange={(value) => {
|
|
const selectedSubcontractor = subcontractors.find(s => s.id === value);
|
|
if (selectedSubcontractor) {
|
|
subcontractForm.setFieldsValue({ subcontractor_name: selectedSubcontractor.name });
|
|
}
|
|
}}
|
|
>
|
|
{subcontractors.map(subcontractor => (
|
|
<Select.Option key={subcontractor.id} value={subcontractor.id}>
|
|
{subcontractor.name}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="分包商名称"
|
|
name="subcontractor_name"
|
|
rules={[{ required: true, message: '请输入分包商名称' }]}
|
|
>
|
|
<Input placeholder="请输入分包商名称" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="币种"
|
|
name="currency"
|
|
>
|
|
<Select
|
|
placeholder="请选择币种"
|
|
defaultValue="CNY"
|
|
>
|
|
<Select.Option value="CNY">人民币</Select.Option>
|
|
<Select.Option value="USD">美元</Select.Option>
|
|
<Select.Option value="LAK">老挝基普</Select.Option>
|
|
<Select.Option value="THB">泰铢</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="合同金额"
|
|
name="contract_amount"
|
|
rules={[{ required: true, message: '请输入合同金额' }]}
|
|
>
|
|
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.currency !== currentValues.currency}>
|
|
{({ getFieldValue }) => {
|
|
const currency = getFieldValue('currency') || 'CNY';
|
|
const currencySymbol = {
|
|
CNY: '¥',
|
|
USD: '$',
|
|
LAK: '₭',
|
|
THB: '฿'
|
|
}[currency] || '';
|
|
|
|
return (
|
|
<InputNumber
|
|
style={{ width: '100%' }}
|
|
min={0}
|
|
placeholder="请输入合同金额"
|
|
formatter={(value) => {
|
|
if (value === undefined || value === null) return '';
|
|
return `${currencySymbol} ${value}`;
|
|
}}
|
|
parser={(value) => {
|
|
if (!value) return 0;
|
|
// 移除所有非数字字符,保留小数点
|
|
return parseFloat(value.replace(/[^0-9.]/g, '')) || 0;
|
|
}}
|
|
/>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="结算方式"
|
|
name="settlement_type"
|
|
rules={[{ required: true, message: '请选择结算方式' }]}
|
|
>
|
|
<Select
|
|
placeholder="请选择结算方式"
|
|
defaultValue="lump_sum"
|
|
onChange={(value) => {
|
|
if (value === 'unit_price') {
|
|
// 当选择单价结算时,自动计算并设置合同金额
|
|
const totalAmount = unitPriceItems.reduce((sum, item) => sum + (item.total || 0), 0);
|
|
subcontractForm.setFieldsValue({ contract_amount: totalAmount });
|
|
}
|
|
}}
|
|
>
|
|
<Select.Option value="lump_sum">总价包干</Select.Option>
|
|
<Select.Option value="unit_price">单价结算</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="其他约定"
|
|
name="other_terms"
|
|
>
|
|
<Input.TextArea rows={3} placeholder="请输入其他约定" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="付款说明"
|
|
name="payment_description"
|
|
>
|
|
<Input.TextArea rows={3} placeholder="请输入付款说明" />
|
|
</Form.Item>
|
|
|
|
{/* 单价结算项目列表 - 只在单价结算时显示 */}
|
|
<Form.Item
|
|
noStyle
|
|
shouldUpdate={(prevValues, currentValues) => prevValues.settlement_type !== currentValues.settlement_type || prevValues.currency !== currentValues.currency}
|
|
>
|
|
{({ getFieldValue }) => {
|
|
const settlementType = getFieldValue('settlement_type');
|
|
const currency = getFieldValue('currency') || 'CNY';
|
|
const currencySymbol = {
|
|
CNY: '¥',
|
|
USD: '$',
|
|
LAK: '₭',
|
|
THB: '฿'
|
|
}[currency] || '';
|
|
|
|
if (settlementType === 'unit_price') {
|
|
// 计算总合计
|
|
const totalAmount = unitPriceItems.reduce((sum, item) => sum + (item.total || 0), 0);
|
|
|
|
return (
|
|
<Form.Item label="项目单项价">
|
|
<div style={{ marginBottom: 16, padding: 16, border: '1px solid #f0f0f0', borderRadius: 8, backgroundColor: '#fafafa' }}>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr 0.5fr', gap: 12, marginBottom: 12, paddingBottom: 12, borderBottom: '1px solid #e8e8e8' }}>
|
|
<div style={{ fontWeight: 'bold' }}>项目名称</div>
|
|
<div style={{ fontWeight: 'bold' }}>单位</div>
|
|
<div style={{ fontWeight: 'bold' }}>数量</div>
|
|
<div style={{ fontWeight: 'bold' }}>单价 ({currencySymbol})</div>
|
|
<div style={{ fontWeight: 'bold' }}>总价 ({currencySymbol})</div>
|
|
<div style={{ fontWeight: 'bold' }}>操作</div>
|
|
</div>
|
|
{unitPriceItems.map((item, index) => (
|
|
<div key={item.key} style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr 0.5fr', gap: 12, marginBottom: 12, alignItems: 'center' }}>
|
|
<Input
|
|
placeholder="请输入项目名称"
|
|
value={item.name}
|
|
onChange={(e) => handleUnitPriceItemChange(index, 'name', e.target.value)}
|
|
/>
|
|
<Input
|
|
placeholder="请输入单位"
|
|
value={item.unit}
|
|
onChange={(e) => handleUnitPriceItemChange(index, 'unit', e.target.value)}
|
|
/>
|
|
<InputNumber
|
|
min={0}
|
|
placeholder="数量"
|
|
value={item.quantity}
|
|
onChange={(value) => handleUnitPriceItemChange(index, 'quantity', value)}
|
|
/>
|
|
<InputNumber
|
|
min={0}
|
|
placeholder="单价"
|
|
value={item.price}
|
|
onChange={(value) => handleUnitPriceItemChange(index, 'price', value)}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
<InputNumber
|
|
min={0}
|
|
disabled
|
|
value={item.total}
|
|
formatter={(value) => `${currencySymbol} ${value}`}
|
|
/>
|
|
<Button danger size="small" onClick={() => removeUnitPriceItem(index)}>删除</Button>
|
|
</div>
|
|
))}
|
|
{/* 合计行 */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr 0.5fr', gap: 12, marginTop: 12, paddingTop: 12, borderTop: '1px solid #e8e8e8' }}>
|
|
<div style={{ fontWeight: 'bold' }}>合计</div>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
<div style={{ fontWeight: 'bold', color: '#888' }}>{currencySymbol} {totalAmount.toLocaleString()}</div>
|
|
<div></div>
|
|
</div>
|
|
</div>
|
|
<Button type="dashed" style={{ width: '100%' }} onClick={() => addUnitPriceItem()}>
|
|
+ 添加项目单项
|
|
</Button>
|
|
</Form.Item>
|
|
);
|
|
}
|
|
return null;
|
|
}}
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="开始日期"
|
|
name="start_date"
|
|
rules={[{ required: true, message: '请选择开始日期' }]}
|
|
>
|
|
<DatePicker
|
|
style={{ width: '100%' }}
|
|
onChange={(date) => {
|
|
const values = subcontractForm.getFieldsValue();
|
|
if (date && values.work_days) {
|
|
// 计算结束日期(减去1天,因为后端计算时会加1天)
|
|
const endDate = dayjs(date).add(values.work_days - 1, 'day');
|
|
subcontractForm.setFieldsValue({ end_date: endDate });
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="工期天数"
|
|
name="work_days"
|
|
rules={[{ required: true, message: '请输入工期天数' }]}
|
|
>
|
|
<InputNumber
|
|
min={1}
|
|
placeholder="请输入工期天数"
|
|
onChange={(value) => {
|
|
const values = subcontractForm.getFieldsValue();
|
|
if (value && values.start_date) {
|
|
// 计算结束日期(减去1天,因为后端计算时会加1天)
|
|
const endDate = dayjs(values.start_date).add(value - 1, 'day');
|
|
subcontractForm.setFieldsValue({ end_date: endDate });
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="结束日期"
|
|
name="end_date"
|
|
rules={[{ required: true, message: '请选择结束日期' }]}
|
|
>
|
|
<DatePicker
|
|
style={{ width: '100%' }}
|
|
onChange={(date) => {
|
|
const values = subcontractForm.getFieldsValue();
|
|
if (date && values.start_date) {
|
|
// 计算工期天数(加上1天,与后端计算逻辑一致)
|
|
const startDate = dayjs(values.start_date);
|
|
const endDate = dayjs(date);
|
|
const diffDays = endDate.diff(startDate, 'day') + 1;
|
|
subcontractForm.setFieldsValue({ work_days: diffDays });
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="状态"
|
|
name="status"
|
|
>
|
|
<Select
|
|
placeholder="请选择状态"
|
|
defaultValue="active"
|
|
>
|
|
<Select.Option value="active">进行中</Select.Option>
|
|
<Select.Option value="completed">已完成</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Form>
|
|
</div>
|
|
</Modal>
|
|
|
|
{/* 合同编辑模态框 */}
|
|
<Modal
|
|
title="合同细节录入"
|
|
open={contractEditModalVisible}
|
|
onCancel={() => setContractEditModalVisible(false)}
|
|
onOk={async () => {
|
|
try {
|
|
// 手动设置合同总价字段,确保在单价结算时也有值
|
|
if (settlementType === 'unit_price') {
|
|
await contractForm.setFieldsValue({ contract_total: contractTotal });
|
|
}
|
|
|
|
const values = await contractForm.validateFields();
|
|
const actualSettlementType = values.settlement_type || settlementType;
|
|
const finalContractTotal = actualSettlementType === 'unit_price' ? contractTotal : (values.contract_total || 0);
|
|
const saveData = {
|
|
project_id: id,
|
|
project_overview: values.project_overview,
|
|
settlement_type: actualSettlementType,
|
|
contract_total: finalContractTotal,
|
|
tax_included: values.tax_included || false,
|
|
unit_price_items: unitPriceItems,
|
|
payment_nodes: paymentNodes,
|
|
other_info: values.other_info || '',
|
|
contract_file: contractFile
|
|
};
|
|
|
|
|
|
// 发送保存请求
|
|
const response = await apiClient.put(`/projects/${id}/contract`, saveData);
|
|
if (response.data.success) {
|
|
message.success('合同细节保存成功');
|
|
// 重新获取项目信息
|
|
fetchProject();
|
|
fetchProjectData();
|
|
setContractEditModalVisible(false);
|
|
} else {
|
|
message.error('保存失败:' + response.data.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('保存失败:', error);
|
|
message.error('保存失败,请检查表单数据');
|
|
}
|
|
}}
|
|
width={900}
|
|
okText="保存"
|
|
cancelText="取消"
|
|
>
|
|
<div style={{ padding: 20 }}>
|
|
<Form layout="vertical" form={contractForm}>
|
|
{/* 工程概况 */}
|
|
<Form.Item
|
|
label="工程概况"
|
|
name="project_overview"
|
|
rules={[{ required: true, message: '请输入工程概况' }]}
|
|
>
|
|
<Input.TextArea rows={4} placeholder="请输入工程概况" />
|
|
</Form.Item>
|
|
|
|
{/* 结算方式 */}
|
|
<Form.Item
|
|
label="结算方式"
|
|
name="settlement_type"
|
|
rules={[{ required: true, message: '请选择结算方式' }]}
|
|
>
|
|
<Select
|
|
placeholder="请选择结算方式"
|
|
options={[
|
|
{ value: 'lump_sum', label: '总价包干' },
|
|
{ value: 'unit_price', label: '单价结算' }
|
|
]}
|
|
onChange={(val) => {
|
|
setSettlementType(val);
|
|
if (val === 'lump_sum') {
|
|
const formTotal = contractForm.getFieldValue('contract_total') || 0;
|
|
setContractTotal(formTotal);
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
{/* 合同总价 - 只在总价包干时显示 */}
|
|
{settlementType === 'lump_sum' && (
|
|
<Form.Item
|
|
label="合同总价"
|
|
name="contract_total"
|
|
rules={[{ required: true, message: '请输入合同总价' }]}
|
|
>
|
|
<InputNumber
|
|
min={0}
|
|
placeholder="请输入合同总价"
|
|
formatter={(value) => `¥ ${value}`}
|
|
parser={(value) => value?.replace(/¥\s?/, '')}
|
|
onChange={(val) => setContractTotal(val || 0)}
|
|
/>
|
|
</Form.Item>
|
|
)}
|
|
{/* 隐藏的合同总价字段 - 用于单价结算时的验证 */}
|
|
{settlementType === 'unit_price' && (
|
|
<Form.Item
|
|
name="contract_total"
|
|
hidden
|
|
>
|
|
<InputNumber
|
|
min={0}
|
|
/>
|
|
</Form.Item>
|
|
)}
|
|
|
|
{/* 合同是否含税 */}
|
|
<Form.Item
|
|
label="合同是否含税"
|
|
name="tax_included"
|
|
valuePropName="checked"
|
|
>
|
|
<Switch checkedChildren="是" unCheckedChildren="否" />
|
|
</Form.Item>
|
|
|
|
{/* 单价结算项目 - 只在单价结算时显示 */}
|
|
{settlementType === 'unit_price' && (
|
|
<Form.Item label="项目单项价">
|
|
<Table
|
|
dataSource={unitPriceItems}
|
|
columns={[
|
|
{
|
|
title: '项目名称',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
render: (_, record, index) => (
|
|
<Input
|
|
placeholder="请输入项目名称"
|
|
value={unitPriceItems[index].name}
|
|
onChange={(e) => handleUnitPriceItemChange(index, 'name', e.target.value)}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: '单位',
|
|
dataIndex: 'unit',
|
|
key: 'unit',
|
|
render: (_, record, index) => (
|
|
<Input
|
|
placeholder="请输入单位"
|
|
value={unitPriceItems[index].unit}
|
|
onChange={(e) => handleUnitPriceItemChange(index, 'unit', e.target.value)}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: '数量',
|
|
dataIndex: 'quantity',
|
|
key: 'quantity',
|
|
render: (_, record, index) => (
|
|
<InputNumber
|
|
min={0}
|
|
placeholder="请输入数量"
|
|
value={unitPriceItems[index].quantity}
|
|
onChange={(value) => handleUnitPriceItemChange(index, 'quantity', value)}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: '单价',
|
|
dataIndex: 'price',
|
|
key: 'price',
|
|
render: (_, record, index) => (
|
|
<InputNumber
|
|
min={0}
|
|
placeholder="请输入单价"
|
|
value={unitPriceItems[index].price}
|
|
onChange={(value) => handleUnitPriceItemChange(index, 'price', value)}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: '总价',
|
|
dataIndex: 'total',
|
|
key: 'total',
|
|
render: (_, record, index) => (
|
|
<InputNumber
|
|
min={0}
|
|
disabled
|
|
value={unitPriceItems[index].total}
|
|
/>
|
|
)
|
|
},
|
|
{ title: '操作', key: 'action', render: (_, record, index) => <Button danger onClick={() => removeUnitPriceItem(index)}>删除</Button> }
|
|
]}
|
|
pagination={false}
|
|
locale={{ emptyText: '暂无项目单项' }}
|
|
/>
|
|
<Button type="dashed" style={{ marginTop: 16 }} onClick={() => addUnitPriceItem()}>添加项目单项</Button>
|
|
</Form.Item>
|
|
)}
|
|
|
|
{/* 付款节点 */}
|
|
<Form.Item label="付款节点">
|
|
<Table
|
|
dataSource={paymentNodes}
|
|
columns={[
|
|
{
|
|
title: '节点名称',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
render: (_, record, index) => (
|
|
<Input
|
|
placeholder="请输入节点名称"
|
|
value={paymentNodes[index].name}
|
|
onChange={(e) => {
|
|
const newNodes = [...paymentNodes]
|
|
newNodes[index].name = e.target.value
|
|
setPaymentNodes(newNodes)
|
|
}}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: '节点条件',
|
|
dataIndex: 'condition',
|
|
key: 'condition',
|
|
render: (_, record, index) => (
|
|
<Input
|
|
placeholder="请输入节点条件"
|
|
value={paymentNodes[index].condition}
|
|
onChange={(e) => {
|
|
const newNodes = [...paymentNodes]
|
|
newNodes[index].condition = e.target.value
|
|
setPaymentNodes(newNodes)
|
|
}}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: '比例(%)',
|
|
dataIndex: 'percentage',
|
|
key: 'percentage',
|
|
render: (_, record, index) => (
|
|
<InputNumber
|
|
min={0}
|
|
max={100}
|
|
placeholder="请输入比例"
|
|
value={paymentNodes[index].percentage}
|
|
onChange={(value) => handlePaymentNodePercentageChange(index, value || 0)}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: '金额',
|
|
dataIndex: 'amount',
|
|
key: 'amount',
|
|
render: (_, record, index) => (
|
|
<InputNumber
|
|
min={0}
|
|
placeholder="请输入金额"
|
|
value={paymentNodes[index].amount}
|
|
disabled
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
render: () => <Tag color="default">未到达付款节点</Tag>
|
|
},
|
|
{ title: '操作', key: 'action', render: (_, record, index) => <Button danger onClick={() => { const newNodes = [...paymentNodes]; newNodes.splice(index, 1); setPaymentNodes(newNodes); }}>删除</Button> }
|
|
]}
|
|
pagination={false}
|
|
locale={{ emptyText: '暂无付款节点' }}
|
|
/>
|
|
<Button type="dashed" style={{ marginTop: 16 }} onClick={() => { setPaymentNodes([...paymentNodes, { key: String(Date.now()), name: '', condition: '', percentage: 0, amount: 0, status: 'pending' }]); }}>添加付款节点</Button>
|
|
</Form.Item>
|
|
|
|
{/* 合同附件 */}
|
|
<Form.Item label="合同附件">
|
|
<Upload
|
|
name="file"
|
|
customRequest={handleFileUpload}
|
|
listType="text"
|
|
maxCount={1}
|
|
fileList={contractFile ? [{ uid: '1', name: contractFile.split('/').pop() || '', status: 'done', url: contractFile }] : []}
|
|
onRemove={() => setContractFile('')}
|
|
>
|
|
<Button icon={<UploadOutlined />}>{contractFile ? '更换文件' : '点击上传'}</Button>
|
|
</Upload>
|
|
</Form.Item>
|
|
|
|
{/* 其他信息 */}
|
|
<Form.Item
|
|
label="其他合同信息"
|
|
name="other_info"
|
|
>
|
|
<Input.TextArea rows={4} placeholder="请输入其他合同相关信息" />
|
|
</Form.Item>
|
|
</Form>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ProjectDetail
|