feat: 完成 Web2App 核心功能实现

- Electron 主进程:窗口管理、IPC、项目读写、打包引擎
- Vue 3 前端:4 个视图页面、3 个组件、Pinia 状态管理
- 生成应用模板:Express 后端 + Electron 窗口
- 支持资源导入、应用配置、跨平台打包
This commit is contained in:
gmh01
2026-07-23 18:25:35 +08:00
parent b67e27beb1
commit 2f3ec636e0
26 changed files with 8455 additions and 0 deletions

163
electron/main.js Normal file
View File

@@ -0,0 +1,163 @@
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('path')
const fs = require('fs')
const { createProject, loadProject, saveProject, importFiles, removeFile, setProjectIcon } = require('./project')
const { startPack, cancelPack } = require('./builder')
let mainWindow
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 900,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
},
title: 'Web2App',
show: false
})
if (process.env.NODE_ENV === 'development' || process.argv.includes('--dev')) {
mainWindow.loadURL('http://localhost:5173')
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'))
}
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
}
ipcMain.handle('dialog:selectDirectory', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory']
})
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0]
}
return null
})
ipcMain.handle('dialog:selectFile', async (_event, options) => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile', 'multiSelections'],
filters: options?.filters || [
{ name: '所有文件', extensions: ['*'] }
]
})
if (!result.canceled) {
return result.filePaths
}
return []
})
ipcMain.handle('dialog:selectIcon', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: '图标文件', extensions: ['ico', 'png', 'icns'] }
]
})
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0]
}
return null
})
ipcMain.handle('dialog:saveFile', async (_event, options) => {
const result = await dialog.showSaveDialog(mainWindow, {
defaultPath: options?.defaultPath,
filters: options?.filters || [{ name: '安装包', extensions: ['exe', 'dmg', 'AppImage'] }]
})
if (!result.canceled && result.filePath) {
return result.filePath
}
return null
})
ipcMain.handle('dir:readTree', async (_event, dirPath) => {
return readDirectoryTree(dirPath)
})
ipcMain.handle('project:create', async (_event, name, dir) => {
return createProject(name, dir)
})
ipcMain.handle('project:load', async (_event, projectPath) => {
return loadProject(projectPath)
})
ipcMain.handle('project:save', async (_event, projectPath, data) => {
return saveProject(projectPath, data)
})
ipcMain.handle('project:importFiles', async (_event, projectPath, files) => {
return importFiles(projectPath, files)
})
ipcMain.handle('project:removeFile', async (_event, projectPath, relativePath) => {
return removeFile(projectPath, relativePath)
})
ipcMain.handle('project:setIcon', async (_event, projectPath, iconPath) => {
return setProjectIcon(projectPath, iconPath)
})
ipcMain.handle('pack:start', async (_event, projectPath, outputPath) => {
const result = await startPack(projectPath, outputPath, (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('pack:progress', progress)
}
})
return result
})
ipcMain.handle('pack:cancel', () => {
cancelPack()
return true
})
function readDirectoryTree(dirPath) {
const items = fs.readdirSync(dirPath, { withFileTypes: true })
const result = []
for (const item of items) {
if (item.name.startsWith('.')) continue
const fullPath = path.join(dirPath, item.name)
if (item.isDirectory()) {
result.push({
name: item.name,
path: fullPath,
type: 'directory',
children: readDirectoryTree(fullPath)
})
} else {
result.push({
name: item.name,
path: fullPath,
type: 'file',
size: fs.statSync(fullPath).size,
ext: path.extname(item.name).toLowerCase()
})
}
}
return result.sort((a, b) => {
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
return a.name.localeCompare(b.name)
})
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})