518 lines
16 KiB
JavaScript
518 lines
16 KiB
JavaScript
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;
|
||
}).catch(err => {
|
||
console.error('Failed to load electron-is-dev:', err);
|
||
isDev = false;
|
||
});
|
||
const sqlite3 = require('sqlite3').verbose();
|
||
const ExcelService = require('./src/services/ExcelService');
|
||
|
||
// 保持对window对象的全局引用,避免JavaScript对象被垃圾回收时,窗口被自动关闭
|
||
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;
|
||
|
||
// 加载应用
|
||
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>`);
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
// 开发模式立即尝试加载,生产模式直接加载
|
||
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(`
|
||
CREATE TABLE IF NOT EXISTS projects (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
unit TEXT, -- 单位
|
||
project_number TEXT, -- 项目编号
|
||
safety_code TEXT, -- 安全编码
|
||
major_project_name TEXT, -- 大项工程名称
|
||
sub_project_name TEXT, -- 单项工程名称
|
||
construction_scope TEXT, -- 在施工程作业范围
|
||
project_scale TEXT, -- 工程规模
|
||
safety_director TEXT, -- 安全总监
|
||
construction_unit TEXT, -- 建设单位
|
||
supervision_unit TEXT, -- 监理单位
|
||
construction_company TEXT, -- 施工单位
|
||
project_location TEXT, -- 工程位置
|
||
actual_start_time TEXT, -- 实际开工时间
|
||
planned_completion_time TEXT, -- 计划竣工时间
|
||
current_progress TEXT, -- 当前工程进度
|
||
current_status TEXT, -- 当前工程状态
|
||
participants_count INTEGER, -- 参建人数
|
||
new_team_count INTEGER, -- 新班组进场数量
|
||
new_person_count INTEGER, -- 新人进场数量
|
||
leader_info TEXT, -- 带班人姓名、电话
|
||
next_week_plan TEXT, -- 下周作业计划
|
||
next_week_condition TEXT, -- 下周8+2工况内容
|
||
is_schedule_tight BOOLEAN, -- 工期是否紧张
|
||
has_off_book_matters BOOLEAN, -- 是否存在"账外事"
|
||
current_risk_level TEXT, -- 当前风险等级
|
||
risk_judgment_reason TEXT, -- 当前风险判断理由
|
||
risk_tips TEXT, -- 隐患提示/工作要求
|
||
completion_time TEXT, -- 完成时间
|
||
next_review_time TEXT, -- 下次梳理时间
|
||
remarks TEXT -- 备注
|
||
)
|
||
`);
|
||
|
||
db.run(`
|
||
CREATE TABLE IF NOT EXISTS tree_structure (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
headquarters TEXT, -- 总部
|
||
unit TEXT, -- 单位
|
||
construction_unit TEXT, -- 建设单位
|
||
parent_id INTEGER, -- 父节点ID
|
||
node_level INTEGER, -- 节点级别(1:总部, 2:单位, 3:建设单位)
|
||
FOREIGN KEY (parent_id) REFERENCES tree_structure(id)
|
||
)
|
||
`);
|
||
|
||
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'
|
||
})
|
||
}
|
||
}
|
||
]
|
||
}
|
||
];
|
||
|
||
// 当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: '所有数据已清除' });
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
} 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();
|
||
}
|
||
});
|
||
|
||
// 处理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];
|
||
});
|
||
|
||
// 获取数据库中的项目数据
|
||
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 || []);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// 获取树状结构数据
|
||
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);
|
||
});
|
||
});
|
||
});
|
||
|
||
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 || []);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// 更新项目数据
|
||
ipcMain.handle('update-project', (event, project) => {
|
||
return new Promise((resolve, reject) => {
|
||
const query = `
|
||
UPDATE projects SET
|
||
unit = ?,
|
||
project_number = ?,
|
||
safety_code = ?,
|
||
major_project_name = ?,
|
||
sub_project_name = ?,
|
||
construction_scope = ?,
|
||
project_scale = ?,
|
||
safety_director = ?,
|
||
construction_unit = ?,
|
||
supervision_unit = ?,
|
||
construction_company = ?,
|
||
project_location = ?,
|
||
actual_start_time = ?,
|
||
planned_completion_time = ?,
|
||
current_progress = ?,
|
||
current_status = ?,
|
||
participants_count = ?,
|
||
new_team_count = ?,
|
||
new_person_count = ?,
|
||
leader_info = ?,
|
||
next_week_plan = ?,
|
||
next_week_condition = ?,
|
||
is_schedule_tight = ?,
|
||
has_off_book_matters = ?,
|
||
current_risk_level = ?,
|
||
risk_judgment_reason = ?,
|
||
risk_tips = ?,
|
||
completion_time = ?,
|
||
next_review_time = ?,
|
||
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 });
|
||
}
|
||
});
|
||
});
|
||
});
|