This commit is contained in:
2026-04-08 21:26:18 +08:00
commit 8fdc7ac0c3
401 changed files with 53093 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
{
"globals": {}
}

View File

@@ -0,0 +1,29 @@
appId: com.cslab.dcs.editor
productName: cslab-dcs-editor
directories:
output: dist
buildResources: resources
files:
- out/**/*
- '!**/*.map'
extraMetadata:
main: out/main/index.js
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
deleteAppDataOnUninstall: false
mac:
target: dmg
hardenedRuntime: true
gatekeeperAssess: false
win:
icon: resources/icon.ico
target:
- target: nsis
arch:
- x64
- ia32
artifactName: '${productName}_${version}_win7-setup.${ext}'
linux:
target: AppImage

View File

@@ -0,0 +1,28 @@
import { resolve } from 'node:path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { createSharedViteConfig } from '../../packages/core/vite.shared'
// Core 包的根目录
const coreRoot = resolve(__dirname, '../../packages/core')
// 使用 any 断言绕过 Vite 版本差异electron: vite@5, core: vite@6
const sharedConfig = createSharedViteConfig(coreRoot) as any
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()],
},
renderer: {
...sharedConfig,
root: resolve(__dirname, 'src/renderer'),
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'src/renderer/index.html'),
},
},
},
},
})

View File

@@ -0,0 +1,36 @@
{
"name": "@cslab-dcs/electron",
"version": "1.0.0",
"description": "DCS Editor Electron App",
"author": "cslab-dcs",
"main": "./out/main/index.js",
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"dev": "electron-vite dev",
"build": "electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"@cslab-dcs/bridge": "workspace:*",
"@cslab-dcs/core": "workspace:*",
"@electron-toolkit/preload": "2.0.0",
"@electron-toolkit/utils": "2.0.0",
"electron-updater": "6.3.9"
},
"devDependencies": {
"@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "4.1.1",
"electron": "22.3.27",
"electron-builder": "25.1.8",
"electron-vite": "2.3.0",
"typescript": "5.9.3",
"vite": "5.4.11",
"vue": "3.5.13",
"vue-tsc": "2.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

1
apps/electron/src/main/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="electron-vite/node" />

View File

@@ -0,0 +1,201 @@
/* eslint-disable node/prefer-global/process */
import fs from 'node:fs/promises'
import os from 'node:os'
import { join } from 'node:path'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron'
import icon from '../../resources/icon.png?asset'
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
if (isUrlAllowed(details.url)) {
shell.openExternal(details.url)
}
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
}
else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// ── 文件路径授权管理 ──
// 只允许访问经用户通过原生对话框选择的文件路径
const authorizedPaths = new Set<string>()
const MAX_AUTHORIZED_PATHS = 200
function authorizePath(filePath: string) {
if (authorizedPaths.size >= MAX_AUTHORIZED_PATHS) {
const oldest = authorizedPaths.values().next().value
if (oldest)
authorizedPaths.delete(oldest)
}
authorizedPaths.add(filePath)
}
function isPathAuthorized(filePath: string): boolean {
return authorizedPaths.has(filePath)
}
// ── URL 协议白名单 ──
const ALLOWED_URL_PROTOCOLS = new Set(['https:', 'http:'])
function isUrlAllowed(url: string): boolean {
try {
return ALLOWED_URL_PROTOCOLS.has(new URL(url).protocol)
}
catch {
return false
}
}
// IPC Handlers
function registerIpcHandlers() {
// File — 仅允许经对话框授权的路径
ipcMain.handle('file:read', async (_, path) => {
if (!isPathAuthorized(path)) {
throw new Error('文件访问被拒绝:路径未经用户选择授权')
}
return await fs.readFile(path, 'utf-8')
})
ipcMain.handle('file:exists', async (_, path) => {
if (!isPathAuthorized(path)) {
return false
}
try {
await fs.access(path)
return true
}
catch {
return false
}
})
ipcMain.handle('file:write', async (_, path, content) => {
if (!isPathAuthorized(path)) {
throw new Error('文件写入被拒绝:路径未经用户选择授权')
}
await fs.writeFile(path, content, 'utf-8')
})
// Dialog — 记录用户选择的路径
ipcMain.handle('dialog:open', async (_, options) => {
const { filePaths } = await dialog.showOpenDialog({
title: options?.title,
defaultPath: options?.defaultPath,
filters: options?.filters,
properties: [
options?.multiple ? 'multiSelections' : 'openFile',
options?.directory ? 'openDirectory' : 'openFile',
],
})
filePaths.forEach(p => authorizePath(p))
return options?.multiple ? filePaths : filePaths[0] || null
})
ipcMain.handle('dialog:save', async (_, options) => {
const { filePath } = await dialog.showSaveDialog({
title: options?.title,
defaultPath: options?.defaultPath,
filters: options?.filters,
})
if (filePath)
authorizePath(filePath)
return filePath || null
})
ipcMain.handle('dialog:message', async (_, options) => {
await dialog.showMessageBox({
title: options?.title,
message: options?.message,
type: options?.type || 'info',
})
})
ipcMain.handle('dialog:confirm', async (_, options) => {
const { response } = await dialog.showMessageBox({
title: options?.title,
message: options?.message,
type: options?.type || 'info',
buttons: [options?.okLabel || 'Yes', options?.cancelLabel || 'No'],
defaultId: 0,
cancelId: 1,
})
return response === 0
})
// System
ipcMain.handle('app:version', () => app.getVersion())
ipcMain.handle('app:info', () => ({
name: 'electron',
version: process.versions.electron,
os: process.platform,
osVersion: os.release(),
arch: process.arch,
}))
ipcMain.handle('shell:open', (_, url) => {
if (!isUrlAllowed(url)) {
throw new Error('不允许打开此类型的链接,仅支持 HTTP/HTTPS')
}
return shell.openExternal(url)
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.cslab.dcs')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
registerIpcHandlers()
createWindow()
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0)
createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

8
apps/electron/src/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import type { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: Record<string, unknown>
}
}

View File

@@ -0,0 +1,22 @@
/* eslint-disable node/prefer-global/process */
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge } from 'electron'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose IPC renderer to the renderer process.
// Read more at https://www.electronjs.org/docs/latest/tutorial/context-isolation
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
}
catch (error) {
console.error(error)
}
}
else {
window.electron = electronAPI
window.api = api
}

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>DCS Editor (Electron)</title>
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"> -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

17
apps/electron/src/renderer/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
/// <reference types="vite/client" />
/// <reference types="element-plus/global" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -0,0 +1,5 @@
import { bridge } from '@cslab-dcs/bridge'
import { createDCSApp } from '@cslab-dcs/core'
const app = createDCSApp(bridge)
app.mount('#app')

View File

@@ -0,0 +1,8 @@
{
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.preload.json" },
{ "path": "./tsconfig.web.json" }
],
"files": []
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"],
"outDir": "out"
},
"include": ["src/main/**/*"],
"exclude": ["node_modules", "out", "dist"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"],
"outDir": "out"
},
"include": ["src/preload/**/*"],
"exclude": ["node_modules", "out", "dist"]
}

View File

@@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["../../packages/core/src/*"],
"@cslab-dcs/core": ["../../packages/core/src"],
"@cslab-dcs/bridge": ["../../packages/bridge/src"],
"@cslab-dcs/schema": ["../../packages/schema/src"]
},
"types": ["element-plus/global"]
},
"include": [
"src/renderer/**/*",
"../../packages/core/src/**/*",
"../../packages/bridge/src/**/*",
"../../packages/request/src/**/*",
"../../packages/schema/src/**/*",
"../../packages/utils/src/**/*"
],
"exclude": ["node_modules", "out", "dist"]
}