304 lines
15 KiB
JavaScript
304 lines
15 KiB
JavaScript
import React from 'react';
|
||
import {Spin} from 'antd';
|
||
|
||
const ProjectWarningView = ({projects = [], projectsDataRange = [], loading = false, onClick}) => {
|
||
const warningFields = [
|
||
{key: 'new_team', label: '新班组', message: '存在新班组,请做好班组入场管理。'},
|
||
{key: 'new_homework_content', label: '新的作业内容', message: '存在新的作业内容,请加强现场管控。'},
|
||
{
|
||
key: 'change_homework_method',
|
||
label: '改变作业方法',
|
||
message: '存在改变作业方法,请及时核查施工方案编审、方案交底及人员、机具准备情况。'
|
||
},
|
||
{
|
||
key: 'changes_geographical',
|
||
label: '地理环境的变化',
|
||
message: '存在作业环境的变化,请及时核查施工方案编审、方案交底及人员、机具准备情况。'
|
||
},
|
||
{key: 'changes_meteorological', label: '气象环境的变化', message: '存在气象预警,请关注天气变化,做好应对措施。'},
|
||
{key: 'changes_social', label: '社会环境的变化', message: '存在社会环境变化,请合理安排作业计划,避免人员失控。'},
|
||
{
|
||
key: 'changes_management',
|
||
label: '管理要求的变化',
|
||
message: '存在管理要求的变化,请加强现场巡查力度,严防无计划作业。'
|
||
},
|
||
{key: 'changes_homework_plan', label: '作业计划的变化', message: '存在作业计划的变化,请做好施工力量配备。'},
|
||
{key: 'changes_management_personnel', label: '管理人员的变化', message: '存在管理人员的变化,请加强现场管控。'},
|
||
];
|
||
|
||
|
||
const hasWarnings = (item) => {
|
||
if (typeof item !== 'object') return false;
|
||
if (item.new_members || item.new_high_altitude || item.new_hired_general) return true;
|
||
return warningFields.some(({key}) => item[key]);
|
||
};
|
||
|
||
const mismatchWarning = (item) => {
|
||
if (typeof item !== 'object') return false;
|
||
if (item.current_status !== '在施') return false;
|
||
const progressText = item.current_progress || '';
|
||
const planText = item.next_week_plan || '';
|
||
|
||
// 识别当前进度中的“完成数/总数”结构,例如:325.5/620
|
||
const extractRates = (text) => {
|
||
const matches = [...text.matchAll(/(\d+(\.\d+)?)\s*\/\s*(\d+(\.\d+)?)/g)];
|
||
return matches.map(match => {
|
||
const done = parseFloat(match[1]);
|
||
const total = parseFloat(match[3]);
|
||
if (!isNaN(done) && !isNaN(total) && total > 0) {
|
||
return done / total;
|
||
}
|
||
return null;
|
||
}).filter(r => r !== null);
|
||
};
|
||
|
||
const progressRates = extractRates(progressText);
|
||
const hasLowProgress = !progressRates.some(rate => rate > 0.7 && rate != null);
|
||
// 如果下周计划中包含关键作业,就说明任务已经安排了
|
||
const criticalTasks = [
|
||
'组塔', '导线展放'
|
||
];
|
||
const hasHeavyNextPlan = criticalTasks.some(task => planText.includes(task));
|
||
|
||
// 核心判断逻辑
|
||
return hasLowProgress && hasHeavyNextPlan;
|
||
};
|
||
// 转成以项目类型为key的对象,方便查找
|
||
const rangeMap = projectsDataRange.reduce((acc, item) => {
|
||
acc[item.project_type] = {min: item.p5, max: item.p95};
|
||
return acc;
|
||
}, {});
|
||
|
||
function getRangeByProjectName(projectName) {
|
||
if (typeof projectName !== 'string') return null;
|
||
|
||
if (projectName.includes('变电工程')) {
|
||
return rangeMap['变电工程'];
|
||
} else if (projectName.includes('线路工程')) {
|
||
return rangeMap['线路工程'];
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const projectsRangeWarning = (item) => {
|
||
if (typeof item !== 'object') return false;
|
||
if (item.current_status !== '在施') return false;
|
||
const range = getRangeByProjectName(item.major_project_name);
|
||
if (!range) return false; // 未知工程类型,不判断
|
||
if (item.participants_count == 0) return false;
|
||
return !(item.participants_count > range.min && item.participants_count < range.max);
|
||
}
|
||
|
||
|
||
const productionPlanScaleWarning = (item) => {
|
||
if (typeof item !== 'object') return false;
|
||
if (item.current_status !== '在施') return false;
|
||
if (!item.planned_completion_time) return false;
|
||
|
||
const completionDate = new Date(item.planned_completion_time);
|
||
const now = new Date();
|
||
const daysToCompletion = (completionDate - now) / (1000 * 60 * 60 * 24);
|
||
if (daysToCompletion < 0 || daysToCompletion > 30) return false;
|
||
|
||
// 仅判断“导线展放”类型
|
||
const scale = item.project_scale || '';
|
||
if (!scale.includes('导线')) return false;
|
||
|
||
// 判断进度是否 < 80%
|
||
const progressStr = item.current_progress || '';
|
||
const progressMatch = progressStr.match(/(\d+(\.\d+)?)%/);
|
||
if (!progressMatch) return false;
|
||
|
||
const progress = parseFloat(progressMatch[1]);
|
||
return progress < 80;
|
||
};
|
||
|
||
|
||
|
||
const filteredProjects = projects.filter(item =>
|
||
hasWarnings(item) ||
|
||
mismatchWarning(item) ||
|
||
projectsRangeWarning(item) ||
|
||
productionPlanScaleWarning(item)
|
||
);
|
||
|
||
|
||
|
||
return (
|
||
<div
|
||
className="project-warning-view"
|
||
>
|
||
<div
|
||
style={{
|
||
padding: '16px 20px',
|
||
fontWeight: '700',
|
||
fontSize: '18px',
|
||
userSelect: 'none',
|
||
borderBottom: '1px solid var(--vscode-sidebar-border)'
|
||
}}
|
||
>
|
||
工程预警
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
padding: '20px',
|
||
overflowY: 'auto',
|
||
flexGrow: 1,
|
||
maxHeight: '90%',
|
||
scrollbarWidth: 'thin',
|
||
scrollbarColor: '#c1c1c1 transparent',
|
||
}}
|
||
|
||
>
|
||
<Spin spinning={loading} tip="加载中...">
|
||
{!loading && (
|
||
<>
|
||
{filteredProjects.length > 0 ? (
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
||
gap: '24px',
|
||
}}
|
||
>
|
||
{filteredProjects.map((item, index) => (
|
||
<div
|
||
key={index}
|
||
style={{
|
||
border: '1px solid #ddd',
|
||
|
||
borderRadius: '12px',
|
||
padding: '20px',
|
||
boxShadow: '0 6px 15px rgba(0,0,0,0.07)',
|
||
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '12px',
|
||
userSelect: 'none',
|
||
}}
|
||
onClick={() => onClick(item)}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.transform = 'translateY(-6px) scale(1.02)';
|
||
e.currentTarget.style.boxShadow = '0 12px 24px rgba(0,0,0,0.15)';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||
e.currentTarget.style.boxShadow = '0 6px 15px rgba(0,0,0,0.07)';
|
||
}}
|
||
title={typeof item === 'string' ? item : item.sub_project_name || '未命名项目'}
|
||
>
|
||
<div
|
||
style={{
|
||
fontWeight: '700',
|
||
fontSize: '16px',
|
||
whiteSpace: 'nowrap',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
maxWidth: '100%',
|
||
}}
|
||
>
|
||
{typeof item === 'string' ? item : item.sub_project_name || '未命名项目'}
|
||
</div>
|
||
|
||
{(item.new_members || item.new_high_altitude || item.new_hired_general) && (
|
||
<div
|
||
style={{
|
||
fontSize: '14px',
|
||
color: '#e64c3c',
|
||
fontWeight: '600',
|
||
borderLeft: '4px solid #e64c3c',
|
||
paddingLeft: '10px',
|
||
}}
|
||
>
|
||
新人: 存在新人员,请做好人员“面对面”核实。
|
||
</div>
|
||
)}
|
||
|
||
{warningFields.map(({key, label, message}) =>
|
||
item[key] ? (
|
||
<div
|
||
key={key}
|
||
style={{
|
||
fontSize: '14px',
|
||
color: '#e64c3c',
|
||
fontWeight: '600',
|
||
borderLeft: '4px solid #e64c3c',
|
||
paddingLeft: '10px',
|
||
}}
|
||
>
|
||
{label}: {message}
|
||
</div>
|
||
) : null
|
||
)}
|
||
|
||
{mismatchWarning(item) && (
|
||
<div
|
||
style={{
|
||
fontSize: '14px',
|
||
color: '#e64c3c',
|
||
fontWeight: '600',
|
||
borderLeft: '4px solid #e64c3c',
|
||
paddingLeft: '10px',
|
||
}}
|
||
>
|
||
工程进展与下周作业计划不匹配,请注意。
|
||
</div>
|
||
)}
|
||
|
||
{projectsRangeWarning(item) && (
|
||
<div
|
||
style={{
|
||
fontSize: '14px',
|
||
color: '#e64c3c',
|
||
fontWeight: '600',
|
||
borderLeft: '4px solid #e64c3c',
|
||
paddingLeft: '10px',
|
||
}}
|
||
>
|
||
工程参建人数与作业内容不匹配,请注意。
|
||
</div>
|
||
)}
|
||
|
||
{productionPlanScaleWarning(item) && (
|
||
<div
|
||
style={{
|
||
fontSize: '14px',
|
||
color: '#e64c3c',
|
||
fontWeight: '600',
|
||
borderLeft: '4px solid #e64c3c',
|
||
paddingLeft: '10px',
|
||
}}
|
||
>
|
||
投产计划与当前工程进度不匹配,请注意。
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div
|
||
style={{
|
||
textAlign: 'center',
|
||
color: '#aaa',
|
||
fontSize: '15px',
|
||
marginTop: '50px',
|
||
userSelect: 'none',
|
||
}}
|
||
>
|
||
暂无项目预警
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</Spin>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ProjectWarningView;
|