Compare commits
4 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
a59c925d56 | |
|
|
3ce40dd036 | |
|
|
bed25c9317 | |
|
|
7d839712ca |
825
main.js
825
main.js
|
|
@ -1,12 +1,12 @@
|
|||
const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
|
||||
const {app, BrowserWindow, ipcMain, dialog, Menu} = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
let isDev;
|
||||
import('electron-is-dev').then(module => {
|
||||
isDev = module.default;
|
||||
isDev = module.default;
|
||||
}).catch(err => {
|
||||
console.error('Failed to load electron-is-dev:', err);
|
||||
isDev = false;
|
||||
console.error('Failed to load electron-is-dev:', err);
|
||||
isDev = false;
|
||||
});
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const ExcelService = require('./src/services/ExcelService');
|
||||
|
|
@ -18,88 +18,88 @@ let mainWindow;
|
|||
let db;
|
||||
|
||||
function createWindow() {
|
||||
// 创建浏览器窗口
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
title: '值班助手工具',
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
// 将mainWindow设为全局变量,以便其他模块可以访问
|
||||
global.mainWindow = mainWindow;
|
||||
// 创建浏览器窗口
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
title: '值班助手工具',
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
// 加载应用
|
||||
const loadAppUrl = () => {
|
||||
const devUrl = 'http://localhost:3000';
|
||||
const prodUrl = `file://${path.join(__dirname, 'build/index.html')}`;
|
||||
const url = isDev ? devUrl : prodUrl;
|
||||
// 将mainWindow设为全局变量,以便其他模块可以访问
|
||||
global.mainWindow = mainWindow;
|
||||
|
||||
mainWindow.loadURL(url).catch((err) => {
|
||||
if (isDev) {
|
||||
console.log(`首次加载失败,10秒后重试...`);
|
||||
let retries = 3;
|
||||
const retryLoad = () => {
|
||||
mainWindow.loadURL(devUrl).catch((retryErr) => {
|
||||
if (retries-- > 0) {
|
||||
console.log(`剩余重试次数:${retries}`);
|
||||
setTimeout(retryLoad, 10000);
|
||||
} else {
|
||||
mainWindow.loadURL(`data:text/html,<h1 style="color:red">无法连接开发服务器,请检查:</h1>
|
||||
// 加载应用
|
||||
const loadAppUrl = () => {
|
||||
const devUrl = 'http://localhost:3000';
|
||||
const prodUrl = `file://${path.join(__dirname, 'build/index.html')}`;
|
||||
const url = isDev ? devUrl : prodUrl;
|
||||
|
||||
mainWindow.loadURL(url).catch((err) => {
|
||||
if (isDev) {
|
||||
console.log(`首次加载失败,10秒后重试...`);
|
||||
let retries = 3;
|
||||
const retryLoad = () => {
|
||||
mainWindow.loadURL(devUrl).catch((retryErr) => {
|
||||
if (retries-- > 0) {
|
||||
console.log(`剩余重试次数:${retries}`);
|
||||
setTimeout(retryLoad, 10000);
|
||||
} else {
|
||||
mainWindow.loadURL(`data:text/html,<h1 style="color:red">无法连接开发服务器,请检查:</h1>
|
||||
<ol>
|
||||
<li>React开发服务器是否启动(npm run react-start)</li>
|
||||
<li>端口3000是否被占用</li>
|
||||
<li>网络连接是否正常</li>
|
||||
</ol>`);
|
||||
}
|
||||
});
|
||||
};
|
||||
setTimeout(retryLoad, 10000);
|
||||
} else {
|
||||
mainWindow.loadURL(prodUrl).catch(() => {
|
||||
mainWindow.loadURL(`data:text/html,<h1 style="color:red">生产文件加载失败,请重新编译(npm run react-build)</h1>`);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
setTimeout(retryLoad, 10000);
|
||||
} else {
|
||||
mainWindow.loadURL(prodUrl).catch(() => {
|
||||
mainWindow.loadURL(`data:text/html,<h1 style="color:red">生产文件加载失败,请重新编译(npm run react-build)</h1>`);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// 开发模式立即尝试加载,生产模式直接加载
|
||||
if (isDev) {
|
||||
setTimeout(loadAppUrl, 3000); // 给开发服务器更多启动时间
|
||||
} else {
|
||||
loadAppUrl();
|
||||
}
|
||||
|
||||
// 开发者工具已禁用
|
||||
|
||||
// 当window被关闭时,触发下面的事件
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
// 关闭数据库连接
|
||||
if (db) {
|
||||
db.close();
|
||||
// 开发模式立即尝试加载,生产模式直接加载
|
||||
if (isDev) {
|
||||
setTimeout(loadAppUrl, 3000); // 给开发服务器更多启动时间
|
||||
} else {
|
||||
loadAppUrl();
|
||||
}
|
||||
});
|
||||
|
||||
// 开发者工具已禁用
|
||||
|
||||
// 当window被关闭时,触发下面的事件
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
// 关闭数据库连接
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
function initDatabase() {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const dbPath = path.join(userDataPath, 'datatools.db');
|
||||
|
||||
// 确保目录存在
|
||||
fs.ensureDirSync(path.dirname(dbPath));
|
||||
|
||||
// 连接数据库
|
||||
db = new sqlite3.Database(dbPath);
|
||||
|
||||
// 创建表(如果不存在)
|
||||
db.serialize(() => {
|
||||
db.run(`
|
||||
const userDataPath = app.getPath('userData');
|
||||
const dbPath = path.join(userDataPath, 'datatools.db');
|
||||
|
||||
// 确保目录存在
|
||||
fs.ensureDirSync(path.dirname(dbPath));
|
||||
|
||||
// 连接数据库
|
||||
db = new sqlite3.Database(dbPath);
|
||||
|
||||
// 创建表(如果不存在)
|
||||
db.serialize(() => {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
unit TEXT, -- 单位
|
||||
|
|
@ -131,11 +131,24 @@ function initDatabase() {
|
|||
risk_tips TEXT, -- 隐患提示/工作要求
|
||||
completion_time TEXT, -- 完成时间
|
||||
next_review_time TEXT, -- 下次梳理时间
|
||||
remarks 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 -- 备注
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS tree_structure (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
headquarters TEXT, -- 总部
|
||||
|
|
@ -147,298 +160,298 @@ function initDatabase() {
|
|||
)
|
||||
`);
|
||||
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_unit ON projects(unit)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_construction_unit ON projects(construction_unit)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_sub_project_name ON projects(sub_project_name)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_current_risk_level ON projects(current_risk_level)`);
|
||||
});
|
||||
|
||||
return db;
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_unit ON projects(unit)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_construction_unit ON projects(construction_unit)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_sub_project_name ON projects(sub_project_name)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_current_risk_level ON projects(current_risk_level)`);
|
||||
});
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
// 创建中文菜单模板
|
||||
const menuTemplate = [
|
||||
{
|
||||
label: '文件',
|
||||
submenu: [
|
||||
{ role: 'quit', label: '退出' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
submenu: [
|
||||
{ role: 'undo', label: '撤销' },
|
||||
{ role: 'redo', label: '恢复' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut', label: '剪切' },
|
||||
{ role: 'copy', label: '复制' },
|
||||
{ role: 'paste', label: '粘贴' },
|
||||
{ role: 'delete', label: '删除' },
|
||||
{ role: 'selectAll', label: '全选' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '视图',
|
||||
submenu: [
|
||||
{ role: 'reload', label: '重新加载' },
|
||||
{ role: 'forceReload', label: '强制重新加载' },
|
||||
{ role: 'toggleDevTools', label: '开发者工具' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom', label: '重置缩放' },
|
||||
{ role: 'zoomIn', label: '放大' },
|
||||
{ role: 'zoomOut', label: '缩小' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen', label: '全屏' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '窗口',
|
||||
submenu: [
|
||||
{ role: 'minimize', label: '最小化' },
|
||||
{ role: 'zoom', label: '缩放' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'front', label: '前置所有窗口' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '帮助',
|
||||
submenu: [
|
||||
{
|
||||
label: '关于',
|
||||
click: () => {
|
||||
dialog.showMessageBox({
|
||||
title: '关于',
|
||||
message: '值班助手工具 v1.0.0'
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
label: '文件',
|
||||
submenu: [
|
||||
{role: 'quit', label: '退出'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
submenu: [
|
||||
{role: 'undo', label: '撤销'},
|
||||
{role: 'redo', label: '恢复'},
|
||||
{type: 'separator'},
|
||||
{role: 'cut', label: '剪切'},
|
||||
{role: 'copy', label: '复制'},
|
||||
{role: 'paste', label: '粘贴'},
|
||||
{role: 'delete', label: '删除'},
|
||||
{role: 'selectAll', label: '全选'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '视图',
|
||||
submenu: [
|
||||
{role: 'reload', label: '重新加载'},
|
||||
{role: 'forceReload', label: '强制重新加载'},
|
||||
{role: 'toggleDevTools', label: '开发者工具'},
|
||||
{type: 'separator'},
|
||||
{role: 'resetZoom', label: '重置缩放'},
|
||||
{role: 'zoomIn', label: '放大'},
|
||||
{role: 'zoomOut', label: '缩小'},
|
||||
{type: 'separator'},
|
||||
{role: 'togglefullscreen', label: '全屏'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '窗口',
|
||||
submenu: [
|
||||
{role: 'minimize', label: '最小化'},
|
||||
{role: 'zoom', label: '缩放'},
|
||||
{type: 'separator'},
|
||||
{role: 'front', label: '前置所有窗口'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '帮助',
|
||||
submenu: [
|
||||
{
|
||||
label: '关于',
|
||||
click: () => {
|
||||
dialog.showMessageBox({
|
||||
title: '关于',
|
||||
message: '值班助手工具 v1.0.0'
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 当Electron完成初始化并准备创建浏览器窗口时调用此方法
|
||||
app.whenReady().then(() => {
|
||||
// 设置中文菜单
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(menu);
|
||||
createWindow();
|
||||
db = initDatabase();
|
||||
|
||||
// 初始化Excel服务
|
||||
const excelService = new ExcelService(db);
|
||||
|
||||
// 注册import-excel事件处理器(确保只注册一次)
|
||||
ipcMain.removeHandler('import-excel'); // 先移除已有的处理器
|
||||
ipcMain.handle('import-excel', async (event, filePath) => {
|
||||
try {
|
||||
console.log('处理Excel导入请求...');
|
||||
return await excelService.importExcel(filePath);
|
||||
} catch (error) {
|
||||
console.error('导入Excel错误:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// 注册clear-all-data事件处理器
|
||||
ipcMain.removeHandler('clear-all-data'); // 先移除已有的处理器
|
||||
ipcMain.handle('clear-all-data', async () => {
|
||||
try {
|
||||
console.log('处理清除数据请求...');
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.run('DELETE FROM projects', function(err) {
|
||||
if (err) {
|
||||
console.error('清除项目数据失败:', err);
|
||||
reject({ success: false, error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
db.run('DELETE FROM tree_structure', function(err) {
|
||||
if (err) {
|
||||
console.error('清除树状结构数据失败:', err);
|
||||
reject({ success: false, error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置自增ID
|
||||
db.run('DELETE FROM sqlite_sequence WHERE name IN (\'projects\', \'tree_structure\')', function(err) {
|
||||
if (err) {
|
||||
console.error('重置自增ID失败:', err);
|
||||
// 不影响主要功能,继续执行
|
||||
}
|
||||
|
||||
resolve({ success: true, message: '所有数据已清除' });
|
||||
});
|
||||
// 设置中文菜单
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(menu);
|
||||
createWindow();
|
||||
db = initDatabase();
|
||||
|
||||
// 初始化Excel服务
|
||||
const excelService = new ExcelService(db);
|
||||
|
||||
// 注册import-excel事件处理器(确保只注册一次)
|
||||
ipcMain.removeHandler('import-excel'); // 先移除已有的处理器
|
||||
ipcMain.handle('import-excel', async (event, filePath) => {
|
||||
try {
|
||||
console.log('处理Excel导入请求...');
|
||||
return await excelService.importExcel(filePath);
|
||||
} catch (error) {
|
||||
console.error('导入Excel错误:', error);
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
});
|
||||
|
||||
// 注册clear-all-data事件处理器
|
||||
ipcMain.removeHandler('clear-all-data'); // 先移除已有的处理器
|
||||
ipcMain.handle('clear-all-data', async () => {
|
||||
try {
|
||||
console.log('处理清除数据请求...');
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.run('DELETE FROM projects', function (err) {
|
||||
if (err) {
|
||||
console.error('清除项目数据失败:', err);
|
||||
reject({success: false, error: err.message});
|
||||
return;
|
||||
}
|
||||
|
||||
db.run('DELETE FROM tree_structure', function (err) {
|
||||
if (err) {
|
||||
console.error('清除树状结构数据失败:', err);
|
||||
reject({success: false, error: err.message});
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置自增ID
|
||||
db.run('DELETE FROM sqlite_sequence WHERE name IN (\'projects\', \'tree_structure\')', function (err) {
|
||||
if (err) {
|
||||
console.error('重置自增ID失败:', err);
|
||||
// 不影响主要功能,继续执行
|
||||
}
|
||||
|
||||
resolve({success: true, message: '所有数据已清除'});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('清除数据错误:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// 在macOS上,当点击dock图标并且没有其他窗口打开时,通常在应用程序中重新创建一个窗口
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('清除数据错误:', error);
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// 在macOS上,当点击dock图标并且没有其他窗口打开时,通常在应用程序中重新创建一个窗口
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 当所有窗口关闭时退出应用
|
||||
app.on('window-all-closed', () => {
|
||||
// 在macOS上,除非用户用Cmd + Q确定地退出,否则绝大部分应用及其菜单栏会保持激活
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
// 在macOS上,除非用户用Cmd + Q确定地退出,否则绝大部分应用及其菜单栏会保持激活
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// 处理Excel文件选择
|
||||
ipcMain.handle('select-excel-file', async () => {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'Excel Files', extensions: ['xlsx', 'xls'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filePaths[0];
|
||||
const {canceled, filePaths} = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{name: 'Excel Files', extensions: ['xlsx', 'xls']}
|
||||
]
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filePaths[0];
|
||||
});
|
||||
|
||||
// 获取数据库中的项目数据
|
||||
ipcMain.handle('get-projects', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT * FROM projects', (err, rows) => {
|
||||
if (err) {
|
||||
console.error('获取项目数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows || []);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT * FROM projects', (err, rows) => {
|
||||
if (err) {
|
||||
console.error('获取项目数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows || []);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 获取树状结构数据
|
||||
ipcMain.handle('get-tree-structure', () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT * FROM tree_structure ORDER BY node_level, id', (err, rows) => {
|
||||
if (err) {
|
||||
console.error('获取树状结构数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
// 转换为Ant Design Tree组件所需的格式
|
||||
const treeData = [];
|
||||
const nodeMap = {};
|
||||
|
||||
// 首先找到总部节点
|
||||
const headquartersNodes = rows.filter(node => node.node_level === 1);
|
||||
|
||||
// 处理每个总部节点
|
||||
headquartersNodes.forEach(hqNode => {
|
||||
const headquartersTreeNode = {
|
||||
key: `headquarters-${hqNode.id}`,
|
||||
title: hqNode.headquarters,
|
||||
level: 1,
|
||||
children: []
|
||||
};
|
||||
|
||||
// 添加到树数据和节点映射
|
||||
treeData.push(headquartersTreeNode);
|
||||
nodeMap[hqNode.id] = headquartersTreeNode;
|
||||
|
||||
// 找到该总部下的所有单位节点
|
||||
const unitNodes = rows.filter(node =>
|
||||
node.node_level === 2 && node.parent_id === hqNode.id
|
||||
);
|
||||
|
||||
// 处理每个单位节点
|
||||
unitNodes.forEach(unitNode => {
|
||||
const unitTreeNode = {
|
||||
key: `unit-${unitNode.unit}`,
|
||||
title: unitNode.unit,
|
||||
level: 2,
|
||||
children: []
|
||||
};
|
||||
|
||||
// 添加到总部节点的子节点和节点映射
|
||||
headquartersTreeNode.children.push(unitTreeNode);
|
||||
nodeMap[unitNode.id] = unitTreeNode;
|
||||
|
||||
// 找到该单位下的所有建设单位节点
|
||||
const constructionNodes = rows.filter(node =>
|
||||
node.node_level === 3 && node.parent_id === unitNode.id
|
||||
);
|
||||
|
||||
// 处理每个建设单位节点
|
||||
constructionNodes.forEach(constructionNode => {
|
||||
const constructionTreeNode = {
|
||||
key: `construction-${unitNode.unit}-${constructionNode.construction_unit}`,
|
||||
title: constructionNode.construction_unit,
|
||||
level: 3,
|
||||
isLeaf: true
|
||||
};
|
||||
|
||||
// 添加到单位节点的子节点
|
||||
unitTreeNode.children.push(constructionTreeNode);
|
||||
});
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT * FROM tree_structure ORDER BY node_level, id', (err, rows) => {
|
||||
if (err) {
|
||||
console.error('获取树状结构数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
// 转换为Ant Design Tree组件所需的格式
|
||||
const treeData = [];
|
||||
const nodeMap = {};
|
||||
|
||||
// 首先找到总部节点
|
||||
const headquartersNodes = rows.filter(node => node.node_level === 1);
|
||||
|
||||
// 处理每个总部节点
|
||||
headquartersNodes.forEach(hqNode => {
|
||||
const headquartersTreeNode = {
|
||||
key: `headquarters-${hqNode.id}`,
|
||||
title: hqNode.headquarters,
|
||||
level: 1,
|
||||
children: []
|
||||
};
|
||||
|
||||
// 添加到树数据和节点映射
|
||||
treeData.push(headquartersTreeNode);
|
||||
nodeMap[hqNode.id] = headquartersTreeNode;
|
||||
|
||||
// 找到该总部下的所有单位节点
|
||||
const unitNodes = rows.filter(node =>
|
||||
node.node_level === 2 && node.parent_id === hqNode.id
|
||||
);
|
||||
|
||||
// 处理每个单位节点
|
||||
unitNodes.forEach(unitNode => {
|
||||
const unitTreeNode = {
|
||||
key: `unit-${unitNode.unit}`,
|
||||
title: unitNode.unit,
|
||||
level: 2,
|
||||
children: []
|
||||
};
|
||||
|
||||
// 添加到总部节点的子节点和节点映射
|
||||
headquartersTreeNode.children.push(unitTreeNode);
|
||||
nodeMap[unitNode.id] = unitTreeNode;
|
||||
|
||||
// 找到该单位下的所有建设单位节点
|
||||
const constructionNodes = rows.filter(node =>
|
||||
node.node_level === 3 && node.parent_id === unitNode.id
|
||||
);
|
||||
|
||||
// 处理每个建设单位节点
|
||||
constructionNodes.forEach(constructionNode => {
|
||||
const constructionTreeNode = {
|
||||
key: `construction-${unitNode.unit}-${constructionNode.construction_unit}`,
|
||||
title: constructionNode.construction_unit,
|
||||
level: 3,
|
||||
isLeaf: true
|
||||
};
|
||||
|
||||
// 添加到单位节点的子节点
|
||||
unitTreeNode.children.push(constructionTreeNode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log('树状结构数据转换完成,节点数量:', treeData.length);
|
||||
resolve(treeData);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('树状结构数据转换完成,节点数量:', treeData.length);
|
||||
resolve(treeData);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 根据条件筛选项目数据
|
||||
ipcMain.handle('filter-projects', (event, filters) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let query = 'SELECT * FROM projects WHERE 1=1';
|
||||
const params = [];
|
||||
|
||||
if (filters.unit) {
|
||||
query += ' AND unit = ?';
|
||||
params.push(filters.unit);
|
||||
}
|
||||
|
||||
if (filters.constructionUnit) {
|
||||
query += ' AND construction_unit = ?';
|
||||
params.push(filters.constructionUnit);
|
||||
}
|
||||
|
||||
if (filters.subProjectName) {
|
||||
query += ' AND sub_project_name LIKE ?';
|
||||
params.push(`%${filters.subProjectName}%`);
|
||||
}
|
||||
|
||||
if (filters.riskLevel) {
|
||||
query += ' AND current_risk_level = ?';
|
||||
params.push(filters.riskLevel);
|
||||
}
|
||||
|
||||
db.all(query, params, (err, rows) => {
|
||||
if (err) {
|
||||
console.error('筛选项目数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows || []);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let query = 'SELECT * FROM projects WHERE 1=1';
|
||||
const params = [];
|
||||
|
||||
if (filters.unit) {
|
||||
query += ' AND unit = ?';
|
||||
params.push(filters.unit);
|
||||
}
|
||||
|
||||
if (filters.constructionUnit) {
|
||||
query += ' AND construction_unit = ?';
|
||||
params.push(filters.constructionUnit);
|
||||
}
|
||||
|
||||
if (filters.subProjectName) {
|
||||
query += ' AND sub_project_name LIKE ?';
|
||||
params.push(`%${filters.subProjectName}%`);
|
||||
}
|
||||
|
||||
if (filters.riskLevel) {
|
||||
query += ' AND current_risk_level = ?';
|
||||
params.push(filters.riskLevel);
|
||||
}
|
||||
|
||||
db.all(query, params, (err, rows) => {
|
||||
if (err) {
|
||||
console.error('筛选项目数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows || []);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 更新项目数据
|
||||
ipcMain.handle('update-project', (event, project) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = `
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = `
|
||||
UPDATE projects SET
|
||||
unit = ?,
|
||||
project_number = ?,
|
||||
|
|
@ -469,49 +482,123 @@ 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 = ?
|
||||
`;
|
||||
|
||||
db.run(query, [
|
||||
project.unit,
|
||||
project.project_number,
|
||||
project.safety_code,
|
||||
project.major_project_name,
|
||||
project.sub_project_name,
|
||||
project.construction_scope,
|
||||
project.project_scale,
|
||||
project.safety_director,
|
||||
project.construction_unit,
|
||||
project.supervision_unit,
|
||||
project.construction_company,
|
||||
project.project_location,
|
||||
project.actual_start_time,
|
||||
project.planned_completion_time,
|
||||
project.current_progress,
|
||||
project.current_status,
|
||||
project.participants_count,
|
||||
project.new_team_count,
|
||||
project.new_person_count,
|
||||
project.leader_info,
|
||||
project.next_week_plan,
|
||||
project.next_week_condition,
|
||||
project.is_schedule_tight,
|
||||
project.has_off_book_matters,
|
||||
project.current_risk_level,
|
||||
project.risk_judgment_reason,
|
||||
project.risk_tips,
|
||||
project.completion_time,
|
||||
project.next_review_time,
|
||||
project.remarks,
|
||||
project.id
|
||||
], function(err) {
|
||||
if (err) {
|
||||
console.error('更新项目数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ success: true, changes: this.changes });
|
||||
}
|
||||
|
||||
db.run(query, [
|
||||
project.unit,
|
||||
project.project_number,
|
||||
project.safety_code,
|
||||
project.major_project_name,
|
||||
project.sub_project_name,
|
||||
project.construction_scope,
|
||||
project.project_scale,
|
||||
project.safety_director,
|
||||
project.construction_unit,
|
||||
project.supervision_unit,
|
||||
project.construction_company,
|
||||
project.project_location,
|
||||
project.actual_start_time,
|
||||
project.planned_completion_time,
|
||||
project.current_progress,
|
||||
project.current_status,
|
||||
project.participants_count,
|
||||
project.new_team_count,
|
||||
project.new_person_count,
|
||||
project.leader_info,
|
||||
project.next_week_plan,
|
||||
project.next_week_condition,
|
||||
project.is_schedule_tight,
|
||||
project.has_off_book_matters,
|
||||
project.current_risk_level,
|
||||
project.risk_judgment_reason,
|
||||
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) {
|
||||
if (err) {
|
||||
console.error('更新项目数据失败:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({success: true, changes: this.changes});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 获取数据库中的项目数据
|
||||
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 || []);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
49
preload.js
49
preload.js
|
|
@ -1,28 +1,29 @@
|
|||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
const {contextBridge, ipcRenderer} = require('electron');
|
||||
|
||||
// 暴露安全的API给渲染进程
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 文件操作
|
||||
selectExcelFile: () => ipcRenderer.invoke('select-excel-file'),
|
||||
|
||||
// 数据库操作
|
||||
getProjects: () => ipcRenderer.invoke('get-projects'),
|
||||
getTreeStructure: () => ipcRenderer.invoke('get-tree-structure'),
|
||||
filterProjects: (filters) => ipcRenderer.invoke('filter-projects', filters),
|
||||
updateProject: (project) => ipcRenderer.invoke('update-project', project),
|
||||
deleteProject: (projectId) => ipcRenderer.invoke('delete-project', projectId),
|
||||
|
||||
// Excel处理
|
||||
importExcel: (filePath) => ipcRenderer.invoke('import-excel', filePath),
|
||||
|
||||
// 数据清除
|
||||
clearAllData: () => ipcRenderer.invoke('clear-all-data'),
|
||||
|
||||
// 事件监听
|
||||
onImportProgress: (callback) => {
|
||||
// 移除之前的监听器,避免重复
|
||||
ipcRenderer.removeAllListeners('import-progress');
|
||||
// 添加新的监听器
|
||||
ipcRenderer.on('import-progress', (event, data) => callback(data));
|
||||
}
|
||||
// 文件操作
|
||||
selectExcelFile: () => ipcRenderer.invoke('select-excel-file'),
|
||||
|
||||
// 数据库操作
|
||||
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),
|
||||
deleteProject: (projectId) => ipcRenderer.invoke('delete-project', projectId),
|
||||
|
||||
// Excel处理
|
||||
importExcel: (filePath) => ipcRenderer.invoke('import-excel', filePath),
|
||||
|
||||
// 数据清除
|
||||
clearAllData: () => ipcRenderer.invoke('clear-all-data'),
|
||||
|
||||
// 事件监听
|
||||
onImportProgress: (callback) => {
|
||||
// 移除之前的监听器,避免重复
|
||||
ipcRenderer.removeAllListeners('import-progress');
|
||||
// 添加新的监听器
|
||||
ipcRenderer.on('import-progress', (event, data) => callback(data));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
783
src/App.js
783
src/App.js
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { ConfigProvider, theme, Switch, Space, Modal, Progress, message } from 'antd';
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
import {ConfigProvider, theme, Switch, Space, Modal, Progress, message} from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
|
@ -13,396 +13,411 @@ 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 [treeData, setTreeData] = useState([]);
|
||||
const [selectedNode, setSelectedNode] = useState(null);
|
||||
const [selectedProjects, setSelectedProjects] = useState([]);
|
||||
const [activeFilter, setActiveFilter] = useState(null);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [currentProject, setCurrentProject] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isDarkMode] = useState(true); // 固定使用深色主题
|
||||
const [lastImportedFilePath, setLastImportedFilePath] = useState(null); // 记录最后导入的文件路径
|
||||
// 状态管理
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [projectsDataRange, setProjectsDataRange] = useState([]);
|
||||
const [treeData, setTreeData] = useState([]);
|
||||
const [selectedNode, setSelectedNode] = useState(null);
|
||||
const [selectedProjects, setSelectedProjects] = useState([]);
|
||||
const [activeFilter, setActiveFilter] = useState(null);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [currentProject, setCurrentProject] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isDarkMode] = useState(true); // 固定使用深色主题
|
||||
const [lastImportedFilePath, setLastImportedFilePath] = useState(null); // 记录最后导入的文件路径
|
||||
// 根据选中的节点筛选项目数据
|
||||
let select = {};
|
||||
|
||||
// 初始化数据
|
||||
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) {
|
||||
// 更新成功,重新加载数据
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
// 加载数据
|
||||
const loadData = 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
// 获取项目数据
|
||||
const projectsData = await window.electronAPI.getProjects();
|
||||
setProjects(projectsData);
|
||||
|
||||
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}
|
||||
/>
|
||||
|
||||
{/* 项目详情表单 */}
|
||||
<ProjectDetailForm
|
||||
visible={detailModalVisible}
|
||||
project={currentProject}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
onSave={handleProjectUpdate}
|
||||
/>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
const projectsDataRange = await window.electronAPI.getProjectsRange();
|
||||
setProjectsDataRange(projectsDataRange);
|
||||
|
||||
// 获取树状结构数据
|
||||
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);
|
||||
|
||||
|
||||
if (key === 'headquarters') {
|
||||
// 总部节点,显示所有数据
|
||||
setProjects(projects);
|
||||
} else if (key.startsWith('unit-')) {
|
||||
// 单位节点,筛选该单位的数据
|
||||
const unit = key.replace('unit-', '');
|
||||
select = {unit};
|
||||
} else if (key.startsWith('construction-')) {
|
||||
// 建设单位节点,筛选该建设单位的数据
|
||||
const parts = key.split('-');
|
||||
const unit = parts[1];
|
||||
const constructionUnit = parts.slice(2).join('-');
|
||||
select = {unit, constructionUnit};
|
||||
}
|
||||
|
||||
// 应用筛选
|
||||
filterProjects(select);
|
||||
}
|
||||
};
|
||||
|
||||
// 筛选项目数据
|
||||
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, ...select });
|
||||
};
|
||||
|
||||
// 处理项目搜索
|
||||
const handleSearch = (text) => {
|
||||
|
||||
setSearchText(text);
|
||||
console.log(select)
|
||||
// 根据搜索文本筛选项目数据
|
||||
const filters = {subProjectName: text};
|
||||
filterProjects({ ...filters, ...select });
|
||||
};
|
||||
|
||||
// 处理项目选择
|
||||
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 antProgressText = progressBar.querySelector('.ant-progress-text');
|
||||
// 更新进度条
|
||||
const antProgress = progressBar.querySelector('.ant-progress-bg');
|
||||
if (antProgress) {
|
||||
antProgress.style.width = `${data.progress}%`;
|
||||
antProgress.setAttribute('aria-valuenow', data.progress);
|
||||
}
|
||||
|
||||
// 更新文本
|
||||
progressMessage.textContent = data.message;
|
||||
antProgressText.textContent = `${data.progress}%`;
|
||||
// 如果导入完成,关闭对话框并重新加载数据
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,146 +1,291 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Modal, Form, Input, Select, Switch, Button } from 'antd';
|
||||
import React, {useEffect} from 'react';
|
||||
import {Modal, Form, Input, Select, Switch, Button, InputNumber} from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
const {Option} = Select;
|
||||
const {TextArea} = Input;
|
||||
|
||||
const ProjectDetailForm = ({ visible, project, onCancel, onSave }) => {
|
||||
const [form] = Form.useForm();
|
||||
const ProjectDetailForm = ({visible, project, onCancel, onSave}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (project && visible) {
|
||||
// 格式化日期字段为字符串
|
||||
const formattedProject = { ...project };
|
||||
['actual_start_time', 'planned_completion_time', 'completion_time', 'next_review_time'].forEach(field => {
|
||||
if (formattedProject[field]) {
|
||||
formattedProject[field] = dayjs(formattedProject[field]).format('YYYY-MM-DD');
|
||||
useEffect(() => {
|
||||
if (project && visible) {
|
||||
// 格式化日期字段为字符串
|
||||
const formattedProject = {...project};
|
||||
['actual_start_time', 'planned_completion_time', 'completion_time', 'next_review_time'].forEach(field => {
|
||||
if (formattedProject[field]) {
|
||||
formattedProject[field] = dayjs(formattedProject[field]).format('YYYY-MM-DD');
|
||||
}
|
||||
});
|
||||
form.setFieldsValue(formattedProject);
|
||||
}
|
||||
});
|
||||
form.setFieldsValue(formattedProject);
|
||||
}
|
||||
}, [form, project, visible]);
|
||||
}, [form, project, visible]);
|
||||
|
||||
const handleSave = () => {
|
||||
form.validateFields()
|
||||
.then(values => {
|
||||
onSave({ ...project, ...values });
|
||||
})
|
||||
.catch(info => {
|
||||
console.log('验证失败:', info);
|
||||
});
|
||||
};
|
||||
const handleSave = () => {
|
||||
form.validateFields()
|
||||
.then(values => {
|
||||
onSave({...project, ...values});
|
||||
})
|
||||
.catch(info => {
|
||||
console.log('验证失败:', info);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="项目详情"
|
||||
open={visible}
|
||||
width={800}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="save" type="primary" onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item label="单位" name="unit">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="项目编号" name="project_number">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="安全编码" name="safety_code">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="大项工程名称" name="major_project_name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="单项工程名称" name="sub_project_name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="在施工程作业范围" name="construction_scope">
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label="工程规模" name="project_scale">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="安全总监" name="safety_director">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="建设单位" name="construction_unit">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="监理单位" name="supervision_unit">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="施工单位" name="construction_company">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="工程位置" name="project_location">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="实际开工时间" name="actual_start_time">
|
||||
<Input placeholder="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<Form.Item label="计划竣工时间" name="planned_completion_time">
|
||||
<Input placeholder="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<Form.Item label="完成时间" name="completion_time">
|
||||
<Input placeholder="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<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>
|
||||
<Form.Item label="新班组进场数量" name="new_team_count">
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
<Form.Item label="新人进场数量" name="new_person_count">
|
||||
<Input type="number" />
|
||||
</Form.Item>
|
||||
<Form.Item label="带班人姓名、电话" name="leader_info">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="下周作业计划" name="next_week_plan">
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label="下周8+2工况内容" name="next_week_condition">
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label="工期是否紧张" name="is_schedule_tight" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="是否存在账外事" name="has_off_book_matters" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="当前风险等级" name="current_risk_level">
|
||||
<Select>
|
||||
<Option value="高风险">高风险</Option>
|
||||
<Option value="中风险">中风险</Option>
|
||||
<Option value="低风险">低风险</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="当前风险判断理由" name="risk_judgment_reason">
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label="隐患提示/工作要求" name="risk_tips">
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="remarks">
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
title="项目详情"
|
||||
open={visible}
|
||||
width={800}
|
||||
onCancel={onCancel}
|
||||
bodyStyle={{
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'scroll',
|
||||
msOverflowStyle: 'none', // IE, Edge
|
||||
scrollbarWidth: 'none' // Firefox
|
||||
}}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="save" type="primary" onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item label="单位" name="unit">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="项目编号" name="project_number">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="安全编码" name="safety_code">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="大项工程名称" name="major_project_name">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="单项工程名称" name="sub_project_name">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="在施工程作业范围" name="construction_scope">
|
||||
<TextArea rows={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="工程规模" name="project_scale">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="安全总监" name="safety_director">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="建设单位" name="construction_unit">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="监理单位" name="supervision_unit">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="施工单位" name="construction_company">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="工程位置" name="project_location">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="实际开工时间" name="actual_start_time">
|
||||
<Input placeholder="YYYY-MM-DD"/>
|
||||
</Form.Item>
|
||||
<Form.Item label="计划竣工时间" name="planned_completion_time">
|
||||
<Input placeholder="YYYY-MM-DD"/>
|
||||
</Form.Item>
|
||||
<Form.Item label="完成时间" name="completion_time">
|
||||
<Input placeholder="YYYY-MM-DD"/>
|
||||
</Form.Item>
|
||||
<Form.Item label="下次梳理时间" name="next_review_time">
|
||||
<Input placeholder="YYYY-MM-DD"/>
|
||||
</Form.Item>
|
||||
<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" rules={[
|
||||
{type: 'number', min: 1, message: '必须为正整数'},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={0} style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
<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/>
|
||||
</Form.Item>
|
||||
<Form.Item label="下周作业计划" name="next_week_plan">
|
||||
<TextArea rows={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="下周8+2工况内容" name="next_week_condition">
|
||||
<TextArea rows={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="工期是否紧张" name="is_schedule_tight" valuePropName="checked">
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否存在账外事" name="has_off_book_matters" valuePropName="checked">
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
<Form.Item label="当前风险等级" name="current_risk_level">
|
||||
<Select>
|
||||
<Option value="高风险">高风险</Option>
|
||||
<Option value="中风险">中风险</Option>
|
||||
<Option value="低风险">低风险</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="当前风险判断理由" name="risk_judgment_reason">
|
||||
<TextArea rows={2}/>
|
||||
</Form.Item>
|
||||
<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>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetailForm;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,62 +1,62 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Tree, Spin } from 'antd';
|
||||
import { FolderOutlined, ApartmentOutlined, HomeOutlined } from '@ant-design/icons';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {Tree, Spin} from 'antd';
|
||||
import {FolderOutlined, ApartmentOutlined, HomeOutlined} from '@ant-design/icons';
|
||||
|
||||
const TreeView = ({ treeData, selectedNode, onSelect, loading }) => {
|
||||
const [expandedKeys, setExpandedKeys] = useState([]);
|
||||
const TreeView = ({treeData, selectedNode, onSelect, loading}) => {
|
||||
const [expandedKeys, setExpandedKeys] = useState([]);
|
||||
|
||||
// 默认只展开一级目录
|
||||
useEffect(() => {
|
||||
if (!treeData || treeData.length === 0) return;
|
||||
|
||||
// 只获取第一级节点的key
|
||||
const firstLevelKeys = treeData.map(node => node.key);
|
||||
setExpandedKeys(firstLevelKeys);
|
||||
}, [treeData]);
|
||||
// 自定义树节点图标
|
||||
const getIcon = (node) => {
|
||||
if (node.level === 1) {
|
||||
return <HomeOutlined />;
|
||||
} else if (node.level === 2) {
|
||||
return <ApartmentOutlined />;
|
||||
} else {
|
||||
return <FolderOutlined />;
|
||||
}
|
||||
};
|
||||
// 默认只展开一级目录
|
||||
useEffect(() => {
|
||||
if (!treeData || treeData.length === 0) return;
|
||||
|
||||
// 自定义树节点标题
|
||||
const titleRender = (nodeData) => {
|
||||
return (
|
||||
<span>
|
||||
// 只获取第一级节点的key
|
||||
const firstLevelKeys = treeData.map(node => node.key);
|
||||
setExpandedKeys(firstLevelKeys);
|
||||
}, [treeData]);
|
||||
// 自定义树节点图标
|
||||
const getIcon = (node) => {
|
||||
if (node.level === 1) {
|
||||
return <HomeOutlined/>;
|
||||
} else if (node.level === 2) {
|
||||
return <ApartmentOutlined/>;
|
||||
} else {
|
||||
return <FolderOutlined/>;
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义树节点标题
|
||||
const titleRender = (nodeData) => {
|
||||
return (
|
||||
<span>
|
||||
{getIcon(nodeData)} {nodeData.title}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tree-view">
|
||||
<div style={{ padding: '10px', fontWeight: 'bold', borderBottom: '1px solid var(--vscode-sidebar-border)' }}>
|
||||
项目结构
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
|
||||
<Spin />
|
||||
return (
|
||||
<div className="tree-view">
|
||||
<div style={{padding: '10px', fontWeight: 'bold', borderBottom: '1px solid var(--vscode-sidebar-border)'}}>
|
||||
项目预警
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{display: 'flex', justifyContent: 'center', padding: '20px'}}>
|
||||
<Spin/>
|
||||
</div>
|
||||
) : (
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
selectedKeys={selectedNode ? [selectedNode] : []}
|
||||
onSelect={onSelect}
|
||||
titleRender={titleRender}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={keys => setExpandedKeys(keys)}
|
||||
defaultExpandedKeys={expandedKeys}
|
||||
blockNode
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
selectedKeys={selectedNode ? [selectedNode] : []}
|
||||
onSelect={onSelect}
|
||||
titleRender={titleRender}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={keys => setExpandedKeys(keys)}
|
||||
defaultExpandedKeys={expandedKeys}
|
||||
blockNode
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default TreeView;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const path = require('path');
|
|||
class ExcelService {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
|
||||
|
||||
// 不在构造函数中注册事件,避免重复注册
|
||||
// 事件处理器现在由main.js处理
|
||||
}
|
||||
|
|
@ -15,11 +15,11 @@ class ExcelService {
|
|||
async importExcel(filePath) {
|
||||
try {
|
||||
console.log('正在导入Excel文件:', filePath);
|
||||
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('文件路径不能为空');
|
||||
}
|
||||
|
||||
|
||||
// 发送开始导入进度事件
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -28,21 +28,21 @@ class ExcelService {
|
|||
message: '正在读取Excel文件...'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(filePath);
|
||||
|
||||
|
||||
// 读取第一个工作表
|
||||
const worksheet = workbook.getWorksheet(1);
|
||||
const data = [];
|
||||
const totalRows = worksheet.rowCount - 1; // 减去表头行
|
||||
|
||||
|
||||
// 读取表头
|
||||
const headers = [];
|
||||
worksheet.getRow(1).eachCell((cell, colNumber) => {
|
||||
headers[colNumber - 1] = cell.value;
|
||||
});
|
||||
|
||||
|
||||
// 发送读取表头完成进度事件
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -51,7 +51,7 @@ class ExcelService {
|
|||
message: '表头读取完成,正在读取数据...'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 读取数据
|
||||
let processedRows = 0;
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
|
|
@ -61,7 +61,7 @@ class ExcelService {
|
|||
rowData[headers[colNumber - 1]] = cell.value;
|
||||
});
|
||||
data.push(rowData);
|
||||
|
||||
|
||||
// 计算并发送进度
|
||||
processedRows++;
|
||||
if (processedRows % 10 === 0 || processedRows === totalRows) { // 每10行或最后一行发送一次进度
|
||||
|
|
@ -76,7 +76,7 @@ class ExcelService {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 发送数据读取完成进度事件
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -85,11 +85,11 @@ class ExcelService {
|
|||
message: `数据读取完成,正在保存到数据库...`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 将数据保存到数据库(这里可以根据实际需求修改)
|
||||
if (data.length > 0 && this.db) {
|
||||
await this.saveDataToDatabase(data);
|
||||
|
||||
|
||||
// 发送数据保存完成进度事件
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -98,10 +98,10 @@ class ExcelService {
|
|||
message: `数据保存完成,正在构建树状结构...`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 构建树状结构
|
||||
await this.buildTreeStructure(data);
|
||||
|
||||
|
||||
// 发送导入完成进度事件
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -120,7 +120,7 @@ class ExcelService {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `成功读取 ${data.length} 条数据`,
|
||||
|
|
@ -128,7 +128,7 @@ class ExcelService {
|
|||
};
|
||||
} catch (error) {
|
||||
console.error('Excel导入错误:', error);
|
||||
|
||||
|
||||
// 发送导入错误进度事件
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -137,14 +137,14 @@ class ExcelService {
|
|||
message: `导入失败: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 将数据保存到数据库
|
||||
saveDataToDatabase(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -183,35 +183,68 @@ class ExcelService {
|
|||
remarks
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
|
||||
// 开始事务
|
||||
this.db.serialize(() => {
|
||||
this.db.run('BEGIN TRANSACTION');
|
||||
|
||||
|
||||
// 发送进度更新
|
||||
let processedRows = 0;
|
||||
const totalRows = data.length;
|
||||
|
||||
|
||||
for (const row of data) {
|
||||
// 处理字段值,确保所有字段都能正确导入
|
||||
const getValue = (fieldName, defaultValue = null) => {
|
||||
|
||||
|
||||
// 检查字段是否存在,并处理空值
|
||||
const value = row[fieldName];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return defaultValue;
|
||||
}
|
||||
// 如果是日期类型,转换为ISO格式
|
||||
if (fieldName === '实际开工时间' || fieldName === '计划竣工时间' || fieldName === '完成时间' || fieldName === '下次梳理时间(注意与 隐患提示/工作要求 对应)') {
|
||||
const date = new Date(value);
|
||||
if (!isNaN(date.getTime())) {
|
||||
|
||||
return date.toISOString().split('T')[0]; // 只保留日期部分
|
||||
if (
|
||||
fieldName === '实际开工时间' ||
|
||||
fieldName === '计划竣工时间' ||
|
||||
fieldName === '完成时间' ||
|
||||
fieldName === '下次梳理时间(注意与 隐患提示/工作要求 对应)'
|
||||
) {
|
||||
if (value == null) return ''; // 空值处理
|
||||
|
||||
// ✅ 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;
|
||||
};
|
||||
|
||||
|
||||
// 处理布尔值字段
|
||||
const getBooleanValue = (fieldName) => {
|
||||
const value = row[fieldName];
|
||||
|
|
@ -220,7 +253,7 @@ class ExcelService {
|
|||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
// 处理数字字段
|
||||
const getNumberValue = (fieldName, defaultValue = 0) => {
|
||||
const value = row[fieldName];
|
||||
|
|
@ -231,7 +264,7 @@ class ExcelService {
|
|||
const num = Number(value);
|
||||
return isNaN(num) ? defaultValue : num;
|
||||
};
|
||||
|
||||
|
||||
// 使用处理函数获取字段值
|
||||
stmt.run(
|
||||
getValue('单位'),
|
||||
|
|
@ -265,12 +298,15 @@ class ExcelService {
|
|||
getValue('下次梳理时间(注意与 隐患提示/工作要求 对应)'),
|
||||
getValue('备注')
|
||||
);
|
||||
|
||||
|
||||
// 更新进度
|
||||
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,
|
||||
|
|
@ -279,7 +315,7 @@ class ExcelService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.db.run('COMMIT', (err) => {
|
||||
if (err) {
|
||||
console.error('提交事务失败:', err);
|
||||
|
|
@ -291,12 +327,12 @@ class ExcelService {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 完成后关闭准备好的语句
|
||||
stmt.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 构建树状结构
|
||||
buildTreeStructure(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -307,10 +343,10 @@ class ExcelService {
|
|||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 使用Promise和事务来确保操作的原子性和顺序性
|
||||
const headquarters = '总部';
|
||||
|
||||
|
||||
// 开始事务
|
||||
this.db.run('BEGIN TRANSACTION', (err) => {
|
||||
if (err) {
|
||||
|
|
@ -318,7 +354,7 @@ class ExcelService {
|
|||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 发送进度更新 - 开始创建树状结构
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -327,7 +363,7 @@ class ExcelService {
|
|||
message: `正在创建总部节点...`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 插入总部节点
|
||||
this.db.run(
|
||||
`INSERT INTO tree_structure (headquarters, unit, construction_unit, parent_id, node_level)
|
||||
|
|
@ -340,7 +376,7 @@ class ExcelService {
|
|||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 获取总部节点ID
|
||||
this.db.get('SELECT last_insert_rowid() as id', (err, row) => {
|
||||
if (err) {
|
||||
|
|
@ -349,16 +385,16 @@ class ExcelService {
|
|||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const headquartersId = row.id;
|
||||
console.log(`创建总部节点成功, ID: ${headquartersId}`);
|
||||
|
||||
|
||||
// 单位映射
|
||||
const unitMap = {};
|
||||
|
||||
|
||||
// 首先处理所有单位节点
|
||||
const uniqueUnits = [...new Set(data.filter(row => row['单位']).map(row => row['单位']))];
|
||||
|
||||
|
||||
// 发送进度更新 - 开始创建单位节点
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -367,16 +403,16 @@ class ExcelService {
|
|||
message: `正在创建单位节点...`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const processUnits = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let unitProcessed = 0;
|
||||
|
||||
|
||||
if (uniqueUnits.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
uniqueUnits.forEach(unit => {
|
||||
this.db.run(
|
||||
`INSERT INTO tree_structure (headquarters, unit, construction_unit, parent_id, node_level)
|
||||
|
|
@ -388,13 +424,13 @@ class ExcelService {
|
|||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const unitId = this.lastID;
|
||||
unitMap[unit] = { id: unitId, constructionUnits: {} };
|
||||
console.log(`创建单位节点: ${unit}, ID: ${unitId}`);
|
||||
|
||||
|
||||
unitProcessed++;
|
||||
|
||||
|
||||
// 更新进度
|
||||
if (unitProcessed % 5 === 0 || unitProcessed === uniqueUnits.length) {
|
||||
const progress = Math.floor(80 + (unitProcessed / uniqueUnits.length) * 10); // 80%-90%的进度
|
||||
|
|
@ -406,7 +442,7 @@ class ExcelService {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (unitProcessed === uniqueUnits.length) {
|
||||
resolve();
|
||||
}
|
||||
|
|
@ -415,18 +451,18 @@ class ExcelService {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 然后处理所有建设单位节点
|
||||
const processConstructionUnits = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 收集所有有效的建设单位数据
|
||||
const constructionUnitData = [];
|
||||
|
||||
|
||||
data.forEach(row => {
|
||||
const unit = row['单位'];
|
||||
const constructionUnit = row['建设单位'];
|
||||
|
||||
if (unit && constructionUnit && unitMap[unit] &&
|
||||
|
||||
if (unit && constructionUnit && unitMap[unit] &&
|
||||
!unitMap[unit].constructionUnits[constructionUnit]) {
|
||||
constructionUnitData.push({
|
||||
unit,
|
||||
|
|
@ -436,7 +472,7 @@ class ExcelService {
|
|||
unitMap[unit].constructionUnits[constructionUnit] = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 发送进度更新 - 开始创建建设单位节点
|
||||
if (global.mainWindow) {
|
||||
global.mainWindow.webContents.send('import-progress', {
|
||||
|
|
@ -445,14 +481,14 @@ class ExcelService {
|
|||
message: `正在创建建设单位节点...`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (constructionUnitData.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let processed = 0;
|
||||
|
||||
|
||||
constructionUnitData.forEach(item => {
|
||||
this.db.run(
|
||||
`INSERT INTO tree_structure (headquarters, unit, construction_unit, parent_id, node_level)
|
||||
|
|
@ -465,9 +501,9 @@ class ExcelService {
|
|||
} else {
|
||||
console.log(`创建建设单位节点: ${item.constructionUnit}, 父节点: ${item.unit}`);
|
||||
}
|
||||
|
||||
|
||||
processed++;
|
||||
|
||||
|
||||
// 更新进度
|
||||
if (processed % 10 === 0 || processed === constructionUnitData.length) {
|
||||
const progress = Math.floor(90 + (processed / constructionUnitData.length) * 10); // 90%-100%的进度
|
||||
|
|
@ -479,7 +515,7 @@ class ExcelService {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (processed === constructionUnitData.length) {
|
||||
resolve();
|
||||
}
|
||||
|
|
@ -488,7 +524,7 @@ class ExcelService {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 按顺序执行:先处理单位,再处理建设单位
|
||||
processUnits()
|
||||
.then(() => processConstructionUnits())
|
||||
|
|
|
|||
|
|
@ -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