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(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('') const [contracts, setContracts] = useState([]) const [subcontracts, setSubcontracts] = useState([]) const [materials, setMaterials] = useState([]) const [milestones, setMilestones] = useState([]) const [finances, setFinances] = useState([]) const [warrantyDeposits, setWarrantyDeposits] = useState([]) const [constructionLogs, setConstructionLogs] = useState([]) const [subcontractModalVisible, setSubcontractModalVisible] = useState(false) const [subcontractForm] = Form.useForm() const [subcontractors, setSubcontractors] = useState([]) const [users, setUsers] = useState([]) 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 = { 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 {config.text} } // 计算合同总价 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 (
) } if (!project) { return (
项目不存在或已被删除
) } // 基本信息 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 ( } onClick={() => setBasicInfoEditModalVisible(true)}>编辑}> {project.project_code} {project.name} {project.customer_name} {getManagerName()} {project.location || '-'} {getStatusTag(project.status)} {project.start_date?.split('T')[0] || '-'} {project.end_date?.split('T')[0] || '-'} {project.contract_days} 天 {project.settlement_type === 'lump_sum' ? '总价包干' : '单价结算'} {project.project_situation || project.description || '-'} {new Date().toLocaleDateString()} ); } // 合同详情 Tab const ContractTab = () => { // 获取最新的合同信息 const latestContract = contracts.length > 0 ? contracts[0] : null; return ( } 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); }}>合同细节录入}> {project.currency} {parseFloat(latestContract?.contract_amount || project.contract_amount || '0').toLocaleString()} {project.currency} {project.contract_type === 'lump_sum' ? '总价包干' : '单价合同'} {latestContract?.settlement_method === 'lump_sum' ? '总价包干' : '单价结算'} {latestContract?.tax_included ? '是' : '否'} {latestContract?.contract_code || '-'} ({ ...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) => }, { title: '状态', dataIndex: 'status', key: 'status', render: (v: string) => {v === 'completed' ? '已完成' : v === 'in_progress' ? '进行中' : '待开始'} }, ]} locale={{ emptyText: '暂无节点记录' }} />
{latestContract ? (
{latestContract.contract_code} {latestContract.contract_file ? ( 查看合同文件 ) : ( '-' )}
) : (
暂无合同附件
)}
{latestContract?.other_info || '-'} {milestones.find(m => m.milestone_name === '质保金') ? '是' : '否'} {milestones.find(m => m.milestone_name === '质保金')?.percentage || 0}% ¥{(milestones.find(m => m.milestone_name === '质保金')?.amount || 0).toLocaleString()} {project.warranty_months} 个月 {project.warranty_end_date?.split('T')[0] || '-'} {project.warranty_status === 'released' ? '已释放' : '待释放'} ) } // 分包管理 Tab const SubcontractTab = () => { const [subcontractDetailModalVisible, setSubcontractDetailModalVisible] = useState(false); const [selectedSubcontract, setSelectedSubcontract] = useState(null); const openSubcontractDetail = (subcontract: any) => { setSelectedSubcontract(subcontract); setSubcontractDetailModalVisible(true); }; const closeSubcontractDetail = () => { setSubcontractDetailModalVisible(false); setSelectedSubcontract(null); }; const getCurrencySymbol = (currency: string) => { const symbols: Record = { CNY: '¥', USD: '$', LAK: '₭', THB: '฿' }; return symbols[currency] || ''; }; return ( 新增分包}>
({ ...item, key: item.id }))} columns={[ { title: '分包商', dataIndex: 'subcontractor_name', key: 'subcontractor_name', render: (text: string, record: any) => ( openSubcontractDetail(record)}>{text} ) }, { 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) => {v === 'completed' ? '已完成' : '进行中'} }, ]} locale={{ emptyText: '暂无分包记录' }} /> {/* 分包详情模态框 */} 关闭 ]} > {selectedSubcontract && (
{selectedSubcontract.subcontractor_name} {getCurrencySymbol(selectedSubcontract.currency)}{selectedSubcontract.contract_amount.toLocaleString()} {selectedSubcontract.currency} {selectedSubcontract.settlement_type === 'lump_sum' ? '总价包干' : '单价结算'} {getCurrencySymbol(selectedSubcontract.currency)}{selectedSubcontract.paid_amount.toLocaleString()} {selectedSubcontract.status === 'completed' ? '已完成' : '进行中'} {selectedSubcontract.start_date ? selectedSubcontract.start_date.split('T')[0] : '-'} {selectedSubcontract.end_date ? selectedSubcontract.end_date.split('T')[0] : '-'} {selectedSubcontract.other_terms || '-'} {selectedSubcontract.payment_description || '-'} {/* 单价项目列表 */} {selectedSubcontract.settlement_type === 'unit_price' && selectedSubcontract.unit_price_items && selectedSubcontract.unit_price_items.length > 0 && (
({ ...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} /> )} )} ); } // 材料管理 Tab const MaterialTab = () => (
({ ...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: '暂无材料记录' }} /> ) // 施工节点 Tab const MilestoneTab = () => (
({ ...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) => }, { title: '状态', dataIndex: 'status', key: 'status', render: (v: string) => {v === 'completed' ? '已完成' : v === 'in_progress' ? '进行中' : '待开始'} }, ]} locale={{ emptyText: '暂无节点记录' }} />
({ ...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) => {v === 'completed' ? '已完成' : v === 'in_progress' ? '进行中' : '待开始'} }, { title: '操作', key: 'action', render: () => }, ]} locale={{ emptyText: '暂无记录' }} /> ) // 施工日志 Tab const LogTab = () => ( 新增日志}>
({ ...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: '暂无日志记录' }} /> ) const FinanceTab = () => { const [profitData, setProfitData] = React.useState(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 = { material: '材料采购', equipment: '设备采购', subcontract: '施工分包', labor: '人工工资', travel: '差旅交通', accommodation: '食宿费用', freight: '运输物流', design: '勘测设计', tools: '小型工具', client_relations: '客户/EDL关系', other_project: '其他项目支出', contract_payment: '合同收款', deposit_refund: '质保金退回' }; return ( ¥{contractAmount.toLocaleString()} ¥{totalIncome.toLocaleString()} ¥{totalExpense.toLocaleString()} = 0 ? '#52c41a' : '#ff4d4f', fontWeight: 'bold', fontSize: 16 }}> ¥{grossProfit.toLocaleString()} = 0 ? '#52c41a' : '#ff4d4f', fontWeight: 'bold' }}> {grossMargin}% {profitData?.expense_by_category?.length > 0 && (
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}%`; } }, ]} /> )} {profitData?.expense_by_user?.length > 0 && (
parseFloat(v).toLocaleString(), align: 'right' as const }, { title: '笔数', dataIndex: 'count', align: 'center' as const }, ]} /> )} ) } // 质保金 Tab const WarrantyTab = () => { // 从付款节点中查找质保金节点 const warrantyMilestone = milestones.find(m => m.milestone_name === '质保金'); // 如果找到质保金节点,使用该节点的数据;否则使用0 const warrantyAmount = warrantyMilestone?.amount || 0; const warrantyPercent = warrantyMilestone?.percentage || 0; return ( ¥{warrantyAmount.toLocaleString()} {warrantyPercent}% {project.warranty_months} 个月 {project.warranty_start_date?.split('T')[0] || '-'} {project.warranty_end_date?.split('T')[0] || '-'} {project.warranty_status === 'released' ? '已释放' : '待释放'}
) } return (

{project.name}

{getStatusTag(project.status)} {project.current_phase && 当前: {project.current_phase}} {project.phase_progress > 0 && 进度: {project.phase_progress}%} 质保金: ¥{(milestones.find(m => m.milestone_name === '质保金')?.amount || 0).toLocaleString()}
{project.phase_progress > 0 && ( )}
{isMobile ? (
{ 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 }); } }} /> { 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 }); } }} /> { 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 }); } }} />
{/* 新增分包模态框 */}
prevValues.currency !== currentValues.currency}> {({ getFieldValue }) => { const currency = getFieldValue('currency') || 'CNY'; const currencySymbol = { CNY: '¥', USD: '$', LAK: '₭', THB: '฿' }[currency] || ''; return ( { if (value === undefined || value === null) return ''; return `${currencySymbol} ${value}`; }} parser={(value) => { if (!value) return 0; // 移除所有非数字字符,保留小数点 return parseFloat(value.replace(/[^0-9.]/g, '')) || 0; }} /> ); }} {/* 单价结算项目列表 - 只在单价结算时显示 */} 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 (
项目名称
单位
数量
单价 ({currencySymbol})
总价 ({currencySymbol})
操作
{unitPriceItems.map((item, index) => (
handleUnitPriceItemChange(index, 'name', e.target.value)} /> handleUnitPriceItemChange(index, 'unit', e.target.value)} /> handleUnitPriceItemChange(index, 'quantity', value)} /> handleUnitPriceItemChange(index, 'price', value)} style={{ width: '100%' }} /> `${currencySymbol} ${value}`} />
))} {/* 合计行 */}
合计
{currencySymbol} {totalAmount.toLocaleString()}
); } return null; }}
{ 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 }); } }} /> { 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 }); } }} /> { 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 }); } }} />
{/* 合同编辑模态框 */} 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="取消" >
{/* 工程概况 */} {/* 结算方式 */}
( handleUnitPriceItemChange(index, 'name', e.target.value)} /> ) }, { title: '单位', dataIndex: 'unit', key: 'unit', render: (_, record, index) => ( handleUnitPriceItemChange(index, 'unit', e.target.value)} /> ) }, { title: '数量', dataIndex: 'quantity', key: 'quantity', render: (_, record, index) => ( handleUnitPriceItemChange(index, 'quantity', value)} /> ) }, { title: '单价', dataIndex: 'price', key: 'price', render: (_, record, index) => ( handleUnitPriceItemChange(index, 'price', value)} /> ) }, { title: '总价', dataIndex: 'total', key: 'total', render: (_, record, index) => ( ) }, { title: '操作', key: 'action', render: (_, record, index) => } ]} pagination={false} locale={{ emptyText: '暂无项目单项' }} /> )} {/* 付款节点 */}
( { const newNodes = [...paymentNodes] newNodes[index].name = e.target.value setPaymentNodes(newNodes) }} /> ) }, { title: '节点条件', dataIndex: 'condition', key: 'condition', render: (_, record, index) => ( { const newNodes = [...paymentNodes] newNodes[index].condition = e.target.value setPaymentNodes(newNodes) }} /> ) }, { title: '比例(%)', dataIndex: 'percentage', key: 'percentage', render: (_, record, index) => ( handlePaymentNodePercentageChange(index, value || 0)} /> ) }, { title: '金额', dataIndex: 'amount', key: 'amount', render: (_, record, index) => ( ) }, { title: '状态', dataIndex: 'status', key: 'status', render: () => 未到达付款节点 }, { title: '操作', key: 'action', render: (_, record, index) => } ]} pagination={false} locale={{ emptyText: '暂无付款节点' }} /> {/* 合同附件 */} setContractFile('')} > {/* 其他信息 */} ) } export default ProjectDetail