抓变化
This commit is contained in:
parent
7d839712ca
commit
bed25c9317
48
main.js
48
main.js
|
|
@ -554,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'),
|
getProjects: () => ipcRenderer.invoke('get-projects'),
|
||||||
|
getProjectsRange: () => ipcRenderer.invoke('get-projects-range'),
|
||||||
getTreeStructure: () => ipcRenderer.invoke('get-tree-structure'),
|
getTreeStructure: () => ipcRenderer.invoke('get-tree-structure'),
|
||||||
filterProjects: (filters) => ipcRenderer.invoke('filter-projects', filters),
|
filterProjects: (filters) => ipcRenderer.invoke('filter-projects', filters),
|
||||||
updateProject: (project) => ipcRenderer.invoke('update-project', project),
|
updateProject: (project) => ipcRenderer.invoke('update-project', project),
|
||||||
|
|
|
||||||
769
src/App.js
769
src/App.js
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, {useState, useEffect, useRef} from 'react';
|
||||||
import { ConfigProvider, theme, Switch, Space, Modal, Progress, message } from 'antd';
|
import {ConfigProvider, theme, Switch, Space, Modal, Progress, message} from 'antd';
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
@ -16,401 +16,406 @@ import ProjectDetailForm from './components/ProjectDetailForm';
|
||||||
import ProjectWarningView from "./components/ProjectWarningView";
|
import ProjectWarningView from "./components/ProjectWarningView";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
const [treeData, setTreeData] = useState([]);
|
const [projectsDataRange, setProjectsDataRange] = useState([]);
|
||||||
const [selectedNode, setSelectedNode] = useState(null);
|
const [treeData, setTreeData] = useState([]);
|
||||||
const [selectedProjects, setSelectedProjects] = useState([]);
|
const [selectedNode, setSelectedNode] = useState(null);
|
||||||
const [activeFilter, setActiveFilter] = useState(null);
|
const [selectedProjects, setSelectedProjects] = useState([]);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [activeFilter, setActiveFilter] = useState(null);
|
||||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
const [searchText, setSearchText] = useState('');
|
||||||
const [currentProject, setCurrentProject] = useState(null);
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [currentProject, setCurrentProject] = useState(null);
|
||||||
const [isDarkMode] = useState(true); // 固定使用深色主题
|
const [loading, setLoading] = useState(false);
|
||||||
const [lastImportedFilePath, setLastImportedFilePath] = useState(null); // 记录最后导入的文件路径
|
const [isDarkMode] = useState(true); // 固定使用深色主题
|
||||||
|
const [lastImportedFilePath, setLastImportedFilePath] = useState(null); // 记录最后导入的文件路径
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 加载数据
|
|
||||||
const loadData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
// 获取项目数据
|
|
||||||
const projectsData = await window.electronAPI.getProjects();
|
|
||||||
setProjects(projectsData);
|
|
||||||
|
|
||||||
// 获取树状结构数据
|
|
||||||
const treeStructureData = await window.electronAPI.getTreeStructure();
|
|
||||||
|
|
||||||
// 如果树状结构为空,则从项目数据构建树状结构
|
|
||||||
if (treeStructureData.length === 0 && projectsData.length > 0) {
|
|
||||||
const tree = buildTreeFromProjects(projectsData);
|
|
||||||
setTreeData(tree);
|
|
||||||
} else {
|
|
||||||
setTreeData(treeStructureData);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载数据失败:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从项目数据构建树状结构
|
|
||||||
const buildTreeFromProjects = (projectsData) => {
|
|
||||||
// 总部节点
|
|
||||||
const headquarters = {
|
|
||||||
key: 'headquarters',
|
|
||||||
title: '总部',
|
|
||||||
level: 1,
|
|
||||||
children: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// 单位映射
|
|
||||||
const unitMap = {};
|
|
||||||
|
|
||||||
// 遍历项目数据,构建树状结构
|
|
||||||
projectsData.forEach(project => {
|
|
||||||
const { unit, construction_unit } = project;
|
|
||||||
|
|
||||||
// 如果单位不存在,则创建单位节点
|
|
||||||
if (!unitMap[unit]) {
|
|
||||||
unitMap[unit] = {
|
|
||||||
key: `unit-${unit}`,
|
|
||||||
title: unit,
|
|
||||||
level: 2,
|
|
||||||
children: []
|
|
||||||
};
|
|
||||||
headquarters.children.push(unitMap[unit]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果建设单位不为空,则添加建设单位节点
|
|
||||||
if (construction_unit && !unitMap[unit].children.find(child => child.title === construction_unit)) {
|
|
||||||
unitMap[unit].children.push({
|
|
||||||
key: `construction-${unit}-${construction_unit}`,
|
|
||||||
title: construction_unit,
|
|
||||||
level: 3,
|
|
||||||
isLeaf: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return [headquarters];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理树节点选择
|
|
||||||
const handleTreeSelect = (selectedKeys, info) => {
|
|
||||||
if (selectedKeys.length > 0) {
|
|
||||||
const key = selectedKeys[0];
|
|
||||||
setSelectedNode(key);
|
|
||||||
|
|
||||||
// 根据选中的节点筛选项目数据
|
|
||||||
let filters = {};
|
|
||||||
|
|
||||||
if (key === 'headquarters') {
|
|
||||||
// 总部节点,显示所有数据
|
|
||||||
setProjects(projects);
|
|
||||||
} else if (key.startsWith('unit-')) {
|
|
||||||
// 单位节点,筛选该单位的数据
|
|
||||||
const unit = key.replace('unit-', '');
|
|
||||||
filters = { unit };
|
|
||||||
} else if (key.startsWith('construction-')) {
|
|
||||||
// 建设单位节点,筛选该建设单位的数据
|
|
||||||
const parts = key.split('-');
|
|
||||||
const unit = parts[1];
|
|
||||||
const constructionUnit = parts.slice(2).join('-');
|
|
||||||
filters = { unit, constructionUnit };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用筛选
|
|
||||||
filterProjects(filters);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 筛选项目数据
|
|
||||||
const filterProjects = async (filters) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const filteredProjects = await window.electronAPI.filterProjects(filters);
|
|
||||||
setProjects(filteredProjects);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('筛选项目数据失败:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理工具栏筛选
|
|
||||||
const handleToolbarFilter = (filter) => {
|
|
||||||
setActiveFilter(filter === activeFilter ? null : filter);
|
|
||||||
|
|
||||||
// 根据筛选条件筛选项目数据
|
|
||||||
let filters = {};
|
|
||||||
|
|
||||||
if (filter === 'risk') {
|
|
||||||
filters = { riskLevel: '高风险' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用筛选
|
|
||||||
filterProjects(filters);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理项目搜索
|
|
||||||
const handleSearch = (text) => {
|
|
||||||
setSearchText(text);
|
|
||||||
|
|
||||||
// 根据搜索文本筛选项目数据
|
|
||||||
const filters = { subProjectName: text };
|
|
||||||
filterProjects(filters);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理项目选择
|
|
||||||
const handleProjectSelect = (selectedRowKeys) => {
|
|
||||||
setSelectedProjects(selectedRowKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理项目点击,打开详情弹窗
|
|
||||||
const handleProjectClick = (project) => {
|
|
||||||
setCurrentProject(project);
|
|
||||||
setDetailModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理项目更新
|
|
||||||
const handleProjectUpdate = async (updatedProject) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await window.electronAPI.updateProject(updatedProject);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// 更新成功,重新加载数据
|
|
||||||
loadData();
|
loadData();
|
||||||
setDetailModalVisible(false);
|
}, []);
|
||||||
} else {
|
|
||||||
console.error('更新项目数据失败:', result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新项目数据失败:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理Excel导入
|
// 加载数据
|
||||||
const handleImportExcel = async () => {
|
const loadData = async () => {
|
||||||
try {
|
|
||||||
// 选择Excel文件
|
|
||||||
const filePath = await window.electronAPI.selectExcelFile();
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
// 检查是否重复导入同一个文件
|
|
||||||
if (lastImportedFilePath === filePath) {
|
|
||||||
// 显示确认对话框
|
|
||||||
Modal.confirm({
|
|
||||||
title: '文件已导入',
|
|
||||||
content: '该文件已经导入过,重新导入会覆盖之前的记录。确定要继续吗?',
|
|
||||||
okText: '确定',
|
|
||||||
cancelText: '取消',
|
|
||||||
onOk: () => {
|
|
||||||
// 用户确认后继续导入
|
|
||||||
importFile(filePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 直接导入文件
|
|
||||||
importFile(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('导入Excel文件失败:', error);
|
|
||||||
message.error(`导入失败: ${error.message}`);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 导入文件的实际逻辑
|
|
||||||
const importFile = async (filePath) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// 创建进度对话框
|
|
||||||
const progressModal = Modal.info({
|
|
||||||
title: '导入Excel',
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
<p id="import-progress-message">正在准备导入...</p>
|
|
||||||
<Progress id="import-progress-bar" percent={0} status="active" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
icon: null,
|
|
||||||
okButtonProps: { style: { display: 'none' } },
|
|
||||||
maskClosable: false,
|
|
||||||
closable: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听导入进度
|
|
||||||
window.electronAPI.onImportProgress((data) => {
|
|
||||||
// 更新进度条和消息
|
|
||||||
const progressBar = document.getElementById('import-progress-bar');
|
|
||||||
const progressMessage = document.getElementById('import-progress-message');
|
|
||||||
|
|
||||||
if (progressBar && progressMessage) {
|
|
||||||
// 更新进度条
|
|
||||||
const antProgress = progressBar.querySelector('.ant-progress-bg');
|
|
||||||
if (antProgress) {
|
|
||||||
antProgress.style.width = `${data.progress}%`;
|
|
||||||
antProgress.setAttribute('aria-valuenow', data.progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新文本
|
|
||||||
progressMessage.textContent = data.message;
|
|
||||||
|
|
||||||
// 如果导入完成,关闭对话框并重新加载数据
|
|
||||||
if (data.status === 'complete') {
|
|
||||||
setTimeout(() => {
|
|
||||||
progressModal.destroy();
|
|
||||||
message.success('Excel导入成功');
|
|
||||||
// 重新加载数据
|
|
||||||
loadData();
|
|
||||||
setLoading(false);
|
|
||||||
}, 1000);
|
|
||||||
} else if (data.status === 'error') {
|
|
||||||
setTimeout(() => {
|
|
||||||
progressModal.destroy();
|
|
||||||
message.error(`导入失败: ${data.message}`);
|
|
||||||
setLoading(false);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 导入Excel文件
|
|
||||||
const result = await window.electronAPI.importExcel(filePath);
|
|
||||||
|
|
||||||
// 如果导入失败且没有进度事件处理,则显示错误消息
|
|
||||||
if (!result.success) {
|
|
||||||
progressModal.destroy();
|
|
||||||
message.error(`导入失败: ${result.error}`);
|
|
||||||
console.error('导入Excel文件失败:', result.error);
|
|
||||||
setLoading(false);
|
|
||||||
} else {
|
|
||||||
// 导入成功,更新最后导入的文件路径
|
|
||||||
setLastImportedFilePath(filePath);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('导入Excel文件失败:', error);
|
|
||||||
message.error(`导入失败: ${error.message}`);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理清除数据
|
|
||||||
const handleClearData = async () => {
|
|
||||||
// 显示确认对话框
|
|
||||||
Modal.confirm({
|
|
||||||
title: '确认清除数据',
|
|
||||||
content: '此操作将清除所有项目数据,无法恢复。确定要继续吗?',
|
|
||||||
okText: '确定',
|
|
||||||
okType: 'danger',
|
|
||||||
cancelText: '取消',
|
|
||||||
onOk: async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// 调用API清除数据
|
// 获取项目数据
|
||||||
const result = await window.electronAPI.clearAllData();
|
const projectsData = await window.electronAPI.getProjects();
|
||||||
|
setProjects(projectsData);
|
||||||
|
|
||||||
if (result.success) {
|
const projectsDataRange = await window.electronAPI.getProjectsRange();
|
||||||
// 清除成功,重置状态
|
setProjectsDataRange(projectsDataRange);
|
||||||
setProjects([]);
|
|
||||||
setTreeData([]);
|
|
||||||
setSelectedNode(null);
|
|
||||||
setSelectedProjects([]);
|
|
||||||
setActiveFilter(null);
|
|
||||||
setSearchText('');
|
|
||||||
|
|
||||||
// 显示成功提示
|
// 获取树状结构数据
|
||||||
Modal.success({
|
const treeStructureData = await window.electronAPI.getTreeStructure();
|
||||||
title: '操作成功',
|
|
||||||
content: '所有数据已清除'
|
// 如果树状结构为空,则从项目数据构建树状结构
|
||||||
});
|
if (treeStructureData.length === 0 && projectsData.length > 0) {
|
||||||
} else {
|
const tree = buildTreeFromProjects(projectsData);
|
||||||
// 显示错误提示
|
setTreeData(tree);
|
||||||
Modal.error({
|
} else {
|
||||||
title: '操作失败',
|
setTreeData(treeStructureData);
|
||||||
content: result.error || '清除数据时发生错误'
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清除数据失败:', error);
|
console.error('加载数据失败:', error);
|
||||||
// 显示错误提示
|
|
||||||
Modal.error({
|
|
||||||
title: '操作失败',
|
|
||||||
content: '清除数据时发生错误'
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
// 从项目数据构建树状结构
|
||||||
<ConfigProvider
|
const buildTreeFromProjects = (projectsData) => {
|
||||||
locale={zhCN}
|
// 总部节点
|
||||||
theme={{
|
const headquarters = {
|
||||||
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
key: 'headquarters',
|
||||||
token: {
|
title: '总部',
|
||||||
colorPrimary: '#1890ff',
|
level: 1,
|
||||||
},
|
children: []
|
||||||
}}
|
};
|
||||||
>
|
|
||||||
<div className="app-container">
|
|
||||||
{/* 工具栏 */}
|
|
||||||
<Toolbar
|
|
||||||
activeFilter={activeFilter}
|
|
||||||
onFilterChange={handleToolbarFilter}
|
|
||||||
onImportExcel={handleImportExcel}
|
|
||||||
onClearData={handleClearData}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 树状结构区 */}
|
// 单位映射
|
||||||
<TreeView
|
const unitMap = {};
|
||||||
treeData={treeData}
|
|
||||||
selectedNode={selectedNode}
|
|
||||||
onSelect={handleTreeSelect}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 数据展示区 */}
|
// 遍历项目数据,构建树状结构
|
||||||
<DataView
|
projectsData.forEach(project => {
|
||||||
projects={projects}
|
const {unit, construction_unit} = project;
|
||||||
selectedProjects={selectedProjects}
|
|
||||||
onSelect={handleProjectSelect}
|
|
||||||
onClick={handleProjectClick} // 传入点击事件处理函数
|
|
||||||
onSearch={handleSearch}
|
|
||||||
searchText={searchText}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 树状结构区 */}
|
// 如果单位不存在,则创建单位节点
|
||||||
<ProjectWarningView
|
if (!unitMap[unit]) {
|
||||||
projects={projects}
|
unitMap[unit] = {
|
||||||
loading={loading}
|
key: `unit-${unit}`,
|
||||||
onClick={handleProjectClick} // 传入点击事件处理函数
|
title: unit,
|
||||||
/>
|
level: 2,
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
headquarters.children.push(unitMap[unit]);
|
||||||
|
}
|
||||||
|
|
||||||
{/* 项目详情表单 */}
|
// 如果建设单位不为空,则添加建设单位节点
|
||||||
<ProjectDetailForm
|
if (construction_unit && !unitMap[unit].children.find(child => child.title === construction_unit)) {
|
||||||
visible={detailModalVisible}
|
unitMap[unit].children.push({
|
||||||
project={currentProject}
|
key: `construction-${unit}-${construction_unit}`,
|
||||||
onCancel={() => setDetailModalVisible(false)}
|
title: construction_unit,
|
||||||
onSave={handleProjectUpdate}
|
level: 3,
|
||||||
/>
|
isLeaf: true
|
||||||
</div>
|
});
|
||||||
</ConfigProvider>
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
|
return [headquarters];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理树节点选择
|
||||||
|
const handleTreeSelect = (selectedKeys, info) => {
|
||||||
|
if (selectedKeys.length > 0) {
|
||||||
|
const key = selectedKeys[0];
|
||||||
|
setSelectedNode(key);
|
||||||
|
|
||||||
|
// 根据选中的节点筛选项目数据
|
||||||
|
let filters = {};
|
||||||
|
|
||||||
|
if (key === 'headquarters') {
|
||||||
|
// 总部节点,显示所有数据
|
||||||
|
setProjects(projects);
|
||||||
|
} else if (key.startsWith('unit-')) {
|
||||||
|
// 单位节点,筛选该单位的数据
|
||||||
|
const unit = key.replace('unit-', '');
|
||||||
|
filters = {unit};
|
||||||
|
} else if (key.startsWith('construction-')) {
|
||||||
|
// 建设单位节点,筛选该建设单位的数据
|
||||||
|
const parts = key.split('-');
|
||||||
|
const unit = parts[1];
|
||||||
|
const constructionUnit = parts.slice(2).join('-');
|
||||||
|
filters = {unit, constructionUnit};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用筛选
|
||||||
|
filterProjects(filters);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 筛选项目数据
|
||||||
|
const filterProjects = async (filters) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const filteredProjects = await window.electronAPI.filterProjects(filters);
|
||||||
|
setProjects(filteredProjects);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('筛选项目数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理工具栏筛选
|
||||||
|
const handleToolbarFilter = (filter) => {
|
||||||
|
setActiveFilter(filter === activeFilter ? null : filter);
|
||||||
|
|
||||||
|
// 根据筛选条件筛选项目数据
|
||||||
|
let filters = {};
|
||||||
|
|
||||||
|
if (filter === 'risk') {
|
||||||
|
filters = {riskLevel: '高风险'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用筛选
|
||||||
|
filterProjects(filters);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理项目搜索
|
||||||
|
const handleSearch = (text) => {
|
||||||
|
setSearchText(text);
|
||||||
|
|
||||||
|
// 根据搜索文本筛选项目数据
|
||||||
|
const filters = {subProjectName: text};
|
||||||
|
filterProjects(filters);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理项目选择
|
||||||
|
const handleProjectSelect = (selectedRowKeys) => {
|
||||||
|
setSelectedProjects(selectedRowKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理项目点击,打开详情弹窗
|
||||||
|
const handleProjectClick = (project) => {
|
||||||
|
setCurrentProject(project);
|
||||||
|
setDetailModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理项目更新
|
||||||
|
const handleProjectUpdate = async (updatedProject) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.updateProject(updatedProject);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 更新成功,重新加载数据
|
||||||
|
loadData();
|
||||||
|
setDetailModalVisible(false);
|
||||||
|
} else {
|
||||||
|
console.error('更新项目数据失败:', result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新项目数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理Excel导入
|
||||||
|
const handleImportExcel = async () => {
|
||||||
|
try {
|
||||||
|
// 选择Excel文件
|
||||||
|
const filePath = await window.electronAPI.selectExcelFile();
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
// 检查是否重复导入同一个文件
|
||||||
|
if (lastImportedFilePath === filePath) {
|
||||||
|
// 显示确认对话框
|
||||||
|
Modal.confirm({
|
||||||
|
title: '文件已导入',
|
||||||
|
content: '该文件已经导入过,重新导入会覆盖之前的记录。确定要继续吗?',
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
// 用户确认后继续导入
|
||||||
|
importFile(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 直接导入文件
|
||||||
|
importFile(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入Excel文件失败:', error);
|
||||||
|
message.error(`导入失败: ${error.message}`);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入文件的实际逻辑
|
||||||
|
const importFile = async (filePath) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// 创建进度对话框
|
||||||
|
const progressModal = Modal.info({
|
||||||
|
title: '导入Excel',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<p id="import-progress-message">正在准备导入...</p>
|
||||||
|
<Progress id="import-progress-bar" percent={0} status="active"/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
icon: null,
|
||||||
|
okButtonProps: {style: {display: 'none'}},
|
||||||
|
maskClosable: false,
|
||||||
|
closable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听导入进度
|
||||||
|
window.electronAPI.onImportProgress((data) => {
|
||||||
|
// 更新进度条和消息
|
||||||
|
const progressBar = document.getElementById('import-progress-bar');
|
||||||
|
const progressMessage = document.getElementById('import-progress-message');
|
||||||
|
|
||||||
|
if (progressBar && progressMessage) {
|
||||||
|
// 更新进度条
|
||||||
|
const antProgress = progressBar.querySelector('.ant-progress-bg');
|
||||||
|
if (antProgress) {
|
||||||
|
antProgress.style.width = `${data.progress}%`;
|
||||||
|
antProgress.setAttribute('aria-valuenow', data.progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新文本
|
||||||
|
progressMessage.textContent = data.message;
|
||||||
|
|
||||||
|
// 如果导入完成,关闭对话框并重新加载数据
|
||||||
|
if (data.status === 'complete') {
|
||||||
|
setTimeout(() => {
|
||||||
|
progressModal.destroy();
|
||||||
|
message.success('Excel导入成功');
|
||||||
|
// 重新加载数据
|
||||||
|
loadData();
|
||||||
|
setLoading(false);
|
||||||
|
}, 1000);
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
setTimeout(() => {
|
||||||
|
progressModal.destroy();
|
||||||
|
message.error(`导入失败: ${data.message}`);
|
||||||
|
setLoading(false);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导入Excel文件
|
||||||
|
const result = await window.electronAPI.importExcel(filePath);
|
||||||
|
|
||||||
|
// 如果导入失败且没有进度事件处理,则显示错误消息
|
||||||
|
if (!result.success) {
|
||||||
|
progressModal.destroy();
|
||||||
|
message.error(`导入失败: ${result.error}`);
|
||||||
|
console.error('导入Excel文件失败:', result.error);
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
// 导入成功,更新最后导入的文件路径
|
||||||
|
setLastImportedFilePath(filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入Excel文件失败:', error);
|
||||||
|
message.error(`导入失败: ${error.message}`);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理清除数据
|
||||||
|
const handleClearData = async () => {
|
||||||
|
// 显示确认对话框
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认清除数据',
|
||||||
|
content: '此操作将清除所有项目数据,无法恢复。确定要继续吗?',
|
||||||
|
okText: '确定',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 调用API清除数据
|
||||||
|
const result = await window.electronAPI.clearAllData();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 清除成功,重置状态
|
||||||
|
setProjects([]);
|
||||||
|
setTreeData([]);
|
||||||
|
setSelectedNode(null);
|
||||||
|
setSelectedProjects([]);
|
||||||
|
setActiveFilter(null);
|
||||||
|
setSearchText('');
|
||||||
|
|
||||||
|
// 显示成功提示
|
||||||
|
Modal.success({
|
||||||
|
title: '操作成功',
|
||||||
|
content: '所有数据已清除'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 显示错误提示
|
||||||
|
Modal.error({
|
||||||
|
title: '操作失败',
|
||||||
|
content: result.error || '清除数据时发生错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除数据失败:', error);
|
||||||
|
// 显示错误提示
|
||||||
|
Modal.error({
|
||||||
|
title: '操作失败',
|
||||||
|
content: '清除数据时发生错误'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
locale={zhCN}
|
||||||
|
theme={{
|
||||||
|
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||||
|
token: {
|
||||||
|
colorPrimary: '#1890ff',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="app-container">
|
||||||
|
{/* 工具栏 */}
|
||||||
|
<Toolbar
|
||||||
|
activeFilter={activeFilter}
|
||||||
|
onFilterChange={handleToolbarFilter}
|
||||||
|
onImportExcel={handleImportExcel}
|
||||||
|
onClearData={handleClearData}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 树状结构区 */}
|
||||||
|
<TreeView
|
||||||
|
treeData={treeData}
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
onSelect={handleTreeSelect}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 数据展示区 */}
|
||||||
|
<DataView
|
||||||
|
projects={projects}
|
||||||
|
selectedProjects={selectedProjects}
|
||||||
|
onSelect={handleProjectSelect}
|
||||||
|
onClick={handleProjectClick} // 传入点击事件处理函数
|
||||||
|
onSearch={handleSearch}
|
||||||
|
searchText={searchText}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 树状结构区 */}
|
||||||
|
<ProjectWarningView
|
||||||
|
projects={projects}
|
||||||
|
projectsDataRange={projectsDataRange}
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleProjectClick} // 传入点击事件处理函数
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 项目详情表单 */}
|
||||||
|
<ProjectDetailForm
|
||||||
|
visible={detailModalVisible}
|
||||||
|
project={currentProject}
|
||||||
|
onCancel={() => setDetailModalVisible(false)}
|
||||||
|
onSave={handleProjectUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,129 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Spin } from 'antd';
|
import {Spin} from 'antd';
|
||||||
|
|
||||||
const ProjectWarningView = ({ projects = [], loading = false,onClick}) => {
|
const ProjectWarningView = ({projects = [], projectsDataRange = [], loading = false, onClick}) => {
|
||||||
const warningFields = [
|
const warningFields = [
|
||||||
{ key: 'new_team', label: '新班组', message: '存在新班组,请做好班组入场管理。' },
|
{key: 'new_team', label: '新班组', message: '存在新班组,请做好班组入场管理。'},
|
||||||
{ key: 'new_homework_content', label: '新的作业内容', message: '存在新的作业内容,请加强现场管控。' },
|
{key: 'new_homework_content', label: '新的作业内容', message: '存在新的作业内容,请加强现场管控。'},
|
||||||
{ key: 'change_homework_method', label: '改变作业方法', message: '存在改变作业方法,请及时核查施工方案编审、方案交底及人员、机具准备情况。' },
|
{
|
||||||
{ key: 'changes_geographical', label: '地理环境的变化', message: '存在作业环境的变化,请及时核查施工方案编审、方案交底及人员、机具准备情况。' },
|
key: 'change_homework_method',
|
||||||
{ key: 'changes_meteorological', label: '气象环境的变化', message: '存在气象预警,请关注天气变化,做好应对措施。' },
|
label: '改变作业方法',
|
||||||
{ key: 'changes_social', label: '社会环境的变化', message: '存在社会环境变化,请合理安排作业计划,避免人员失控。' },
|
message: '存在改变作业方法,请及时核查施工方案编审、方案交底及人员、机具准备情况。'
|
||||||
{ key: 'changes_management', label: '管理要求的变化', message: '存在管理要求的变化,请加强现场巡查力度,严防无计划作业。' },
|
},
|
||||||
{ key: 'changes_homework_plan', label: '作业计划的变化', message: '存在作业计划的变化,请做好施工力量配备。' },
|
{
|
||||||
{ key: 'changes_management_personnel', 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) => {
|
const hasWarnings = (item) => {
|
||||||
if (typeof item !== 'object') return false;
|
if (typeof item !== 'object') return false;
|
||||||
if (item.new_members || item.new_high_altitude || item.new_hired_general) return true;
|
if (item.new_members || item.new_high_altitude || item.new_hired_general) return true;
|
||||||
return warningFields.some(({ key }) => item[key]);
|
return warningFields.some(({key}) => item[key]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredProjects = projects.filter(hasWarnings);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -130,6 +233,49 @@ const ProjectWarningView = ({ projects = [], loading = false,onClick}) => {
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : 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',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
投产计划30天内导线展放进度不足80%,请关注。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue