目录
[2问题英文路径打包不了,git bash复制换项目位置](#2问题英文路径打包不了,git bash复制换项目位置)
[解决electron/main.cjs 里加了前缀剥离](#解决electron/main.cjs 里加了前缀剥离)
0正常步骤
-
装依赖
pnpm add -D electron electron-builder
电子-建造者 -
本地调试(两个终端)
pnpm dev # 终端1:vite dev server
pnpm dev:electron # 终端2:electron 加载 http://localhost:5173/admin/ -
打包 exe
pnpm build:win
1如果下时间比较久卡着了-①换国内镜像②项目纯英文路径
1
"build:win": "pnpm build && cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-builder --win --x64"

2
-
装 cross-env
pnpm add -D cross-env
-
重新打包
pnpm build:win
2问题英文路径打包不了,git bash复制换项目位置
mkdir -p F:/work/ticket-admin
cp -r "F:/vue3/公司/排号系统/ticket-machine-admin/"{src,electron,public,package.json,pnpm-lock.yaml,tsconfig.json,tsconfig.app.json,tsconfig.node.json,vite.config.ts,index.html,.env.production,.env.development} F:/work/ticket-admin/
ls F:/work/ticket-admin
成功\release\文件里面找exe
rm -rf release

3问题安装好了打开是白屏,打包路径问题
vite 打包后 index.html 里引用的是 /admin/assets/index-xxx.js,但文件实际在 dist/assets/。我原来的服务逻辑直接 path.join(rootDir, '/admin/assets/xxx.js') → 找 dist/admin/assets/,不存在 → 回退返回 index.html。
解决electron/main.cjs 里加剥离前缀,把 URL 映射回真实的 dist 结构
/**
* 排号机 Electron 主进程
*
* 设计要点:
* 1. 不改渲染层。现有 base='/admin/'、createBrowserRouter、相对 API('/basic-api') 全部保持原样。
* Electron 下用 http://168.3.0.1:<port>/admin/ 加载,避免 file:// 协议下路由 404 与 base 失效。
* 2. 主进程内置静态服务托管 dist,并复刻 vite dev server 的 proxy 规则(/basic-api -> 后端)。
* 这样 request.ts 和 .env.production 无需任何改动,也不会引入跨域。
* 3. 设备参数(deviceCode / branchId)从 config.json 读,拼到 URL query 上,
* 与浏览器里 ?deviceCode=xx 的入参方式保持一致。
*/
const { app, BrowserWindow, dialog } = require('electron')
const http = require('http')
const fs = require('fs')
const path = require('path')
const isDev = !app.isPackaged
const DEV_SERVER = 'http://localhost:5173' // vite dev server
const STATIC_PORT = 18231 // 生产内置静态服务端口
const WEB_PREFIX = '/admin'
const API_PREFIX = '/basic-api'
// 必须与 vite.config.ts 中的 proxy.target 保持一致
const API_TARGET = { host: '10.1.68.35', port: 5721 }
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
}
let mainWindow = null
let staticServer = null
/** 读取设备配置:优先 exe 同级 config.json(方便现场运维改),其次 userData */
function readDeviceConfig() {
const candidates = isDev
? [path.join(app.getPath('userData'), 'config.json')]
: [
path.join(path.dirname(process.execPath), 'config.json'),
path.join(app.getPath('userData'), 'config.json'),
]
for (const file of candidates) {
try {
return JSON.parse(fs.readFileSync(file, 'utf-8'))
} catch (_) {
/* 继续尝试下一个候选路径 */
}
}
return {}
}
/** 把 /basic-api/xxx 转发到后端,路径前缀剥离逻辑与 vite rewrite 一致 */
function proxyApi(req, res) {
const upstreamPath = req.url.replace(/^\/basic-api/, '') || '/'
const proxyReq = http.request(
{
host: API_TARGET.host,
port: API_TARGET.port,
path: upstreamPath,
method: req.method,
headers: { ...req.headers, host: `${API_TARGET.host}:${API_TARGET.port}` },
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers)
proxyRes.pipe(res)
}
)
proxyReq.on('error', (err) => {
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' })
res.end(JSON.stringify({ code: 500, message: `后端服务不可达: ${err.message}` }))
})
req.pipe(proxyReq)
}
/** 托管 dist:/admin/* 映射到 dist/*,其余路径回退到 index.html(SPA 路由) */
function startStaticServer(rootDir) {
staticServer = http.createServer((req, res) => {
if (req.url.startsWith(API_PREFIX)) {
proxyApi(req, res)
return
}
const urlPath = decodeURIComponent(req.url.split('?')[0])
// vite base='/admin/' 会让产物引用 /admin/assets/*,但文件实际落在 dist/assets/*
// 这里剥离前缀,把 URL 映射回真实的 dist 结构
let relativePath = urlPath
if (relativePath === WEB_PREFIX || relativePath === `${WEB_PREFIX}/`) {
relativePath = '/'
} else if (relativePath.startsWith(`${WEB_PREFIX}/`)) {
relativePath = relativePath.slice(WEB_PREFIX.length)
}
let filePath = path.join(rootDir, relativePath)
// 目录穿越防护
if (!filePath.startsWith(rootDir)) {
res.writeHead(403)
res.end('Forbidden')
return
}
const isDir = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()
// 入口页与 SPA 子路由都回退到 index.html,保证前端路由能正常接管
if (relativePath === '/' || isDir || !fs.existsSync(filePath)) {
filePath = path.join(rootDir, 'index.html')
}
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
res.end('Not Found')
return
}
res.writeHead(200, { 'Content-Type': MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream' })
res.end(data)
})
})
staticServer.listen(STATIC_PORT, '127.0.0.1', () => {
createWindow()
})
}
function buildEntryUrl() {
const cfg = readDeviceConfig()
const query = new URLSearchParams({
deviceCode: cfg.deviceCode || '',
branchId: cfg.branchId || '',
}).toString()
const origin = isDev ? DEV_SERVER : `http://168.1.0.1:${STATIC_PORT}`
return `${origin}${WEB_PREFIX}/?${query}`
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
show: false,
frame: false, // 自助机无边框
fullscreen: true,
autoHideMenuBar: true,
backgroundColor: '#ffffff',
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
webSecurity: true,
},
})
mainWindow.loadURL(buildEntryUrl())
mainWindow.once('ready-to-show', () => mainWindow.show())
// 自助机场景:屏蔽快捷键退出,防止现场人员误操作
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.key === 'r')) event.preventDefault()
})
}
// 单实例:设备重复启动会导致本地静态服务端口冲突
if (!app.requestSingleInstanceLock()) {
app.quit()
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
}
})
app.whenReady().then(() => {
if (isDev) {
createWindow() // 开发态直接连 vite dev server
} else {
const rootDir = path.join(__dirname, '..', 'dist')
if (!fs.existsSync(path.join(rootDir, 'index.html'))) {
dialog.showErrorBox('启动失败', `未找到前端构建产物: ${rootDir}`)
app.quit()
return
}
startStaticServer(rootDir)
}
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
}
app.on('window-all-closed', () => app.quit())
