241 lines
7.2 KiB
TypeScript
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;
|