Compare commits
4 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
a59c925d56 | |
|
|
3ce40dd036 | |
|
|
bed25c9317 | |
|
|
7d839712ca |
87
main.js
87
main.js
|
|
@ -131,6 +131,19 @@ function initDatabase() {
|
|||
risk_tips TEXT, -- 隐患提示/工作要求
|
||||
completion_time TEXT, -- 完成时间
|
||||
next_review_time TEXT, -- 下次梳理时间
|
||||
|
||||
new_team INTEGER, --新进班组数量
|
||||
new_members INTEGER, --新进班组骨干数量
|
||||
new_high_altitude INTEGER, --新进高空人员数量
|
||||
new_hired_general INTEGER, --新进一般人员数量
|
||||
new_homework_content TEXT, --新的作业内容
|
||||
change_homework_method TEXT, --改变作业方法
|
||||
changes_geographical TEXT, --地理环境的变化
|
||||
changes_meteorological TEXT, --气象环境的变化
|
||||
changes_social TEXT, --社会环境的变化
|
||||
changes_management TEXT, --管理要求的变化
|
||||
changes_homework_plan TEXT, --作业计划的变化
|
||||
changes_management_personnel TEXT, --管理人员的变化
|
||||
remarks TEXT -- 备注
|
||||
)
|
||||
`);
|
||||
|
|
@ -469,6 +482,20 @@ ipcMain.handle('update-project', (event, project) => {
|
|||
risk_tips = ?,
|
||||
completion_time = ?,
|
||||
next_review_time = ?,
|
||||
|
||||
new_team = ?,
|
||||
new_members = ?,
|
||||
new_high_altitude = ?,
|
||||
new_hired_general = ?,
|
||||
new_homework_content= ?,
|
||||
change_homework_method = ?,
|
||||
changes_geographical = ?,
|
||||
changes_meteorological = ?,
|
||||
changes_social = ?,
|
||||
changes_management = ?,
|
||||
changes_homework_plan = ?,
|
||||
changes_management_personnel = ?,
|
||||
|
||||
remarks = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
|
@ -503,6 +530,18 @@ ipcMain.handle('update-project', (event, project) => {
|
|||
project.risk_tips,
|
||||
project.completion_time,
|
||||
project.next_review_time,
|
||||
project.new_team,
|
||||
project.new_members,
|
||||
project.new_high_altitude,
|
||||
project.new_hired_general,
|
||||
project.new_homework_content,
|
||||
project.change_homework_method,
|
||||
project.changes_geographical,
|
||||
project.changes_meteorological,
|
||||
project.changes_social,
|
||||
project.changes_management,
|
||||
project.changes_homework_plan,
|
||||
project.changes_management_personnel,
|
||||
project.remarks,
|
||||
project.id
|
||||
], function (err) {
|
||||
|
|
@ -515,3 +554,51 @@ ipcMain.handle('update-project', (event, project) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 获取数据库中的项目数据
|
||||
ipcMain.handle('get-projects-range', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = `
|
||||
WITH filtered AS (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN major_project_name LIKE '%变电工程%' THEN '变电工程'
|
||||
WHEN major_project_name LIKE '%线路工程%' THEN '线路工程'
|
||||
END AS project_type,
|
||||
participants_count
|
||||
FROM projects
|
||||
WHERE (major_project_name LIKE '%变电工程%'
|
||||
OR major_project_name LIKE '%线路工程%')
|
||||
AND current_status = '在施'
|
||||
),
|
||||
ordered AS (
|
||||
SELECT
|
||||
project_type,
|
||||
participants_count,
|
||||
ROW_NUMBER() OVER (PARTITION BY project_type ORDER BY participants_count) AS rn,
|
||||
COUNT(*) OVER (PARTITION BY project_type) AS total_count
|
||||
FROM filtered
|
||||
),
|
||||
percentiles AS (
|
||||
SELECT
|
||||
project_type,
|
||||
MAX(CASE WHEN rn = MAX(1, CAST(total_count * 0.05 AS INTEGER)) THEN participants_count END) AS p5,
|
||||
MAX(CASE WHEN rn = MAX(1, CAST(total_count * 0.95 AS INTEGER)) THEN participants_count END) AS p95
|
||||
FROM ordered
|
||||
GROUP BY project_type
|
||||
)
|
||||
SELECT * FROM percentiles;
|
||||
`;
|
||||
|
||||
db.all(sql, (err, rows) => {
|
||||
if (err) {
|
||||
console.error('获取项目数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows || []);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
|
||||
// 数据库操作
|
||||
getProjects: () => ipcRenderer.invoke('get-projects'),
|
||||
getProjectsRange: () => ipcRenderer.invoke('get-projects-range'),
|
||||
getTreeStructure: () => ipcRenderer.invoke('get-tree-structure'),
|
||||
filterProjects: (filters) => ipcRenderer.invoke('filter-projects', filters),
|
||||
updateProject: (project) => ipcRenderer.invoke('update-project', project),
|
||||
|
|
|
|||
33
src/App.js
33
src/App.js
|
|
@ -13,10 +13,12 @@ import Toolbar from './components/Toolbar';
|
|||
import TreeView from './components/TreeView';
|
||||
import DataView from './components/DataView';
|
||||
import ProjectDetailForm from './components/ProjectDetailForm';
|
||||
import ProjectWarningView from "./components/ProjectWarningView";
|
||||
|
||||
function App() {
|
||||
// 状态管理
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [projectsDataRange, setProjectsDataRange] = useState([]);
|
||||
const [treeData, setTreeData] = useState([]);
|
||||
const [selectedNode, setSelectedNode] = useState(null);
|
||||
const [selectedProjects, setSelectedProjects] = useState([]);
|
||||
|
|
@ -27,6 +29,8 @@ function App() {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [isDarkMode] = useState(true); // 固定使用深色主题
|
||||
const [lastImportedFilePath, setLastImportedFilePath] = useState(null); // 记录最后导入的文件路径
|
||||
// 根据选中的节点筛选项目数据
|
||||
let select = {};
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
|
|
@ -41,6 +45,9 @@ function App() {
|
|||
const projectsData = await window.electronAPI.getProjects();
|
||||
setProjects(projectsData);
|
||||
|
||||
const projectsDataRange = await window.electronAPI.getProjectsRange();
|
||||
setProjectsDataRange(projectsDataRange);
|
||||
|
||||
// 获取树状结构数据
|
||||
const treeStructureData = await window.electronAPI.getTreeStructure();
|
||||
|
||||
|
|
@ -106,8 +113,6 @@ function App() {
|
|||
const key = selectedKeys[0];
|
||||
setSelectedNode(key);
|
||||
|
||||
// 根据选中的节点筛选项目数据
|
||||
let filters = {};
|
||||
|
||||
if (key === 'headquarters') {
|
||||
// 总部节点,显示所有数据
|
||||
|
|
@ -115,17 +120,17 @@ function App() {
|
|||
} else if (key.startsWith('unit-')) {
|
||||
// 单位节点,筛选该单位的数据
|
||||
const unit = key.replace('unit-', '');
|
||||
filters = { unit };
|
||||
select = {unit};
|
||||
} else if (key.startsWith('construction-')) {
|
||||
// 建设单位节点,筛选该建设单位的数据
|
||||
const parts = key.split('-');
|
||||
const unit = parts[1];
|
||||
const constructionUnit = parts.slice(2).join('-');
|
||||
filters = { unit, constructionUnit };
|
||||
select = {unit, constructionUnit};
|
||||
}
|
||||
|
||||
// 应用筛选
|
||||
filterProjects(filters);
|
||||
filterProjects(select);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -154,16 +159,17 @@ function App() {
|
|||
}
|
||||
|
||||
// 应用筛选
|
||||
filterProjects(filters);
|
||||
filterProjects({ ...filters, ...select });
|
||||
};
|
||||
|
||||
// 处理项目搜索
|
||||
const handleSearch = (text) => {
|
||||
setSearchText(text);
|
||||
|
||||
setSearchText(text);
|
||||
console.log(select)
|
||||
// 根据搜索文本筛选项目数据
|
||||
const filters = {subProjectName: text};
|
||||
filterProjects(filters);
|
||||
filterProjects({ ...filters, ...select });
|
||||
};
|
||||
|
||||
// 处理项目选择
|
||||
|
|
@ -256,6 +262,7 @@ function App() {
|
|||
const progressMessage = document.getElementById('import-progress-message');
|
||||
|
||||
if (progressBar && progressMessage) {
|
||||
const antProgressText = progressBar.querySelector('.ant-progress-text');
|
||||
// 更新进度条
|
||||
const antProgress = progressBar.querySelector('.ant-progress-bg');
|
||||
if (antProgress) {
|
||||
|
|
@ -265,7 +272,7 @@ function App() {
|
|||
|
||||
// 更新文本
|
||||
progressMessage.textContent = data.message;
|
||||
|
||||
antProgressText.textContent = `${data.progress}%`;
|
||||
// 如果导入完成,关闭对话框并重新加载数据
|
||||
if (data.status === 'complete') {
|
||||
setTimeout(() => {
|
||||
|
|
@ -393,6 +400,14 @@ function App() {
|
|||
loading={loading}
|
||||
/>
|
||||
|
||||
{/* 树状结构区 */}
|
||||
<ProjectWarningView
|
||||
projects={projects}
|
||||
projectsDataRange={projectsDataRange}
|
||||
loading={loading}
|
||||
onClick={handleProjectClick} // 传入点击事件处理函数
|
||||
/>
|
||||
|
||||
{/* 项目详情表单 */}
|
||||
<ProjectDetailForm
|
||||
visible={detailModalVisible}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import { Modal, Form, Input, Select, Switch, Button } from 'antd';
|
||||
import {Modal, Form, Input, Select, Switch, Button, InputNumber} from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const {Option} = Select;
|
||||
|
|
@ -37,6 +37,12 @@ const ProjectDetailForm = ({ visible, project, onCancel, onSave }) => {
|
|||
open={visible}
|
||||
width={800}
|
||||
onCancel={onCancel}
|
||||
bodyStyle={{
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'scroll',
|
||||
msOverflowStyle: 'none', // IE, Edge
|
||||
scrollbarWidth: 'none' // Firefox
|
||||
}}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
取消
|
||||
|
|
@ -98,14 +104,26 @@ const ProjectDetailForm = ({ visible, project, onCancel, onSave }) => {
|
|||
<Form.Item label="下次梳理时间" name="next_review_time">
|
||||
<Input placeholder="YYYY-MM-DD"/>
|
||||
</Form.Item>
|
||||
<Form.Item label="参建人数" name="participants_count">
|
||||
<Input type="number" />
|
||||
<Form.Item label="参建人数" name="participants_count"
|
||||
rules={[
|
||||
{type: 'number', min: 1, message: '必须为正整数'},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{width: '100%'}}/>
|
||||
|
||||
</Form.Item>
|
||||
<Form.Item label="新班组进场数量" name="new_team_count">
|
||||
<Input type="number" />
|
||||
<Form.Item label="新班组进场数量" name="new_team_count" rules={[
|
||||
{type: 'number', min: 1, message: '必须为正整数'},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="新人进场数量" name="new_person_count">
|
||||
<Input type="number" />
|
||||
<Form.Item label="新人进场数量" name="new_person_count" rules={[
|
||||
{type: 'number', min: 1, message: '必须为正整数'},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{width: '100%'}}/>
|
||||
|
||||
</Form.Item>
|
||||
<Form.Item label="带班人姓名、电话" name="leader_info">
|
||||
<Input/>
|
||||
|
|
@ -135,6 +153,133 @@ const ProjectDetailForm = ({ visible, project, onCancel, onSave }) => {
|
|||
<Form.Item label="隐患提示/工作要求" name="risk_tips">
|
||||
<TextArea rows={2}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div>
|
||||
<p style={{fontSize: '20px', fontWeight: 'bold'}}>人的变化</p><Form.Item
|
||||
label="新进班组数量"
|
||||
name="new_team"
|
||||
rules={[
|
||||
{ message: '请输入新进班组数量'},
|
||||
{type: 'number', min: 1, message: '必须为正整数'},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="新进班组骨干数量"
|
||||
name="new_members"
|
||||
rules={[
|
||||
{message: '请输入新进班组骨干数量'},
|
||||
{type: 'number', min: 1, message: '必须为正整数'},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="新进高空人员数量"
|
||||
name="new_high_altitude"
|
||||
rules={[
|
||||
{message: '请输入新进高空人员数量'},
|
||||
{type: 'number', min: 1, message: '必须为正整数'},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="新进一般人员数量"
|
||||
name="new_hired_general"
|
||||
rules={[
|
||||
{message: '请输入新进一般人员数量'},
|
||||
{type: 'number', min: 1, message: '必须为正整数'},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div>
|
||||
<p style={{fontSize: '20px', fontWeight: 'bold'}}>机的变化</p>
|
||||
<Form.Item label="新的作业内容" name="new_homework_content">
|
||||
<Select allowClear placeholder="请选择新的作业内容">
|
||||
<Option value="同一班组作业类型调整">同一班组作业类型调整</Option>
|
||||
<Option value="在运变电站新增作业">在运变电站新增作业</Option>
|
||||
<Option value="工程转序">工程转序</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="改变作业方法" name="change_homework_method">
|
||||
<Select allowClear placeholder="请选择改变作业方法">
|
||||
<Option value="同一部位基础开挖由机械变为人工">同一部位基础开挖由机械变为人工</Option>
|
||||
<Option
|
||||
value="同一部位铁塔组立由起重机变为抱杆">同一部位铁塔组立由起重机变为抱杆</Option>
|
||||
<Option value="同一跨越物改变跨越方式">同一跨越物改变跨越方式</Option>
|
||||
<Option value="同一放线段改变放线方式">同一放线段改变放线方式</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div>
|
||||
<p style={{fontSize: '20px', fontWeight: 'bold'}}>环境的变化</p>
|
||||
<Form.Item label="地理环境的变化" name="changes_geographical">
|
||||
<Select allowClear placeholder="请选择地理环境的变化">
|
||||
<Option value="新增带电线路(引起近电作业)">新增带电线路(引起近电作业)</Option>
|
||||
<Option value="新增带电线路(未引起近电作业)">新增带电线路(未引起近电作业)</Option>
|
||||
<Option
|
||||
value="新增跨越物(新投入使用铁路、高速、电力线等线性工程)">新增跨越物(新投入使用铁路、高速、电力线等线性工程)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="气象环境的变化" name="changes_meteorological">
|
||||
<Select allowClear placeholder="请选择气象环境的变化">
|
||||
<Option value="降雪预警">降雪预警</Option>
|
||||
<Option value="降雨预警">降雨预警</Option>
|
||||
<Option value="大风预警">大风预警</Option>
|
||||
<Option value="高温预警">高温预警</Option>
|
||||
<Option value="低温预警">低温预警</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="社会环境的变化" name="changes_social">
|
||||
<Select allowClear placeholder="请选择社会环境的变化">
|
||||
<Option value="外部协调引起阻工(属地协调不畅)">外部协调引起阻工(属地协调不畅)</Option>
|
||||
<Option value="物资供应滞后">物资供应滞后</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div>
|
||||
<p style={{fontSize: '20px', fontWeight: 'bold'}}>管理的变化</p>
|
||||
<Form.Item label="管理要求的变化" name="changes_management">
|
||||
<Select allowClear placeholder="请选择管理要求的变化">
|
||||
<Option value="投产计划提前(工期紧张)">投产计划提前(工期紧张)</Option>
|
||||
<Option value="特殊时段作业">特殊时段作业</Option>
|
||||
<Option value="停电计划调整">停电计划调整</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="作业计划的变化" name="changes_homework_plan">
|
||||
<Select allowClear placeholder="请选择作业计划的变化">
|
||||
<Option value="原作业计划未按期执行,顺延至今">原作业计划未按期执行,顺延至今</Option>
|
||||
<Option value="下周作业计划无法执行,临时变更">下周作业计划无法执行,临时变更</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="管理人员的变化" name="changes_management_personnel">
|
||||
<Select allowClear placeholder="请选择管理人员变化">
|
||||
<Option value="施工项目部关键人员变化">施工项目部关键人员变化</Option>
|
||||
<Option value="监理项目部关键人员变化">监理项目部关键人员变化</Option>
|
||||
<Option value="业主项目部关键人员变化">业主项目部关键人员变化</Option>
|
||||
<Option value="施工单位主要负责人、分管领导调整">施工单位主要负责人、分管领导调整</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="remarks">
|
||||
<TextArea rows={2}/>
|
||||
</Form.Item>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,303 @@
|
|||
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;
|
||||
|
|
@ -36,7 +36,7 @@ const TreeView = ({ treeData, selectedNode, onSelect, loading }) => {
|
|||
return (
|
||||
<div className="tree-view">
|
||||
<div style={{padding: '10px', fontWeight: 'bold', borderBottom: '1px solid var(--vscode-sidebar-border)'}}>
|
||||
项目结构
|
||||
项目预警
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
|
|
|
|||
|
|
@ -202,13 +202,46 @@ class ExcelService {
|
|||
return defaultValue;
|
||||
}
|
||||
// 如果是日期类型,转换为ISO格式
|
||||
if (fieldName === '实际开工时间' || fieldName === '计划竣工时间' || fieldName === '完成时间' || fieldName === '下次梳理时间(注意与 隐患提示/工作要求 对应)') {
|
||||
const date = new Date(value);
|
||||
if (!isNaN(date.getTime())) {
|
||||
if (
|
||||
fieldName === '实际开工时间' ||
|
||||
fieldName === '计划竣工时间' ||
|
||||
fieldName === '完成时间' ||
|
||||
fieldName === '下次梳理时间(注意与 隐患提示/工作要求 对应)'
|
||||
) {
|
||||
if (value == null) return ''; // 空值处理
|
||||
|
||||
return date.toISOString().split('T')[0]; // 只保留日期部分
|
||||
// ✅ Excel 日期序列号(一般从 25569 开始)
|
||||
if (typeof value === 'number') {
|
||||
if (value > 1e12 && value < 1e14) {
|
||||
// ✅ 毫秒级时间戳
|
||||
const date = new Date(value);
|
||||
if (!isNaN(date.getTime())) return date.toISOString().split('T')[0];
|
||||
} else {
|
||||
// ✅ Excel 序列号转日期(从 1900-01-01 起,第1天是 1)
|
||||
const utcDays = Math.floor(value - 25569);
|
||||
const utcValue = utcDays * 86400; // 转秒
|
||||
const date = new Date(utcValue * 1000);
|
||||
if (!isNaN(date.getTime())) return date.toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
let parsed = new Date(value);
|
||||
if (!isNaN(parsed.getTime())) return parsed.toISOString().split('T')[0];
|
||||
|
||||
// ✅ 支持“5-12”、“5月12日” 等格式
|
||||
const matched = value.match(/^(\d{1,2})[月/-](\d{1,2})/);
|
||||
if (matched) {
|
||||
const year = new Date().getFullYear(); // 默认今年
|
||||
const mm = matched[1].padStart(2, '0');
|
||||
const dd = matched[2].padStart(2, '0');
|
||||
return `${year}-${mm}-${dd}`;
|
||||
}
|
||||
}
|
||||
|
||||
return ''; // 未能解析,返回空字符串
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
|
|
@ -270,7 +303,10 @@ class ExcelService {
|
|||
processedRows++;
|
||||
if (processedRows % 10 === 0 || processedRows === totalRows) {
|
||||
const progress = Math.floor(50 + (processedRows / totalRows) * 20); // 50%-70%的进度
|
||||
|
||||
if (global.mainWindow) {
|
||||
|
||||
console.log("进度+++",progress)
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
status: 'saving',
|
||||
progress: progress,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,13 @@ code {
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
.project-warning-view {
|
||||
width: 400px;
|
||||
background-color: var(--vscode-sidebar-bg);
|
||||
border-right: 1px solid var(--vscode-sidebar-border);
|
||||
}
|
||||
|
||||
.data-view {
|
||||
flex: 1;
|
||||
background-color: var(--vscode-editor-bg);
|
||||
|
|
@ -79,7 +86,7 @@ code {
|
|||
.ant-tree {
|
||||
background-color: transparent !important;
|
||||
color: var(--vscode-text) !important;
|
||||
}
|
||||
}mm
|
||||
|
||||
.ant-tree-node-content-wrapper:hover {
|
||||
background-color: var(--vscode-hover-item) !important;
|
||||
|
|
|
|||
Loading…
Reference in New Issue