Initial commit

This commit is contained in:
吕继龙 2025-04-01 17:15:19 +08:00
commit f045fc0d37
17 changed files with 24142 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

474
main.js Normal file
View File

@ -0,0 +1,474 @@
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')
}
});
// 加载应用
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 };
}
});
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 });
}
});
});
});

21820
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

69
package.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "data-tools",
"version": "1.0.0",
"description": "基于Electron+React的数据处理桌面应用",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "concurrently \"npm run start\" \"cross-env BROWSER=none npm run react-start\"",
"react-start": "react-scripts start",
"react-build": "react-scripts build",
"build": "electron-builder",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"keywords": [
"electron",
"react",
"excel",
"sqlite",
"data-processing"
],
"author": "",
"license": "ISC",
"homepage": "./",
"build": {
"appId": "com.datatools.app",
"productName": "DataTools",
"files": [
"build/**/*",
"node_modules/**/*",
"main.js",
"preload.js"
],
"directories": {
"buildResources": "assets"
},
"win": {
"target": "nsis"
}
},
"dependencies": {
"antd": "^5.7.1",
"sqlite3": "^5.1.6",
"electron-is-dev": "^2.0.0",
"exceljs": "^4.3.0",
"fs-extra": "^11.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"concurrently": "^8.2.0",
"cross-env": "^7.0.3",
"electron": "^27.3.8",
"electron-builder": "^24.6.3",
"react-scripts": "5.0.1"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

16
preload.js Normal file
View File

@ -0,0 +1,16 @@
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),
// Excel处理
importExcel: (filePath) => ipcRenderer.invoke('import-excel', filePath),
});

20
public/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="基于Electron+React的数据处理桌面应用"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>值班助手工具</title>
</head>
<body>
<noscript>您需要启用JavaScript才能运行此应用。</noscript>
<div id="root"></div>
</body>
</html>

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "DataTools",
"name": "数据处理工具",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

280
src/App.js Normal file
View File

@ -0,0 +1,280 @@
import React, { useState, useEffect } from 'react';
import { ConfigProvider, theme, Switch, Space } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import './styles/index.css';
// 配置dayjs为中文
dayjs.locale('zh-cn');
// 导入组件
import Toolbar from './components/Toolbar';
import TreeView from './components/TreeView';
import DataView from './components/DataView';
import AnalysisView from './components/AnalysisView';
import ProjectDetailForm from './components/ProjectDetailForm';
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); // 固定使用深色主题
// 初始化数据
useEffect(() => {
loadData();
}, []);
// 加载数据
const loadData = async () => {
setLoading(true);
try {
// 获取项目数据
const projectsData = await window.electronAPI.getProjects();
setProjects(projectsData);
// 获取树状结构数据
const treeStructureData = await window.electronAPI.getTreeStructure();
// 如果树状结构为空,则从项目数据构建树状结构
if (treeStructureData.length === 0 && projectsData.length > 0) {
const tree = buildTreeFromProjects(projectsData);
setTreeData(tree);
} else {
setTreeData(treeStructureData);
}
} catch (error) {
console.error('加载数据失败:', error);
} finally {
setLoading(false);
}
};
// 从项目数据构建树状结构
const buildTreeFromProjects = (projectsData) => {
// 总部节点
const headquarters = {
key: 'headquarters',
title: '总部',
level: 1,
children: []
};
// 单位映射
const unitMap = {};
// 遍历项目数据,构建树状结构
projectsData.forEach(project => {
const { unit, construction_unit } = project;
// 如果单位不存在,则创建单位节点
if (!unitMap[unit]) {
unitMap[unit] = {
key: `unit-${unit}`,
title: unit,
level: 2,
children: []
};
headquarters.children.push(unitMap[unit]);
}
// 如果建设单位不为空,则添加建设单位节点
if (construction_unit && !unitMap[unit].children.find(child => child.title === construction_unit)) {
unitMap[unit].children.push({
key: `construction-${unit}-${construction_unit}`,
title: construction_unit,
level: 3,
isLeaf: true
});
}
});
return [headquarters];
};
// 处理树节点选择
const handleTreeSelect = (selectedKeys, info) => {
if (selectedKeys.length > 0) {
const key = selectedKeys[0];
setSelectedNode(key);
// 根据选中的节点筛选项目数据
let filters = {};
if (key === 'headquarters') {
// 总部节点,显示所有数据
setProjects(projects);
} else if (key.startsWith('unit-')) {
// 单位节点,筛选该单位的数据
const unit = key.replace('unit-', '');
filters = { unit };
} else if (key.startsWith('construction-')) {
// 建设单位节点,筛选该建设单位的数据
const parts = key.split('-');
const unit = parts[1];
const constructionUnit = parts.slice(2).join('-');
filters = { unit, constructionUnit };
}
// 应用筛选
filterProjects(filters);
}
};
// 筛选项目数据
const filterProjects = async (filters) => {
setLoading(true);
try {
const filteredProjects = await window.electronAPI.filterProjects(filters);
setProjects(filteredProjects);
} catch (error) {
console.error('筛选项目数据失败:', error);
} finally {
setLoading(false);
}
};
// 处理工具栏筛选
const handleToolbarFilter = (filter) => {
setActiveFilter(filter === activeFilter ? null : filter);
// 根据筛选条件筛选项目数据
let filters = {};
if (filter === 'risk') {
filters = { riskLevel: '高风险' };
}
// 应用筛选
filterProjects(filters);
};
// 处理项目搜索
const handleSearch = (text) => {
setSearchText(text);
// 根据搜索文本筛选项目数据
const filters = { subProjectName: text };
filterProjects(filters);
};
// 处理项目选择
const handleProjectSelect = (selectedRowKeys) => {
setSelectedProjects(selectedRowKeys);
};
// 处理项目点击,打开详情弹窗
const handleProjectClick = (project) => {
setCurrentProject(project);
setDetailModalVisible(true);
};
// 处理项目更新
const handleProjectUpdate = async (updatedProject) => {
setLoading(true);
try {
const result = await window.electronAPI.updateProject(updatedProject);
if (result.success) {
// 更新成功,重新加载数据
loadData();
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) {
setLoading(true);
// 导入Excel文件
const result = await window.electronAPI.importExcel(filePath);
if (result.success) {
// 导入成功,重新加载数据
loadData();
} else {
console.error('导入Excel文件失败:', result.error);
}
}
} catch (error) {
console.error('导入Excel文件失败:', error);
} 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}
/>
{/* 树状结构区 */}
<TreeView
treeData={treeData}
selectedNode={selectedNode}
onSelect={handleTreeSelect}
loading={loading}
/>
{/* 数据展示区 */}
<DataView
projects={projects}
selectedProjects={selectedProjects}
onSelect={handleProjectSelect}
onClick={handleProjectClick} // 传入点击事件处理函数
onSearch={handleSearch}
searchText={searchText}
loading={loading}
/>
{/* 分析结果区 */}
<AnalysisView
selectedProjects={projects.filter(p => selectedProjects.includes(p.id))}
/>
{/* 项目详情表单 */}
<ProjectDetailForm
visible={detailModalVisible}
project={currentProject}
onCancel={() => setDetailModalVisible(false)}
onSave={handleProjectUpdate}
/>
</div>
</ConfigProvider>
);
}
export default App;

View File

@ -0,0 +1,163 @@
import React from 'react';
import { Card, Statistic, Divider, Empty } from 'antd';
import { WarningOutlined, ClockCircleOutlined, TeamOutlined } from '@ant-design/icons';
const AnalysisView = ({ selectedProjects }) => {
// 如果没有选中项目,显示空状态
if (!selectedProjects || selectedProjects.length === 0) {
return (
<div className="analysis-view">
<div style={{ padding: '10px', fontWeight: 'bold', borderBottom: '1px solid var(--vscode-sidebar-border)' }}>
分析结果
</div>
<Empty
description="请选择项目进行分析"
style={{ margin: '50px 0' }}
/>
</div>
);
}
// 计算统计数据
const calculateStatistics = () => {
// 风险等级统计
const riskLevels = {
high: 0,
medium: 0,
low: 0
};
// 工期紧张项目数量
let tightScheduleCount = 0;
// 账外事项目数量
let offBookMattersCount = 0;
// 参建人数总计
let totalParticipants = 0;
// 新班组总计
let totalNewTeams = 0;
// 新人总计
let totalNewPersons = 0;
// 遍历选中项目
selectedProjects.forEach(project => {
// 风险等级
if (project.current_risk_level === '高风险') {
riskLevels.high++;
} else if (project.current_risk_level === '中风险') {
riskLevels.medium++;
} else if (project.current_risk_level === '低风险') {
riskLevels.low++;
}
// 工期紧张
if (project.is_schedule_tight) {
tightScheduleCount++;
}
// 账外事
if (project.has_off_book_matters) {
offBookMattersCount++;
}
// 参建人数
totalParticipants += project.participants_count || 0;
// 新班组
totalNewTeams += project.new_team_count || 0;
// 新人
totalNewPersons += project.new_person_count || 0;
});
return {
riskLevels,
tightScheduleCount,
offBookMattersCount,
totalParticipants,
totalNewTeams,
totalNewPersons
};
};
const stats = calculateStatistics();
return (
<div className="analysis-view">
<div style={{ padding: '10px', fontWeight: 'bold', borderBottom: '1px solid var(--vscode-sidebar-border)' }}>
分析结果 ({selectedProjects.length}个项目)
</div>
<Card title="风险分布" bordered={false} style={{ margin: '10px 0' }}>
<Statistic
title="高风险项目"
value={stats.riskLevels.high}
valueStyle={{ color: '#cf1322' }}
prefix={<WarningOutlined />}
suffix={`/ ${selectedProjects.length}`}
/>
<Statistic
title="中风险项目"
value={stats.riskLevels.medium}
valueStyle={{ color: '#faad14' }}
prefix={<WarningOutlined />}
suffix={`/ ${selectedProjects.length}`}
style={{ marginTop: 10 }}
/>
<Statistic
title="低风险项目"
value={stats.riskLevels.low}
valueStyle={{ color: '#52c41a' }}
prefix={<WarningOutlined />}
suffix={`/ ${selectedProjects.length}`}
style={{ marginTop: 10 }}
/>
</Card>
<Divider style={{ margin: '10px 0' }} />
<Card title="工期状态" bordered={false} style={{ margin: '10px 0' }}>
<Statistic
title="工期紧张项目"
value={stats.tightScheduleCount}
valueStyle={{ color: stats.tightScheduleCount > 0 ? '#faad14' : '#52c41a' }}
prefix={<ClockCircleOutlined />}
suffix={`/ ${selectedProjects.length}`}
/>
<Statistic
title="存在'账外事'项目"
value={stats.offBookMattersCount}
valueStyle={{ color: stats.offBookMattersCount > 0 ? '#cf1322' : '#52c41a' }}
prefix={<WarningOutlined />}
suffix={`/ ${selectedProjects.length}`}
style={{ marginTop: 10 }}
/>
</Card>
<Divider style={{ margin: '10px 0' }} />
<Card title="人员情况" bordered={false} style={{ margin: '10px 0' }}>
<Statistic
title="参建人数总计"
value={stats.totalParticipants}
prefix={<TeamOutlined />}
/>
<Statistic
title="新班组总计"
value={stats.totalNewTeams}
style={{ marginTop: 10 }}
/>
<Statistic
title="新人总计"
value={stats.totalNewPersons}
style={{ marginTop: 10 }}
/>
</Card>
</div>
);
};
export default AnalysisView;

135
src/components/DataView.js Normal file
View File

@ -0,0 +1,135 @@
import React from 'react';
import { Table, Input, Spin } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
const DataView = ({
projects,
selectedProjects,
onSelect,
onClick, // 使用这个属性处理行点击
onSearch,
searchText,
loading
}) => {
// 表格列定义
const columns = [
{
title: '单位',
dataIndex: 'unit',
key: 'unit',
width: 120,
ellipsis: true,
},
{
title: '单项工程名称',
dataIndex: 'sub_project_name',
key: 'sub_project_name',
width: 200,
ellipsis: true,
},
{
title: '建设单位',
dataIndex: 'construction_unit',
key: 'construction_unit',
width: 150,
ellipsis: true,
},
{
title: '当前工程进度',
dataIndex: 'current_progress',
key: 'current_progress',
width: 120,
ellipsis: true,
},
{
title: '当前工程状态',
dataIndex: 'current_status',
key: 'current_status',
width: 120,
ellipsis: true,
},
{
title: '当前风险等级',
dataIndex: 'current_risk_level',
key: 'current_risk_level',
width: 120,
render: (text) => {
let color = '';
if (text === '高风险') {
color = 'red';
} else if (text === '中风险') {
color = 'orange';
} else if (text === '低风险') {
color = 'green';
}
return <span style={{ color }}>{text}</span>;
},
},
{
title: '工期是否紧张',
dataIndex: 'is_schedule_tight',
key: 'is_schedule_tight',
width: 120,
render: (value) => (value ? '是' : '否'),
},
{
title: '是否存在"账外事"',
dataIndex: 'has_off_book_matters',
key: 'has_off_book_matters',
width: 150,
render: (value) => (value ? '是' : '否'),
},
];
// 处理搜索输入变化
const handleSearchChange = (e) => {
onSearch(e.target.value);
};
return (
<div className="data-view">
{/* 搜索框 */}
<div style={{ padding: '10px', borderBottom: '1px solid var(--vscode-border)' }}>
<Input
placeholder="搜索单项工程名称"
prefix={<SearchOutlined />}
value={searchText}
onChange={handleSearchChange}
style={{ width: '100%' }}
/>
</div>
{/* 数据表格 */}
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
<Spin />
</div>
) : (
<Table
columns={columns}
dataSource={projects}
rowKey="id"
rowSelection={{
type: 'checkbox',
selectedRowKeys: selectedProjects,
onChange: onSelect,
}}
pagination={{
pageSize: 20,
showSizeChanger: true,
showTotal: (total) => `${total} 条数据`,
}}
scroll={{ y: 'calc(100vh - 150px)', x: 'max-content' }}
size="small"
onRow={(record) => ({
onClick: () => onClick(record), // 点击行时调用传入的onClick函数
style: { cursor: 'pointer' } // 鼠标悬停时显示指针样式
})}
/>
)}
</div>
);
};
export default DataView;

View File

@ -0,0 +1,146 @@
import React, { useEffect } from 'react';
import { Modal, Form, Input, Select, Switch, Button } from 'antd';
import dayjs from 'dayjs';
const { Option } = Select;
const { TextArea } = Input;
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');
}
});
form.setFieldsValue(formattedProject);
}
}, [form, project, visible]);
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>
);
};
export default ProjectDetailForm;

70
src/components/Toolbar.js Normal file
View File

@ -0,0 +1,70 @@
import React from 'react';
import { Tooltip, Modal } from 'antd';
import {
ProjectOutlined,
WarningOutlined,
SearchOutlined,
ImportOutlined,
SettingOutlined
} from '@ant-design/icons';
const Toolbar = ({ activeFilter, onFilterChange, onImportExcel }) => {
// 显示"正在开发中"的提示
const showDevelopingMessage = () => {
Modal.info({
title: '提示',
content: '正在开发中,请耐心等待',
});
};
// 工具栏项目
const tools = [
{
key: 'project',
icon: <ProjectOutlined />,
title: '工程',
onClick: showDevelopingMessage
},
{
key: 'risk',
icon: <WarningOutlined />,
title: '风险',
onClick: showDevelopingMessage
},
{
key: 'search',
icon: <SearchOutlined />,
title: '查找',
onClick: showDevelopingMessage
},
{
key: 'import',
icon: <ImportOutlined />,
title: '导入Excel',
onClick: onImportExcel
},
{
key: 'settings',
icon: <SettingOutlined />,
title: '设置',
onClick: showDevelopingMessage
}
];
return (
<div className="toolbar">
{tools.map(tool => (
<Tooltip key={tool.key} title={tool.title} placement="right">
<div
className={`toolbar-icon ${activeFilter === tool.key ? 'active' : ''}`}
onClick={tool.onClick}
>
{tool.icon}
</div>
</Tooltip>
))}
</div>
);
};
export default Toolbar;

View File

@ -0,0 +1,62 @@
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([]);
// 默认只展开一级目录
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 />;
}
};
// 自定义树节点标题
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 />
</div>
) : (
<Tree
treeData={treeData}
selectedKeys={selectedNode ? [selectedNode] : []}
onSelect={onSelect}
titleRender={titleRender}
expandedKeys={expandedKeys}
onExpand={keys => setExpandedKeys(keys)}
defaultExpandedKeys={expandedKeys}
blockNode
/>
)}
</div>
);
};
export default TreeView;

11
src/index.js Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './styles/index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,254 @@
const { ipcMain, dialog } = require('electron');
const ExcelJS = require('exceljs');
const fs = require('fs');
const path = require('path');
class ExcelService {
constructor(db) {
this.db = db;
// 不在构造函数中注册事件,避免重复注册
// 事件处理器现在由main.js处理
}
// 导入Excel文件
async importExcel(filePath) {
try {
console.log('正在导入Excel文件:', filePath);
if (!filePath) {
throw new Error('文件路径不能为空');
}
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(filePath);
// 读取第一个工作表
const worksheet = workbook.getWorksheet(1);
const data = [];
// 读取表头
const headers = [];
worksheet.getRow(1).eachCell((cell, colNumber) => {
headers[colNumber - 1] = cell.value;
});
// 读取数据
worksheet.eachRow((row, rowNumber) => {
if (rowNumber > 1) { // 跳过表头
const rowData = {};
row.eachCell((cell, colNumber) => {
rowData[headers[colNumber - 1]] = cell.value;
});
data.push(rowData);
}
});
// 将数据保存到数据库(这里可以根据实际需求修改)
if (data.length > 0 && this.db) {
this.saveDataToDatabase(data);
}
return {
success: true,
message: `成功读取 ${data.length} 条数据`,
data
};
} catch (error) {
console.error('Excel导入错误:', error);
return {
success: false,
error: error.message
};
}
}
// 将数据保存到数据库
saveDataToDatabase(data) {
// 清空现有数据
this.db.run('DELETE FROM projects', (err) => {
if (err) {
console.error('清空项目数据失败:', err);
return;
}
// 插入新数据
const stmt = this.db.prepare(`
INSERT INTO projects (
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
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
// 开始事务
this.db.serialize(() => {
this.db.run('BEGIN TRANSACTION');
for (const row of data) {
stmt.run(
row['单位'] || null,
row['项目编号'] || null,
row['安全编码'] || null,
row['大项工程名称'] || null,
row['单项工程名称'] || null,
row['在施工程作业范围'] || null,
row['工程规模'] || null,
row['安全总监'] || null,
row['建设单位'] || null,
row['监理单位'] || null,
row['施工单位'] || null,
row['工程位置'] || null,
row['实际开工时间'] || null,
row['计划竣工时间'] || null,
row['当前工程进度'] || null,
row['当前工程状态'] || null,
row['参建人数'] || 0,
row['新班组进场数量'] || 0,
row['新人进场数量'] || 0,
row['带班人姓名、电话'] || null,
row['下周作业计划'] || null,
row['下周8+2工况内容'] || null,
row['工期是否紧张'] === '是' ? 1 : 0,
row['是否存在"账外事"'] === '是' ? 1 : 0,
row['当前风险等级'] || null,
row['当前风险判断理由'] || null,
row['隐患提示/工作要求'] || null,
row['完成时间'] || null,
row['下次梳理时间'] || null,
row['备注'] || null
);
}
this.db.run('COMMIT', (err) => {
if (err) {
console.error('提交事务失败:', err);
this.db.run('ROLLBACK');
} else {
console.log(`成功导入 ${data.length} 条数据到数据库`);
// 构建树状结构
this.buildTreeStructure(data);
}
});
});
// 完成后关闭准备好的语句
stmt.finalize();
});
}
// 构建树状结构
buildTreeStructure(data) {
// 清空现有树状结构
this.db.run('DELETE FROM tree_structure', (err) => {
if (err) {
console.error('清空树状结构失败:', err);
return;
}
// 创建总部节点
const headquarters = '总部';
const insertHeadquarters = this.db.prepare(`
INSERT INTO tree_structure (
headquarters,
unit,
construction_unit,
parent_id,
node_level
) VALUES (?, ?, ?, ?, ?)
`);
// 插入总部节点
insertHeadquarters.run(headquarters, null, null, null, 1, function(err) {
if (err) {
console.error('插入总部节点失败:', err);
return;
}
const headquartersId = this.lastID;
console.log(`创建总部节点成功, ID: ${headquartersId}`);
// 单位映射
const unitMap = {};
// 遍历数据,构建树状结构
for (const row of data) {
const unit = row['单位'];
const constructionUnit = row['建设单位'];
// 检查单位是否为空
if (!unit) continue;
// 如果单位不存在,则创建单位节点
if (!unitMap[unit]) {
insertHeadquarters.run(headquarters, unit, null, headquartersId, 2, function(err) {
if (err) {
console.error(`创建单位节点失败: ${unit}`, err);
return;
}
const unitId = this.lastID;
unitMap[unit] = { id: unitId, constructionUnits: {} };
console.log(`创建单位节点: ${unit}, ID: ${unitId}`);
// 如果建设单位不为空且不存在,则添加建设单位节点
if (constructionUnit && !unitMap[unit].constructionUnits[constructionUnit]) {
insertHeadquarters.run(headquarters, unit, constructionUnit, unitId, 3, function(err) {
if (err) {
console.error(`创建建设单位节点失败: ${constructionUnit}`, err);
} else {
unitMap[unit].constructionUnits[constructionUnit] = true;
console.log(`创建建设单位节点: ${constructionUnit}, 父节点: ${unit}`);
}
});
}
});
} else if (constructionUnit && !unitMap[unit].constructionUnits[constructionUnit]) {
// 如果单位已存在但建设单位不存在,则添加建设单位节点
insertHeadquarters.run(headquarters, unit, constructionUnit, unitMap[unit].id, 3, function(err) {
if (err) {
console.error(`创建建设单位节点失败: ${constructionUnit}`, err);
} else {
unitMap[unit].constructionUnits[constructionUnit] = true;
console.log(`创建建设单位节点: ${constructionUnit}, 父节点: ${unit}`);
}
});
}
}
// 完成后关闭准备好的语句
insertHeadquarters.finalize();
console.log('树状结构构建完成');
});
});
}
}
module.exports = ExcelService;

165
src/styles/index.css Normal file
View File

@ -0,0 +1,165 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* VSCode风格的全局样式 */
:root {
--vscode-bg: #1e1e1e;
--vscode-sidebar-bg: #252526;
--vscode-sidebar-border: #333333;
--vscode-editor-bg: #1e1e1e;
--vscode-text: #cccccc;
--vscode-active-item: #37373d;
--vscode-hover-item: #2a2d2e;
--vscode-border: #474747;
}
.app-container {
display: flex;
height: 100vh;
background-color: var(--vscode-bg);
color: var(--vscode-text);
}
.toolbar {
width: 48px;
background-color: var(--vscode-sidebar-bg);
border-right: 1px solid var(--vscode-sidebar-border);
display: flex;
flex-direction: column;
align-items: center;
padding-top: 10px;
}
.tree-view {
width: 250px;
background-color: var(--vscode-sidebar-bg);
border-right: 1px solid var(--vscode-sidebar-border);
overflow-y: auto;
}
.data-view {
flex: 1;
background-color: var(--vscode-editor-bg);
overflow-y: auto;
}
.analysis-view {
width: 300px;
background-color: var(--vscode-sidebar-bg);
border-left: 1px solid var(--vscode-sidebar-border);
overflow-y: auto;
padding: 10px;
}
/* 工具栏图标样式 */
.toolbar-icon {
width: 32px;
height: 32px;
margin-bottom: 15px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 4px;
}
.toolbar-icon:hover {
background-color: var(--vscode-hover-item);
}
.toolbar-icon.active {
background-color: var(--vscode-active-item);
}
/* 树状结构样式 */
.ant-tree {
background-color: transparent !important;
color: var(--vscode-text) !important;
}
.ant-tree-node-content-wrapper:hover {
background-color: var(--vscode-hover-item) !important;
}
.ant-tree-node-selected {
background-color: var(--vscode-active-item) !important;
}
/* 表格样式 */
.ant-table {
background-color: transparent !important;
color: var(--vscode-text) !important;
}
.ant-table-thead > tr > th {
background-color: var(--vscode-sidebar-bg) !important;
color: var(--vscode-text) !important;
border-bottom: 1px solid var(--vscode-border) !important;
}
.ant-table-tbody > tr > td {
border-bottom: 1px solid var(--vscode-border) !important;
}
.ant-table-tbody > tr:hover > td {
background-color: var(--vscode-hover-item) !important;
}
.ant-table-row-selected > td {
background-color: var(--vscode-active-item) !important;
}
/* 表单样式 */
.ant-form-item-label > label {
color: var(--vscode-text) !important;
}
.ant-input, .ant-select-selector, .ant-picker {
background-color: var(--vscode-sidebar-bg) !important;
color: var(--vscode-text) !important;
border-color: var(--vscode-border) !important;
}
.ant-input:hover, .ant-select-selector:hover, .ant-picker:hover {
border-color: #40a9ff !important;
}
.ant-input:focus, .ant-select-selector:focus, .ant-picker:focus {
border-color: #40a9ff !important;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2) !important;
}
/* 按钮样式 */
.ant-btn {
background-color: var(--vscode-sidebar-bg) !important;
color: var(--vscode-text) !important;
border-color: var(--vscode-border) !important;
}
.ant-btn-primary {
background-color: #1890ff !important;
border-color: #1890ff !important;
}
.ant-btn:hover {
border-color: #40a9ff !important;
color: #40a9ff !important;
}
.ant-btn-primary:hover {
background-color: #40a9ff !important;
border-color: #40a9ff !important;
color: white !important;
}

431
需求分析报告.md Normal file
View File

@ -0,0 +1,431 @@
# 数据处理小工具需求分析报告
## 1. 项目概述
本项目是一个基于Electron+React的数据处理桌面应用主要用于处理Excel表格数据并将数据导入到SQLite数据库中进行存储和管理。应用提供树状结构展示、数据筛选、详情查看与编辑等功能界面风格类似VSCode。
### 1.1 项目目标
- 实现Excel数据的导入与处理处理合并单元格问题
- 构建树状结构展示(总部→单位→建设单位)
- 提供数据筛选、查询、编辑功能
- 支持多条数据选择与分析
- 实现Windows桌面应用一键安装
## 2. 功能需求分析
### 2.1 核心功能
#### 2.1.1 Excel数据导入与处理
- **功能描述**导入Excel表格数据处理合并单元格问题
- **处理逻辑**
- 取消Excel中的合并单元格
- 将取消前的内容复制到新的单元格中
- 提取"单位"字段和"建设单位"字段,形成树状结构
- **技术要点**
- 需要使用Node.js的Excel处理库如ExcelJS、xlsx
- 需要处理合并单元格的特殊情况
#### 2.1.2 树状结构展示
- **功能描述**:将单位和建设单位形成树状结构展示
- **结构层次**:总部→单位→建设单位
- **交互功能**
- 点击总部:显示所有数据
- 点击单位:显示与该单位相关的数据
- 点击建设单位:显示该建设单位的相关数据
- **技术要点**
- 使用React组件实现树状结构如Ant Design的Tree组件
- 树节点与数据筛选的联动
#### 2.1.3 数据存储与管理
- **功能描述**将处理后的数据存储到SQLite数据库
- **数据操作**
- 数据导入
- 数据查询
- 数据编辑与保存
- **技术要点**
- 使用Node.js的SQLite库如better-sqlite3
- 设计合理的数据库结构
#### 2.1.4 数据筛选与查询
- **功能描述**:提供多种方式筛选和查询数据
- **筛选方式**
- 通过树状结构筛选(总部/单位/建设单位)
- 通过单项工程名称筛选
- 通过工具栏功能按钮筛选(工程、风险等)
- **技术要点**
- 实现多条件组合筛选
- 优化查询性能
#### 2.1.5 数据详情查看与编辑
- **功能描述**:点击数据条目,以表单形式展示详情并支持编辑
- **操作流程**
- 点击数据行→显示详情表单
- 修改表单内容→保存更新
- **技术要点**
- 表单验证
- 数据更新与状态管理
#### 2.1.6 多数据选择与分析
- **功能描述**:选择多条数据进行分析,在右侧展示分析结果
- **操作方式**:通过复选框选择多条数据
- **技术要点**
- 复选框状态管理
- 分析结果展示区域设计
### 2.2 界面需求
#### 2.2.1 整体布局
- **风格要求**与VSCode工具风格一致
- **布局结构**
- 左侧:工具栏(数据筛选工具栏)
- 中左树状结构展示区类似VSCode文件目录区
- 中右数据展示区类似VSCode代码编辑区
- 右侧:分析结果展示区
#### 2.2.2 工具栏设计
- **位置**:最左侧
- **功能按钮**
- 工程
- 风险
- 查找
- 其他功能按钮
#### 2.2.3 树状结构区
- **位置**VSCode文件目录区域位置
- **内容**:总部→单位→建设单位的树状结构
- **交互**:点击节点筛选相关数据
#### 2.2.4 数据展示区
- **位置**VSCode代码编辑区域位置
- **功能**
- 表格形式展示数据
- 支持单项工程名称筛选
- 包含复选框用于多选
- 点击行显示详情
#### 2.2.5 详情表单区
- **触发方式**:点击数据行
- **内容**:以表单形式展示所有字段
- **功能**:支持编辑和保存
#### 2.2.6 分析结果区
- **位置**:页面最右侧
- **触发条件**:选择多条数据
- **内容**:展示分析结果(具体分析功能不需实现)
### 2.3 非功能需求
#### 2.3.1 性能需求
- Excel处理性能能够处理大型Excel文件
- 查询响应时间:数据筛选和查询响应迅速
- 应用启动时间:应用启动速度快
#### 2.3.2 安装部署需求
- 支持Windows桌面应用
- 实现一键安装功能
- 安装包体积合理
#### 2.3.3 用户体验需求
- 界面简洁美观
- 操作流程直观
- 响应及时
## 3. 技术架构分析
### 3.1 技术栈选择
#### 3.1.1 前端技术
- **框架**Electron + React
- **UI组件库**Ant Design
- **状态管理**Redux或Context API
- **样式方案**Less/Sass + CSS Modules
#### 3.1.2 后端技术
- **运行环境**Node.js (Electron主进程)
- **数据库**SQLite
- **Excel处理**ExcelJS/xlsx
- **文件操作**fs-extra
### 3.2 架构设计
#### 3.2.1 整体架构
- **Electron架构**
- 主进程:负责文件操作、数据库交互
- 渲染进程负责UI渲染、用户交互
- **数据流架构**
- Excel数据 → 数据处理 → 数据存储 → 数据展示
#### 3.2.2 模块划分
- **数据导入模块**处理Excel导入和合并单元格问题
- **数据存储模块**管理SQLite数据库操作
- **树状结构模块**:构建和管理树状结构
- **数据展示模块**:表格展示和交互
- **详情编辑模块**:表单展示和数据编辑
- **数据分析模块**:多选数据分析功能
## 4. 数据流程分析
### 4.1 数据导入流程
1. 用户选择Excel文件
2. 系统读取Excel文件内容
3. 处理合并单元格问题
4. 提取单位和建设单位字段
5. 构建树状结构数据
6. 将数据导入SQLite数据库
### 4.2 数据查询流程
1. 用户通过树状结构或筛选条件选择查询条件
2. 系统根据条件从SQLite数据库查询数据
3. 将查询结果展示在数据展示区
### 4.3 数据编辑流程
1. 用户点击数据行
2. 系统以表单形式展示详情
3. 用户编辑表单内容
4. 用户保存修改
5. 系统更新SQLite数据库
### 4.4 数据分析流程
1. 用户通过复选框选择多条数据
2. 系统收集选中的数据
3. 系统在右侧展示分析结果
## 5. 数据库设计分析
### 5.1 数据库表设计
#### 5.1.1 主数据表
包含Excel中的所有字段
```
CREATE TABLE 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 -- 备注
)
```
#### 5.1.2 树结构表
用于存储和优化树状结构查询:
```
CREATE TABLE 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)
)
```
### 5.2 索引设计
为提高查询性能,建议创建以下索引:
```
CREATE INDEX idx_unit ON projects(unit);
CREATE INDEX idx_construction_unit ON projects(construction_unit);
CREATE INDEX idx_sub_project_name ON projects(sub_project_name);
CREATE INDEX idx_current_risk_level ON projects(current_risk_level);
```
## 6. 界面设计分析
### 6.1 主界面布局
```
+------------------+------------------------+------------------------+------------------+
| | | | |
| | | | |
| 工具栏 | 树状结构区 | 数据展示区 | 分析结果区 |
| (VSCode风格) | (类似文件目录区) | (类似代码编辑区) | |
| | | | |
| - 工程 | 总部 | [筛选框: 单项工程名称] | |
| - 风险 | ├─ 单位1 | +----+----+----+----+ | |
| - 查找 | │ ├─ 建设单位1 | | □ | 字段1 | ... | | |
| - ... | │ └─ 建设单位2 | +----+----+----+----+ | |
| | └─ 单位2 | | □ | 字段2 | ... | | |
| | ├─ 建设单位3 | +----+----+----+----+ | |
| | └─ 建设单位4 | | □ | 字段3 | ... | | |
| | | +----+----+----+----+ | |
| | | | |
+------------------+------------------------+------------------------+------------------+
```
### 6.2 详情表单设计
点击数据行后弹出的详情表单:
```
+----------------------------------------------+
| 数据详情 [X] |
+----------------------------------------------+
| 单位: [ ] |
| 项目编号: [ ] |
| 安全编码: [ ] |
| 大项工程名称: [ ] |
| 单项工程名称: [ ] |
| ... |
| (所有字段以表单形式展示) |
| |
| |
| [取消] [保存] |
+----------------------------------------------+
```
## 7. 实现难点分析
### 7.1 Excel合并单元格处理
- **难点描述**Excel中的合并单元格在导入时需要特殊处理
- **解决思路**
- 使用专业的Excel处理库如ExcelJS识别合并单元格
- 开发算法取消合并单元格并复制内容
- 对处理后的数据进行验证和清洗
### 7.2 树状结构与数据筛选联动
- **难点描述**:点击树节点需要实时筛选相关数据
- **解决思路**
- 设计合理的数据结构关联树节点和数据
- 优化查询性能,可考虑缓存常用查询结果
- 实现高效的状态管理机制
### 7.3 大数据量处理性能
- **难点描述**当Excel数据量较大时可能影响应用性能
- **解决思路**
- 实现数据分批处理和导入
- 优化SQLite查询合理使用索引
- 实现数据虚拟滚动,只渲染可视区域数据
### 7.4 一键安装实现
- **难点描述**实现Windows一键安装功能
- **解决思路**
- 使用Electron-builder打包应用
- 配置安装程序和自动更新功能
- 处理权限和依赖问题
## 8. 项目风险分析
### 8.1 技术风险
- **风险点**Electron应用体积较大启动较慢
- **应对策略**
- 优化应用打包配置
- 实现启动优化
- 考虑使用更轻量级的框架替代方案
### 8.2 数据处理风险
- **风险点**Excel格式多样可能存在特殊情况处理不当
- **应对策略**
- 全面测试各种Excel格式
- 实现错误处理和数据验证机制
- 提供手动修正功能
### 8.3 用户体验风险
- **风险点**:界面复杂度高,用户上手困难
- **应对策略**
- 提供简洁的操作流程
- 添加必要的用户引导
- 收集用户反馈持续优化
## 9. 总结与建议
### 9.1 项目总结
本项目是一个基于Electron+React的数据处理桌面应用主要用于处理Excel表格数据并提供数据管理功能。项目核心功能包括Excel数据导入与处理、树状结构展示、数据筛选查询、详情编辑等。技术架构采用Electron+React+SQLite界面风格类似VSCode。
### 9.2 实施建议
1. **分阶段开发**
- 第一阶段:实现基础数据导入和存储功能
- 第二阶段:实现树状结构和数据展示功能
- 第三阶段:实现数据编辑和分析功能
- 第四阶段:优化界面和用户体验
2. **技术选型建议**
- Excel处理推荐使用ExcelJS库功能全面
- UI组件推荐Ant Design有丰富的组件和VSCode风格主题
- 数据库操作推荐better-sqlite3性能较好
3. **测试建议**
- 重点测试Excel导入功能尤其是合并单元格处理
- 全面测试树状结构筛选功能
- 性能测试大数据量处理能力
### 9.3 扩展建议
1. **功能扩展**
- 添加数据导出功能
- 实现数据备份和恢复功能
- 添加数据可视化图表功能
2. **性能优化**
- 实现数据缓存机制
- 优化大数据量处理性能
- 减小应用体积
3. **用户体验提升**
- 添加自定义主题功能
- 实现快捷键操作
- 提供操作历史记录和撤销功能