Initial commit: ERP system with advance verification fixes
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user