Files
yunhaifinance/company-finance-system/frontend/src/pages/construction/ConstructionMilestones.tsx
T

241 lines
7.2 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card, Typography, Button, Space, Tag, Spin, Empty, Timeline, Progress, Divider
} from 'antd';
import {
ArrowLeftOutlined, CheckCircleOutlined, ClockCircleOutlined,
SyncOutlined, CloseCircleOutlined
} from '@ant-design/icons';
import axios from 'axios';
import dayjs from 'dayjs';
const { Title, Paragraph, Text } = Typography;
// 节点状态配置
const STATUS_CONFIG: Record<string, {
color: string;
text: string;
icon: React.ReactNode;
timelineColor: string;
}> = {
pending: {
color: 'default',
text: '待开始',
icon: <ClockCircleOutlined />,
timelineColor: 'gray'
},
in_progress: {
color: 'processing',
text: '进行中',
icon: <SyncOutlined spin />,
timelineColor: 'blue'
},
completed: {
color: 'success',
text: '已完成',
icon: <CheckCircleOutlined />,
timelineColor: 'green'
},
cancelled: {
color: 'error',
text: '已取消',
icon: <CloseCircleOutlined />,
timelineColor: 'red'
},
};
interface Milestone {
id: number;
node_name: string;
node_type: string;
status: string;
due_date: string;
trigger_condition: string;
created_at: string;
}
const ConstructionMilestones: React.FC = () => {
const { id: projectId } = useParams<{ id: string }>();
const navigate = useNavigate();
const [isMobile, setIsMobile] = useState(false);
const [milestones, setMilestones] = useState<Milestone[]>([]);
const [projectInfo, setProjectInfo] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useEffect(() => {
if (projectId) {
fetchMilestones();
fetchProjectInfo();
}
}, [projectId]);
const fetchMilestones = async () => {
setLoading(true);
try {
const res = await axios.get(`/api/construction/projects/${projectId}/milestones`);
if (res.data.success) {
setMilestones(res.data.data);
}
} catch (error) {
console.error('获取节点列表失败:', error);
} finally {
setLoading(false);
}
};
const fetchProjectInfo = async () => {
try {
const res = await axios.get(`/api/projects/${projectId}`);
if (res.data.success) {
setProjectInfo(res.data.data);
}
} catch (error) {
console.error('获取项目信息失败:', error);
}
};
// 计算进度
const completedCount = milestones.filter(m => m.status === 'completed').length;
const totalCount = milestones.length;
const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
const renderTimelineItem = (milestone: Milestone, index: number) => {
const statusConfig = STATUS_CONFIG[milestone.status] || STATUS_CONFIG.pending;
return (
<Timeline.Item
key={milestone.id}
color={statusConfig.timelineColor}
dot={
<span style={{ fontSize: 16 }}>
{statusConfig.icon}
</span>
}
>
<Card
size="small"
style={{
marginBottom: 8,
borderRadius: 8,
background: milestone.status === 'completed' ? '#f6ffed' : '#fff',
}}
styles={{ body: { padding: 12 } }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Text strong style={{ fontSize: 14 }}>{milestone.node_name}</Text>
{milestone.trigger_condition && (
<Paragraph
type="secondary"
style={{ margin: '4px 0 0', fontSize: 12 }}
>
{milestone.trigger_condition}
</Paragraph>
)}
</div>
<Tag color={statusConfig.color} icon={statusConfig.icon}>
{statusConfig.text}
</Tag>
</div>
{milestone.due_date && (
<Text type="secondary" style={{ fontSize: 12, marginTop: 4, display: 'block' }}>
计划完成: {dayjs(milestone.due_date).format('YYYY-MM-DD')}
</Text>
)}
</Card>
</Timeline.Item>
);
};
return (
<div style={{
padding: isMobile ? 12 : 24,
maxWidth: 800,
margin: '0 auto'
}}>
{/* 页面头部 */}
<div style={{ marginBottom: isMobile ? 16 : 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/construction')}
/>
<div>
<Title level={isMobile ? 4 : 3} style={{ margin: 0 }}>
节点进度
</Title>
{projectInfo && (
<Text type="secondary" style={{ fontSize: 13 }}>
{projectInfo.name}
</Text>
)}
</div>
</div>
</div>
{/* 进度概览 */}
{!loading && milestones.length > 0 && (
<Card style={{ marginBottom: 16, borderRadius: 12 }}>
<div style={{ textAlign: 'center', marginBottom: 16 }}>
<Text type="secondary">整体进度</Text>
<Title level={2} style={{ margin: '8px 0 0' }}>{progressPercent}%</Title>
</div>
<Progress
percent={progressPercent}
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
/>
<div style={{ display: 'flex', justifyContent: 'center', gap: 24, marginTop: 16 }}>
<div style={{ textAlign: 'center' }}>
<Text strong style={{ fontSize: 20 }}>{completedCount}</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>已完成</Text>
</div>
<div style={{ textAlign: 'center' }}>
<Text strong style={{ fontSize: 20 }}>{totalCount - completedCount}</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>进行中</Text>
</div>
<div style={{ textAlign: 'center' }}>
<Text strong style={{ fontSize: 20 }}>{totalCount}</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>总节点</Text>
</div>
</div>
</Card>
)}
{/* 节点时间线 */}
{loading ? (
<div style={{ textAlign: 'center', padding: 60 }}>
<Spin size="large" />
</div>
) : milestones.length === 0 ? (
<Card style={{ borderRadius: 12 }}>
<Empty description="暂无施工节点">
<Text type="secondary">节点由项目经理在项目设置中配置</Text>
</Empty>
</Card>
) : (
<Card style={{ borderRadius: 12 }}>
<Timeline style={{ marginTop: 16 }}>
{milestones.map((milestone, index) => renderTimelineItem(milestone, index))}
</Timeline>
</Card>
)}
</div>
);
};
export default ConstructionMilestones;