import React, { useState, useEffect } from 'react'; import { Card, Typography, Button, Space, Tag, message, Empty, Divider, Row, Col, List, Descriptions, Avatar, Badge, Modal, Input } from 'antd'; import { ArrowLeftOutlined, EyeOutlined, FileAddOutlined, FileOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; import { useNavigate, useParams } from 'react-router-dom'; import apiClient from '../../utils/request'; import dayjs from 'dayjs'; import QuotationCreateModal from './QuotationCreateModal'; import ContractCreateModal from './ContractCreateModal'; import { useAuthStore } from '../../store/authStore'; const { Title, Paragraph, Text } = Typography; interface Quotation { id: number; version: number; quotation_date: string; amount: number; currency: string; status: 'draft' | 'sent' | 'approved' | 'rejected'; file_url?: string; remark?: string; created_at: string; } interface BudgetProject { id: number; name: string; customer_id: number; customer_name: string; manager_id: number; manager_name: string; location?: string; survey_date?: string; intermediary?: string; intermediary_fee_type?: 'fixed' | 'percentage'; intermediary_fee_value?: number; customer_requirements?: string; project_overview?: string; attachments?: string[]; survey_photos?: string[]; status: 'negotiating' | 'signed' | 'unsigned'; days_in_status: number; created_at: string; quotations: Quotation[]; } const CURRENCIES: Record = { CNY: { label: '人民币', symbol: '¥' }, USD: { label: '美元', symbol: '$' }, LAK: { label: '老挝基普', symbol: '₭' }, THB: { label: '泰铢', symbol: '฿' }, }; const BudgetProjectDetail: React.FC = () => { const [project, setProject] = useState(null); const [loading, setLoading] = useState(true); const [quotationModalVisible, setQuotationModalVisible] = useState(false); const [contractModalVisible, setContractModalVisible] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [deletePassword, setDeletePassword] = useState(''); const [deleteLoading, setDeleteLoading] = useState(false); const [quotationDeleteModalVisible, setQuotationDeleteModalVisible] = useState(false); const [quotationDeleteId, setQuotationDeleteId] = useState(null); const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const { user: currentUser } = useAuthStore(); const isAdmin = currentUser?.role === 'admin' || false; useEffect(() => { if (id) { fetchProjectDetail(); } }, [id]); const fetchProjectDetail = async () => { if (!id) return; setLoading(true); try { const res = await apiClient.get(`/budget-projects/${id}`); if (res.data.success) { const projectData = res.data.data; // 后端已经解析了数据,直接使用 projectData.quotations = Array.isArray(projectData.quotations) ? projectData.quotations : []; projectData.attachments = Array.isArray(projectData.attachments) ? projectData.attachments : []; projectData.survey_photos = Array.isArray(projectData.survey_photos) ? projectData.survey_photos : []; setProject(projectData); } } catch (error) { console.error('获取项目详情失败:', error); message.error('获取数据失败'); } finally { setLoading(false); } }; const getStatusTag = (status: string) => { const statusMap: Record = { negotiating: { color: 'processing', text: '商谈中' }, signed: { color: 'success', text: '已签约' }, unsigned: { color: 'error', text: '未签约' }, }; const config = statusMap[status] || { color: 'default', text: status }; return {config.text}; }; const getQuotationStatusTag = (status: string) => { const statusMap: Record = { draft: { color: 'default', text: '草稿' }, sent: { color: 'processing', text: '已发送' }, approved: { color: 'success', text: '已通过' }, rejected: { color: 'error', text: '已拒绝' }, }; const config = statusMap[status] || { color: 'default', text: status }; return {config.text}; }; const formatAmount = (amount: number, currency: string = 'CNY') => { const c = CURRENCIES[currency]; const symbol = c?.symbol || '¥'; return `${symbol}${amount.toLocaleString('zh-CN')}`; }; const handleSign = () => { if (!project) return; setContractModalVisible(true); }; const handleContractSuccess = () => { setContractModalVisible(false); fetchProjectDetail(); }; const handleUnsigned = async () => { if (!project) return; try { const res = await apiClient.put(`/budget-projects/${project.id}/unsigned`, {}, { headers: { 'x-user-role': currentUser?.role || 'employee' } }); if (res.data.success) { message.success('标记未签约成功'); fetchProjectDetail(); } } catch (error) { message.error('操作失败'); } }; const handleDeleteQuotation = (quotationId: number) => { setQuotationDeleteId(quotationId); setDeletePassword(''); setQuotationDeleteModalVisible(true); }; const handleQuotationDeleteConfirm = async () => { if (!project || !quotationDeleteId) return; // 验证密码(这里简单验证,实际项目中应该使用更安全的验证方式) if (deletePassword !== 'X123c321@') { message.error('密码错误'); return; } setDeleteLoading(true); try { const res = await apiClient.delete(`/budget-projects/${project.id}/quotations/${quotationDeleteId}`, { headers: { 'x-user-role': currentUser?.role || 'employee' } }); if (res.data.success) { message.success('删除成功'); setQuotationDeleteModalVisible(false); fetchProjectDetail(); } } catch (error) { message.error('删除失败'); } finally { setDeleteLoading(false); } }; const openQuotationModal = () => { if (project) { setQuotationModalVisible(true); } }; const handleQuotationSuccess = () => { setQuotationModalVisible(false); fetchProjectDetail(); }; const goToProjectManagement = () => { if (project) { navigate(`/projects/${project.id}`); } }; const handleDeleteProject = () => { if (!project) return; setDeletePassword(''); setDeleteModalVisible(true); }; const handleProjectDeleteConfirm = async () => { if (!project) return; // 验证密码(这里简单验证,实际项目中应该使用更安全的验证方式) if (deletePassword !== 'X123c321@') { message.error('密码错误'); return; } setDeleteLoading(true); try { const res = await apiClient.delete(`/budget-projects/${project.id}`, { headers: { 'x-user-role': currentUser?.role || 'employee' } }); if (res.data.success) { message.success('删除成功'); setDeleteModalVisible(false); navigate('/budget-projects'); } } catch (error) { message.error('删除失败'); } finally { setDeleteLoading(false); } }; if (loading) { return (
); } if (!project) { return (
); } return (
预算项目详情
查看项目详细信息和报价版本
{/* 项目基本信息 */} 项目信息 {project.name} {project.customer_name} {project.manager_name} {project.location || '-'} {project.survey_date || '-'} {getStatusTag(project.status)} {dayjs(project.created_at).format('YYYY-MM-DD HH:mm:ss')} {project.intermediary || '-'} {project.intermediary_fee_type === 'fixed' ? '固定金额' : project.intermediary_fee_type === 'percentage' ? '百分比' : '-'} {project.intermediary_fee_value ? project.intermediary_fee_type === 'percentage' ? `${project.intermediary_fee_value}%` : formatAmount(project.intermediary_fee_value, 'CNY') : '-'} {project.customer_requirements || '-'} {project.project_overview || '-'} {/* 附件和照片 */} 附件和照片
附件上传: {project.attachments && project.attachments.length > 0 ? ( { const isOffice = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(url.split('.').pop()?.toLowerCase() || ''); const handleView = () => { if (isOffice) { const previewUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`; window.open(previewUrl, '_blank'); } else { window.open(url, '_blank'); } }; return ( {url.split('/').pop() || `file-${index}`} ); }} /> ) : ( 暂无附件 )}
勘察照片: {project.survey_photos && project.survey_photos.length > 0 ? (
{project.survey_photos.map((url, index) => (
{`survey-${index}`} window.open(url, '_blank')} />
照片 {index + 1}
))}
) : ( 暂无勘察照片 )}
{/* 报价版本列表 */}
报价版本 {isAdmin && project.status === 'negotiating' && ( )}
{Array.isArray(project.quotations) && project.quotations.length > 0 ? ( { const handleViewFile = () => { if (quotation.file_url) { const isOffice = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(quotation.file_url.split('.').pop()?.toLowerCase() || ''); if (isOffice) { const previewUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(quotation.file_url)}`; window.open(previewUrl, '_blank'); } else { window.open(quotation.file_url, '_blank'); } } }; return ( } onClick={handleViewFile} disabled={!quotation.file_url} > 查看 , isAdmin && ( ) ].filter(Boolean)} > V{quotation.version}} title={ 报价V{quotation.version} {getQuotationStatusTag(quotation.status)} } description={ 报价日期: {dayjs(quotation.quotation_date).format('YYYY-MM-DD')} 报价金额: {formatAmount(quotation.amount, quotation.currency)} {quotation.remark && 备注: {quotation.remark}} } /> ); }} /> ) : ( )}
{/* 操作按钮 */} 操作
{isAdmin && project.status === 'negotiating' && ( <> )} {project.status === 'signed' && ( )} {isAdmin && ( )}
{/* 新增报价版本弹窗 */} setQuotationModalVisible(false)} onSuccess={handleQuotationSuccess} /> {/* 合同信息录入弹窗 */} setContractModalVisible(false)} onSuccess={handleContractSuccess} /> {/* 删除项目确认模态框 */} setDeleteModalVisible(false)} confirmLoading={deleteLoading} okText="确认删除" cancelText="取消" >

确定要删除这个预算项目吗?此操作不可恢复。

请输入管理员密码确认删除操作:

setDeletePassword(e.target.value)} size="large" />
{/* 删除报价版本确认模态框 */} setQuotationDeleteModalVisible(false)} confirmLoading={deleteLoading} okText="确认删除" cancelText="取消" >

确定要删除这个报价版本吗?此操作不可恢复。

请输入管理员密码确认删除操作:

setDeletePassword(e.target.value)} size="large" />
); }; export default BudgetProjectDetail;