chore: starter

This commit is contained in:
2026-01-29 18:44:04 +08:00
commit 9cda358a2e
91 changed files with 16946 additions and 0 deletions

20
.editorconfig Normal file
View File

@@ -0,0 +1,20 @@
# EditorConfig is awesome: https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[Makefile]
indent_style = tab

58
.gitignore vendored Normal file
View File

@@ -0,0 +1,58 @@
# Dependencies
node_modules
.pnpm-store
# Build outputs
dist
out
*.local
# Electron
release
# Tauri
**/src-tauri/target
**/src-tauri/gen
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*~
# Environment files
.env
.env.*
!.env.example
# Test coverage
coverage
*.lcov
# TypeScript cache
*.tsbuildinfo
# Vite
.vite
# Auto-generated type declarations
src/auto-imports.d.ts
src/components.d.ts
auto-imports.d.ts
components.d.ts

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
shamefully-hoist=true
strict-peer-dependencies=false
auto-install-peers=true

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

39
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}

50
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,50 @@
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in your IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}

114
README.md Normal file
View File

@@ -0,0 +1,114 @@
# DCS Editor (cslab-dcs)
DCS Editor 是一个跨平台的配置文件编辑器,旨在 Web、Windows 和 macOS 环境下提供一致的体验。本项目采用 **Monorepo** 架构,最大化地实现了跨平台代码复用。
## 🏗 项目架构
本项目使用 `pnpm` workspaces 进行管理:
### `apps/` (宿主应用)
- **`web`**: 纯浏览器版本,使用 Vite 构建。
- **`electron`**: 面向 Windows 7+ (及 Linux/macOS) 的桌面版本,使用 Electron + Vite 构建。
- **`tauri`**: 面向 Windows 10+ 和 macOS 的现代化桌面版本,使用 Tauri 2.x + Vite 构建。
### `packages/` (共享库)
- **`core`**: 包含核心应用逻辑、Vue 视图、组件、路由和状态管理 (Pinia)。**几乎所有的 UI 代码都可以在这里找到。**
- **`bridge`**: 跨平台抽象层 (Bridge 模式)。它提供了一个统一的接口来访问系统能力 (文件读写、对话框等),并在运行时自动适配 Web、Electron 或 Tauri 环境。
- **`schema`**: 使用 Zod 定义的共享数据校验 Schema。
- **`utils`**: 通用工具函数。
## 🛠 技术栈
- **核心框架**: Vue 3, TypeScript, Vite
- **状态管理**: Pinia + pinia-plugin-persistedstate
- **UI 框架**: Element Plus (自动引入)
- **路由**: Vue Router
- **工具库**: VueUse, ofetch, dayjs, mitt, Zod
- **跨平台容器**: Electron (electron-vite), Tauri 2.x
## 🚀 快速开始
### 环境依赖
- **Node.js**: >= 20.0.0
- **pnpm**: >= 9.0.0
- **Rust**: 开发 Tauri 应用必需 (通过 [rustup](https://rustup.rs/) 安装)
### 安装
```bash
# 安装所有包的依赖
pnpm install
```
### 开发模式
启动特定平台的开发环境:
```bash
# Web 浏览器开发
pnpm dev:web
# Electron 应用开发
pnpm dev:electron
# Tauri 应用开发
pnpm dev:tauri
```
### 构建 (Build)
构建生产环境版本:
```bash
# 构建 Web 版本 (输出: apps/web/dist)
pnpm build:web
# 构建 Windows Electron 版本 (输出: apps/electron/dist)
pnpm build:electron:win
# 构建 Tauri 应用 (输出: apps/tauri/src-tauri/target/release/bundle)
# 自动检测当前系统 (macOS 生成 dmg/app, Windows 生成 exe/msi)
pnpm build:tauri
```
## 📂 目录结构
```
cslab-dcs/
├── apps/
│ ├── electron/ # Electron 宿主
│ ├── tauri/ # Tauri 宿主
│ └── web/ # Web 宿主
├── packages/
│ ├── bridge/ # 平台适配层 (Web/Electron/Tauri)
│ ├── core/ # 核心共享 Vue 应用
│ ├── schema/ # Zod 定义
│ └── utils/ # 工具函数
├── package.json # Workspace 配置
├── pnpm-workspace.yaml
└── README.md
```
## 🔌 Bridge 使用指南
项目使用全局 `bridge` 对象来调用系统 API。详细定义请查看 `packages/bridge`
```typescript
import { bridge } from '@cslab-dcs/bridge'
// 示例:打开文件对话框 (在 Web*, Electron, Tauri 下均可工作)
const result = await bridge.file.openDialog({
title: '打开文件',
filters: [{ name: 'JSON', extensions: ['json'] }],
})
// *在 Web 端,会自动模拟文件上传点击行为。
```
## ⚠️ 注意事项
- **Tauri**: 如果添加了新的插件,请确保在 `apps/tauri/src-tauri/capabilities` 中配置了相应的权限。
- **Electron**: 使用 `electron-vite` 进行构建优化。

View File

@@ -0,0 +1,319 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"asyncComputed": true,
"autoResetRef": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createInjectionState": true,
"createPinia": true,
"createReactiveFn": true,
"createRef": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"eagerComputed": true,
"effectScope": true,
"extendRef": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"injectLocal": true,
"isDefined": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"makeDestructurable": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onElementRemoval": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"pausableWatch": true,
"provide": true,
"provideLocal": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useAnimate": true,
"useArrayDifference": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayFindLast": true,
"useArrayIncludes": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useArrayUnique": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClipboard": true,
"useClipboardItems": true,
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCountdown": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useId": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLink": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useModel": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"useParentElement": true,
"usePerformanceObserver": true,
"usePermission": true,
"usePointer": true,
"usePointerLock": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredContrast": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"usePreferredReducedTransparency": true,
"usePrevious": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useSSRWidth": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSorted": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRef": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchDeep": true,
"watchEffect": true,
"watchIgnorable": true,
"watchImmediate": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

View File

@@ -0,0 +1,27 @@
appId: com.cslab.dcs.editor
productName: DCS Editor
directories:
output: dist
buildResources: build
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:
target:
- target: nsis
arch:
- x64
- ia32
linux:
target: AppImage

View File

@@ -0,0 +1,27 @@
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')
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": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"electron-updater": "^6.3.9"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"electron": "^33.2.1",
"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: 35 KiB

View File

@@ -0,0 +1,150 @@
/* eslint-disable node/prefer-global/process */
import fs from 'node:fs/promises'
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: false,
contextIsolation: true,
nodeIntegration: false,
},
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
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'))
}
}
// IPC Handlers
function registerIpcHandlers() {
// File
ipcMain.handle('file:read', async (_, path) => {
return await fs.readFile(path, 'utf-8')
})
ipcMain.handle('file:exists', async (_, path) => {
try {
await fs.access(path)
return true
}
catch {
return false
}
})
ipcMain.handle('file:write', async (_, path, content) => {
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',
],
})
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,
})
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,
arch: process.arch,
}))
ipcMain.handle('shell:open', (_, url) => 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()
}
})

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>

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,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"moduleResolution": "node",
"typeRoots": ["node_modules/@types", "src/preload"],
"outDir": "out/main"
},
"include": ["src/main/**/*", "src/preload/**/*", "src/renderer/**/*"],
"exclude": ["node_modules", "out", "dist"]
}

View File

@@ -0,0 +1,319 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"asyncComputed": true,
"autoResetRef": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createInjectionState": true,
"createPinia": true,
"createReactiveFn": true,
"createRef": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"eagerComputed": true,
"effectScope": true,
"extendRef": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"injectLocal": true,
"isDefined": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"makeDestructurable": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onElementRemoval": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"pausableWatch": true,
"provide": true,
"provideLocal": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useAnimate": true,
"useArrayDifference": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayFindLast": true,
"useArrayIncludes": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useArrayUnique": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClipboard": true,
"useClipboardItems": true,
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCountdown": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useId": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLink": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useModel": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"useParentElement": true,
"usePerformanceObserver": true,
"usePermission": true,
"usePointer": true,
"usePointerLock": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredContrast": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"usePreferredReducedTransparency": true,
"usePrevious": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useSSRWidth": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSorted": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRef": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchDeep": true,
"watchEffect": true,
"watchIgnorable": true,
"watchImmediate": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

13
apps/tauri/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DCS Editor (Tauri)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

29
apps/tauri/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "@cslab-dcs/tauri",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@cslab-dcs/bridge": "workspace:*",
"@cslab-dcs/core": "workspace:*",
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.5",
"vue": "^3.5.13"
},
"devDependencies": {
"@tauri-apps/cli": "^2.1.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"typescript": "^5.9.3",
"vite": "^6.0.3",
"vue-tsc": "^2.2.0"
}
}

5214
apps/tauri/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
[package]
name = "cslab-dcs-tauri"
version = "0.1.0"
description = "A Tauri App"
authors = [ "you" ]
edition = "2021"
[lib]
name = "cslab_dcs_tauri_lib"
crate-type = [
"staticlib",
"cdylib",
"rlib"
]
[build-dependencies]
tauri-build = { version = "2.0.3", features = [] }
[dependencies]
tauri = { version = "2.1.1", features = [] }
tauri-plugin-opener = "2.2.5"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"opener:default",
"fs:default"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,9 @@
// #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cslab_dcs_tauri_lib::run()
}

View File

@@ -0,0 +1,39 @@
{
"identifier": "com.cslab.dcs.editor",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "DCS Editor",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"plugins": {
"fs": {}
}
}

9
apps/tauri/src/main.ts Normal file
View File

@@ -0,0 +1,9 @@
import { WebBridge } from '@cslab-dcs/bridge'
import { TauriBridge } from '@cslab-dcs/bridge/tauri'
import { createDCSApp } from '@cslab-dcs/core'
const isTauri = !!(window as any).__TAURI_INTERNALS__ || !!(window as any).__TAURI__
const bridge = isTauri ? new TauriBridge() : new WebBridge()
const app = createDCSApp(bridge)
app.mount('#app')

12
apps/tauri/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["../../packages/core/src/*"],
"@cslab-dcs/core": ["../../packages/core/src"]
}
},
"include": ["src/**/*", "../../packages/core/src/**/*"],
"exclude": ["node_modules", "dist"]
}

34
apps/tauri/vite.config.ts Normal file
View File

@@ -0,0 +1,34 @@
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
/* eslint-disable node/prefer-global/process */
import { createSharedViteConfig } from '../../packages/core/vite.shared'
const coreRoot = resolve(__dirname, '../../packages/core')
const host = process.env.TAURI_DEV_HOST
export default defineConfig({
...createSharedViteConfig(coreRoot),
root: __dirname,
clearScreen: false,
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: 'ws',
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ['**/src-tauri/**'],
},
},
build: {
outDir: 'dist',
emptyOutDir: true,
},
})

View File

@@ -0,0 +1,319 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"asyncComputed": true,
"autoResetRef": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createInjectionState": true,
"createPinia": true,
"createReactiveFn": true,
"createRef": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"eagerComputed": true,
"effectScope": true,
"extendRef": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"injectLocal": true,
"isDefined": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"makeDestructurable": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onElementRemoval": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"pausableWatch": true,
"provide": true,
"provideLocal": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useAnimate": true,
"useArrayDifference": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayFindLast": true,
"useArrayIncludes": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useArrayUnique": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClipboard": true,
"useClipboardItems": true,
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCountdown": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useId": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLink": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useModel": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"useParentElement": true,
"usePerformanceObserver": true,
"usePermission": true,
"usePointer": true,
"usePointerLock": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredContrast": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"usePreferredReducedTransparency": true,
"usePrevious": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useSSRWidth": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSorted": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRef": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchDeep": true,
"watchEffect": true,
"watchIgnorable": true,
"watchImmediate": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

13
apps/web/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DCS Editor (Web)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

24
apps/web/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "@cslab-dcs/web",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@cslab-dcs/bridge": "workspace:*",
"@cslab-dcs/core": "workspace:*",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"typescript": "^5.9.3",
"vite": "^6.0.3",
"vue-tsc": "^2.2.0"
}
}

5
apps/web/src/main.ts Normal file
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')

12
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["../../packages/core/src/*"],
"@cslab-dcs/core": ["../../packages/core/src"]
}
},
"include": ["src/**/*", "../../packages/core/src/**/*"],
"exclude": ["node_modules", "dist"]
}

19
apps/web/vite.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import { createSharedViteConfig } from '../../packages/core/vite.shared'
// Core 包的根目录
const coreRoot = resolve(__dirname, '../../packages/core')
export default defineConfig({
...createSharedViteConfig(coreRoot),
root: __dirname,
base: './', // 确保相对路径,方便部署
build: {
outDir: 'dist',
emptyOutDir: true,
},
server: {
port: 5173,
},
})

3
eslint.config.js Normal file
View File

@@ -0,0 +1,3 @@
import antfu from '@antfu/eslint-config'
export default antfu()

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "cslab-dcs",
"type": "module",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@9.15.0",
"description": "DCS Editor - A cross-platform configuration editor",
"engines": {
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
},
"scripts": {
"dev:web": "pnpm -F @cslab-dcs/web dev",
"dev:electron": "pnpm -F @cslab-dcs/electron dev",
"dev:tauri": "pnpm -F @cslab-dcs/tauri tauri dev",
"build:web": "pnpm -F @cslab-dcs/web build",
"build:electron:win": "pnpm -F @cslab-dcs/electron build:win",
"build:tauri": "pnpm -F @cslab-dcs/tauri tauri build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "pnpm -r typecheck",
"clean": "pnpm -r exec rm -rf dist out node_modules/.vite",
"clean:all": "pnpm -r exec rm -rf dist out node_modules && rm -rf node_modules"
},
"devDependencies": {
"@antfu/eslint-config": "^7.2.0",
"@types/node": "^22.19.7",
"eslint": "^9.39.2",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,31 @@
{
"name": "@cslab-dcs/bridge",
"type": "module",
"version": "1.0.0",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./tauri": {
"types": "./src/tauri.ts",
"import": "./src/tauri.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.5"
},
"devDependencies": {
"electron": "^33.2.1",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,89 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
IStorageService,
ISystemService,
MessageOptions,
PlatformType,
SaveDialogOptions,
} from '../types'
// Electron IPC 接口定义(需要在 electron preload 中实现)
interface IElectronAPI {
ipcRenderer: {
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>
send: (channel: string, ...args: unknown[]) => void
on: (channel: string, listener: (event: unknown, ...args: unknown[]) => void) => void
removeListener: (channel: string, listener: (...args: unknown[]) => void) => void
}
}
declare global {
interface Window {
electron: IElectronAPI
}
}
export class ElectronBridge implements IPlatformBridge {
readonly platform: PlatformType = 'electron'
file: IFileService = {
async read(path: string): Promise<string> {
return (await window.electron.ipcRenderer.invoke('file:read', path)) as string
},
async write(path: string, content: string): Promise<void> {
await window.electron.ipcRenderer.invoke('file:write', path, content)
},
async exists(path: string): Promise<boolean> {
return (await window.electron.ipcRenderer.invoke('file:exists', path)) as boolean
},
async openDialog(options?: FileDialogOptions): Promise<string | string[] | null> {
return (await window.electron.ipcRenderer.invoke('dialog:open', options)) as string | string[] | null
},
async saveDialog(options?: SaveDialogOptions): Promise<string | null> {
return (await window.electron.ipcRenderer.invoke('dialog:save', options)) as string | null
},
}
dialog: IDialogService = {
async message(options: MessageOptions): Promise<void> {
await window.electron.ipcRenderer.invoke('dialog:message', options)
},
async confirm(options: ConfirmOptions): Promise<boolean> {
return (await window.electron.ipcRenderer.invoke('dialog:confirm', options)) as boolean
},
}
storage: IStorageService = {
async get<T>(key: string): Promise<T | null> {
// Electron 环境也可以复用 localStorage或者使用 electron-store
// 这里为了简单,先使用 localStorage生产环境建议走 IPC 存本地文件
const val = localStorage.getItem(key)
return val ? JSON.parse(val) : null
},
async set<T>(key: string, value: T): Promise<void> {
localStorage.setItem(key, JSON.stringify(value))
},
async remove(key: string): Promise<void> {
localStorage.removeItem(key)
},
async clear(): Promise<void> {
localStorage.clear()
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return (await window.electron.ipcRenderer.invoke('app:version')) as string
},
async getPlatformInfo() {
return (await window.electron.ipcRenderer.invoke('app:info')) as any
},
async openExternal(url: string): Promise<void> {
await window.electron.ipcRenderer.invoke('shell:open', url)
},
}
}

View File

@@ -0,0 +1,103 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
IStorageService,
ISystemService,
MessageOptions,
PlatformType,
SaveDialogOptions,
} from '../types'
import { getTauriVersion, getVersion } from '@tauri-apps/api/app'
import { ask, message, open, save } from '@tauri-apps/plugin-dialog'
import { exists, readTextFile, writeTextFile } from '@tauri-apps/plugin-fs'
import { openUrl } from '@tauri-apps/plugin-opener'
import { arch, platform } from '@tauri-apps/plugin-os'
export class TauriBridge implements IPlatformBridge {
readonly platform: PlatformType = 'tauri'
file: IFileService = {
async read(path: string): Promise<string> {
return await readTextFile(path)
},
async write(path: string, content: string): Promise<void> {
await writeTextFile(path, content)
},
async exists(path: string): Promise<boolean> {
return await exists(path)
},
async openDialog(options?: FileDialogOptions): Promise<string | string[] | null> {
const selected = await open({
multiple: options?.multiple ?? false,
directory: options?.directory ?? false,
filters: options?.filters,
defaultPath: options?.defaultPath,
})
if (selected === null)
return null
// 这里的类型转换取决于 multiple 选项
return selected as string | string[]
},
async saveDialog(options?: SaveDialogOptions): Promise<string | null> {
return await save({
defaultPath: options?.defaultPath,
filters: options?.filters,
})
},
}
dialog: IDialogService = {
async message(options: MessageOptions): Promise<void> {
await message(options.message, {
title: options.title,
kind: options.type === 'error' ? 'error' : options.type === 'warning' ? 'warning' : 'info',
})
},
async confirm(options: ConfirmOptions): Promise<boolean> {
return await ask(options.message, {
title: options.title,
kind: options.type === 'error' ? 'error' : options.type === 'warning' ? 'warning' : 'info',
okLabel: options.okLabel || 'Yes',
cancelLabel: options.cancelLabel || 'No',
})
},
}
storage: IStorageService = {
async get<T>(key: string): Promise<T | null> {
// Tauri 也可以用 tauri-plugin-store这里简化用 localStorage
const val = localStorage.getItem(key)
return val ? JSON.parse(val) : null
},
async set<T>(key: string, value: T): Promise<void> {
localStorage.setItem(key, JSON.stringify(value))
},
async remove(key: string): Promise<void> {
localStorage.removeItem(key)
},
async clear(): Promise<void> {
localStorage.clear()
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return await getVersion()
},
async getPlatformInfo() {
return {
name: 'tauri',
version: await getTauriVersion(),
os: await platform(),
arch: await arch(),
}
},
async openExternal(url: string): Promise<void> {
await openUrl(url)
},
}
}

View File

@@ -0,0 +1,95 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
IStorageService,
ISystemService,
MessageOptions,
PlatformType,
SaveDialogOptions,
} from '../types'
export class WebBridge implements IPlatformBridge {
readonly platform: PlatformType = 'web'
file: IFileService = {
async read(_path: string): Promise<string> {
console.warn('Web environment does not support file system access directly.')
return ''
},
async write(_path: string, _content: string): Promise<void> {
console.warn('Web environment does not support file system access directly.')
},
async exists(_path: string): Promise<boolean> {
return false
},
async openDialog(_options?: FileDialogOptions): Promise<string | string[] | null> {
return new Promise((resolve) => {
const input = document.createElement('input')
input.type = 'file'
// Web端模拟仅用于演示
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
if (files && files.length > 0) {
// Web端通常不能获取完整路径这里只是模拟
resolve(files[0].name)
}
else {
resolve(null)
}
}
input.click()
})
},
async saveDialog(_options?: SaveDialogOptions): Promise<string | null> {
console.warn('Web environment save dialog not fully supported.')
return null
},
}
dialog: IDialogService = {
async message(options: MessageOptions): Promise<void> {
// eslint-disable-next-line no-alert
alert(`${options.title ? `${options.title}\n` : ''}${options.message}`)
},
async confirm(options: ConfirmOptions): Promise<boolean> {
// eslint-disable-next-line no-alert
return confirm(`${options.title ? `${options.title}\n` : ''}${options.message}`)
},
}
storage: IStorageService = {
async get<T>(key: string): Promise<T | null> {
const val = localStorage.getItem(key)
return val ? JSON.parse(val) : null
},
async set<T>(key: string, value: T): Promise<void> {
localStorage.setItem(key, JSON.stringify(value))
},
async remove(key: string): Promise<void> {
localStorage.removeItem(key)
},
async clear(): Promise<void> {
localStorage.clear()
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return '1.0.0'
},
async getPlatformInfo() {
return {
name: 'web',
version: navigator.userAgent,
os: navigator.platform,
arch: 'unknown',
}
},
async openExternal(url: string): Promise<void> {
window.open(url, '_blank')
},
}
}

View File

@@ -0,0 +1,23 @@
import type { IPlatformBridge } from './types'
import { ElectronBridge } from './adapters/electron'
import { WebBridge } from './adapters/web'
export { ElectronBridge } from './adapters/electron'
export { WebBridge } from './adapters/web'
export * from './types'
export function createBridge(): IPlatformBridge {
if (typeof window !== 'undefined') {
const win = window as any
if (win.electron && win.electron.ipcRenderer) {
return new ElectronBridge()
}
// Tauri 检测逻辑 - 注意:这里不能引用 TauriBridge否则会破坏 Electron 构建
// Tauri App 必须手动初始化 Bridge 并通过 provide 注入,或者我们依赖 userAgent 判断
// 但 instantiation 必须在 app 层做。
}
return new WebBridge()
}
export const bridge = createBridge()

View File

@@ -0,0 +1 @@
export * from './adapters/tauri'

View File

@@ -0,0 +1,77 @@
/**
* 平台桥接类型定义
*/
export type PlatformType = 'web' | 'electron' | 'tauri'
export interface FileDialogOptions {
title?: string
defaultPath?: string
filters?: {
name: string
extensions: string[]
}[]
multiple?: boolean
directory?: boolean
}
export interface SaveDialogOptions {
title?: string
defaultPath?: string
filters?: {
name: string
extensions: string[]
}[]
}
export interface MessageOptions {
title?: string
message: string
type?: 'info' | 'warning' | 'error'
}
export interface ConfirmOptions extends MessageOptions {
okLabel?: string
cancelLabel?: string
}
export interface PlatformInfo {
name: PlatformType
version: string
os: string
arch: string
}
export interface IFileService {
read: (path: string) => Promise<string>
write: (path: string, content: string) => Promise<void>
exists: (path: string) => Promise<boolean>
openDialog: (options?: FileDialogOptions) => Promise<string | string[] | null>
saveDialog: (options?: SaveDialogOptions) => Promise<string | null>
}
export interface IDialogService {
message: (options: MessageOptions) => Promise<void>
confirm: (options: ConfirmOptions) => Promise<boolean>
}
export interface IStorageService {
get: <T>(key: string) => Promise<T | null>
set: <T>(key: string, value: T) => Promise<void>
remove: (key: string) => Promise<void>
clear: () => Promise<void>
}
export interface ISystemService {
getAppVersion: () => Promise<string>
getPlatformInfo: () => Promise<PlatformInfo>
openExternal: (url: string) => Promise<void>
}
export interface IPlatformBridge {
readonly platform: PlatformType
file: IFileService
dialog: IDialogService
storage: IStorageService
system: ISystemService
}

View File

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

13
packages/core/env.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
/// <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 Window {
// 可以扩展 window 类型
}

View File

@@ -0,0 +1,46 @@
{
"name": "@cslab-dcs/core",
"type": "module",
"version": "1.0.0",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./vite.shared": {
"types": "./vite.shared.d.ts",
"import": "./vite.shared.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@cslab-dcs/bridge": "workspace:*",
"@cslab-dcs/schema": "workspace:*",
"@cslab-dcs/utils": "workspace:*",
"@vueuse/core": "^12.0.0",
"dayjs": "^1.11.13",
"element-plus": "^2.9.0",
"mitt": "^3.0.1",
"ofetch": "^1.4.1",
"pinia": "^3.0.0",
"pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"sass": "^1.83.0",
"typescript": "^5.9.3",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vite": "^6.0.3",
"vue-tsc": "^2.2.0"
}
}

24
packages/core/src/App.vue Normal file
View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
const locale = zhCn
</script>
<template>
<ElConfigProvider :locale="locale">
<router-view />
</ElConfigProvider>
</template>
<style>
/* 全局样式 */
html, body, #app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
</style>

View File

@@ -0,0 +1,26 @@
@use './variables.scss' as *;
/* Reset or Base styles */
body {
background-color: $bg-color-page;
color: $text-color-primary;
}
/* Scrollbar customization */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: $border-color;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: $text-color-secondary;
}

View File

@@ -0,0 +1,18 @@
/* Global SCSS Variables */
$primary-color: #409eff;
$success-color: #67c23a;
$warning-color: #e6a23c;
$danger-color: #f56c6c;
$info-color: #909399;
$text-color-primary: #303133;
$text-color-regular: #606266;
$text-color-secondary: #909399;
$text-color-placeholder: #a8abb2;
$border-color: #dcdfe6;
$border-color-light: #e4e7ed;
$border-color-lighter: #ebeef5;
$bg-color: #ffffff;
$bg-color-page: #f2f3f5;

View File

@@ -0,0 +1,38 @@
import type { IPlatformBridge } from '@cslab-dcs/bridge'
import type { App } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
/**
* 核心包入口
*/
import { createApp } from 'vue'
import AppRoot from './App.vue'
import router from './router'
import './assets/styles/index.scss'
export function createDCSApp(bridge?: IPlatformBridge): App {
const app = createApp(AppRoot)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
// 如果没有传入 bridge尝试自动创建但这可能无法涵盖 Tauri
// 实际上,为了避免自动创建时的依赖问题,我们建议 Apps 必须传入 bridge
// 或者我们在 @cslab-dcs/core 中不直接依赖 @cslab-dcs/bridge 的实现,只依赖类型
// 但这里为了方便,我们只对 globalProperties 做注入
if (bridge) {
app.config.globalProperties.$bridge = bridge
app.provide('bridge', bridge)
}
return app
}
export { default as AppRoot } from './App.vue'
export * from './router'
export * from './stores'

View File

@@ -0,0 +1,19 @@
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue'),
meta: { title: '首页' },
},
// 可以添加更多路由
]
const router = createRouter({
history: createWebHashHistory(), // 使用 Hash 模式兼容 Electron
routes,
})
export default router

View File

@@ -0,0 +1,17 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAppStore = defineStore('app', () => {
const theme = ref<'light' | 'dark'>('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
return {
theme,
toggleTheme,
}
}, {
persist: true,
})

View File

@@ -0,0 +1,8 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
export const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export * from './app'
export default pinia

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
import type { IPlatformBridge } from '@cslab-dcs/bridge'
import { bridge as defaultBridge } from '@cslab-dcs/bridge'
import { inject, onMounted, ref } from 'vue'
const platformInfo = ref<any>(null)
const appVersion = ref('')
const bridge = inject<IPlatformBridge>('bridge', defaultBridge)
onMounted(async () => {
platformInfo.value = await bridge.system.getPlatformInfo()
appVersion.value = await bridge.system.getAppVersion()
})
async function handleOpenFile() {
const result = await bridge.file.openDialog({
title: '打开配置',
filters: [{ name: 'JSON', extensions: ['json'] }],
})
if (result) {
await bridge.dialog.message({
title: '选择文件',
message: `你选择了: ${Array.isArray(result) ? result.join(', ') : result}`,
})
}
}
function handleGreet() {
bridge.dialog.message({
title: 'Hello',
message: `Welcome to DCS Editor on ${platformInfo.value?.name}`,
})
}
</script>
<template>
<div class="home-container">
<div class="content">
<h1>DCS Editor ({{ platformInfo?.name || 'Loading...' }})</h1>
<p>Version: {{ appVersion }}</p>
<div class="card">
<el-button type="primary" @click="handleGreet">
打招呼
</el-button>
<el-button type="success" @click="handleOpenFile">
打开文件
</el-button>
</div>
<div class="debug-info">
<h3>Platform Info:</h3>
<pre>{{ JSON.stringify(platformInfo, null, 2) }}</pre>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.home-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
.content {
text-align: center;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
.card {
margin: 20px 0;
gap: 10px;
display: flex;
justify-content: center;
}
.debug-info {
margin-top: 20px;
text-align: left;
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
pre {
margin: 0;
font-size: 12px;
}
}
}
}
</style>

View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@cslab-dcs/bridge": ["../bridge/src"],
"@cslab-dcs/schema": ["../schema/src"],
"@cslab-dcs/utils": ["../utils/src"]
},
"types": ["element-plus/global"]
},
"include": ["src/**/*", "vite.shared.ts", "env.d.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,43 @@
import type { UserConfig } from 'vite'
import { resolve } from 'node:path'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
export function createSharedViteConfig(rootDir: string): UserConfig {
return {
plugins: [
vue(),
vueJsx(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
resolvers: [ElementPlusResolver()],
dts: resolve(rootDir, 'src/auto-imports.d.ts'),
eslintrc: {
enabled: true,
},
}),
Components({
resolvers: [ElementPlusResolver()],
dts: resolve(rootDir, 'src/components.d.ts'),
}),
],
resolve: {
alias: {
'@': resolve(rootDir, 'src'),
'@cslab-dcs/core': resolve(__dirname, './src'),
},
},
css: {
preprocessorOptions: {
scss: {
// 自定义 element-plus 主题或全局变量
additionalData: `@use "@/assets/styles/variables.scss" as *;`,
api: 'modern-compiler', // sass-loader v15+ requirement
},
},
},
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "@cslab-dcs/schema",
"type": "module",
"version": "1.0.0",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./*": {
"types": "./src/*.ts",
"import": "./src/*.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"zod": "^3.24.1"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,99 @@
/**
* DCS 配置相关类型和验证器
*/
import { z } from 'zod'
/**
* DCS 配置项类型
*/
export type DCSValueType = 'string' | 'number' | 'boolean' | 'array' | 'object'
/**
* DCS 配置项
*/
export interface DCSConfigItem {
key: string
value: unknown
type: DCSValueType
description?: string
required?: boolean
children?: DCSConfigItem[]
}
/**
* DCS 配置文件
*/
export interface DCSConfigFile {
name: string
path: string
version: string
description?: string
items: DCSConfigItem[]
createdAt: string
updatedAt: string
}
/**
* DCS 项目
*/
export interface DCSProject {
id: string
name: string
description?: string
configs: DCSConfigFile[]
createdAt: string
updatedAt: string
}
/**
* DCS 配置项验证器
*/
export const dcsConfigItemSchema: z.ZodType<DCSConfigItem> = z.lazy(() =>
z.object({
key: z.string().min(1, '配置键不能为空'),
value: z.unknown(),
type: z.enum(['string', 'number', 'boolean', 'array', 'object']),
description: z.string().optional(),
required: z.boolean().optional(),
children: z.array(dcsConfigItemSchema).optional(),
}),
)
/**
* DCS 配置文件验证器
*/
export const dcsConfigFileSchema = z.object({
name: z.string().min(1, '文件名不能为空'),
path: z.string().min(1, '文件路径不能为空'),
version: z.string().regex(/^\d+\.\d+\.\d+$/, '版本号格式应为 x.x.x'),
description: z.string().optional(),
items: z.array(dcsConfigItemSchema),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
/**
* DCS 项目验证器
*/
export const dcsProjectSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, '项目名不能为空'),
description: z.string().optional(),
configs: z.array(dcsConfigFileSchema),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
/**
* 验证 DCS 配置
*/
export function validateDCSConfig(data: unknown): DCSConfigFile {
return dcsConfigFileSchema.parse(data)
}
/**
* 安全验证 DCS 配置
*/
export function safeParseDCSConfig(data: unknown) {
return dcsConfigFileSchema.safeParse(data)
}

View File

@@ -0,0 +1,8 @@
/**
* @cslab-dcs/schema
* 数据模型和验证器统一导出
*/
export * from './dcs'
export * from './types'
export * from './validators'

View File

@@ -0,0 +1,82 @@
/**
* 通用类型定义
*/
/**
* API 响应结构
*/
export interface ApiResponse<T = unknown> {
code: number
message: string
data: T
success: boolean
}
/**
* 分页请求参数
*/
export interface PaginationParams {
page: number
pageSize: number
}
/**
* 分页响应结构
*/
export interface PaginatedData<T> {
list: T[]
total: number
page: number
pageSize: number
totalPages: number
}
/**
* 树形结构节点
*/
export interface TreeNode<T = unknown> {
id: string
label: string
children?: TreeNode<T>[]
data?: T
disabled?: boolean
isLeaf?: boolean
}
/**
* 键值对
*/
export interface KeyValue<T = string> {
key: string
value: T
}
/**
* 选项结构
*/
export interface SelectOption<T = string | number> {
label: string
value: T
disabled?: boolean
}
/**
* 用户信息
*/
export interface UserInfo {
id: string
username: string
nickname?: string
avatar?: string
email?: string
roles?: string[]
}
/**
* 应用状态
*/
export interface AppState {
theme: 'light' | 'dark' | 'system'
language: string
sidebarCollapsed: boolean
}

View File

@@ -0,0 +1,58 @@
/**
* Zod 验证器
*/
import { z } from 'zod'
/**
* 通用验证规则
*/
export const requiredString = z.string().min(1, '此字段为必填项')
export const optionalString = z.string().optional()
export const email = z.string().email('请输入有效的邮箱地址')
export const phone = z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号')
export const url = z.string().url('请输入有效的 URL')
export const positiveNumber = z.number().positive('请输入正数')
export const nonNegativeNumber = z.number().nonnegative('请输入非负数')
/**
* API 响应验证
*/
export const apiResponseSchema = z.object({
code: z.number(),
message: z.string(),
data: z.unknown(),
success: z.boolean(),
})
/**
* 分页参数验证
*/
export const paginationSchema = z.object({
page: z.number().int().min(1).default(1),
pageSize: z.number().int().min(1).max(100).default(20),
})
/**
* 用户信息验证
*/
export const userInfoSchema = z.object({
id: z.string(),
username: z.string().min(2, '用户名至少2个字符'),
nickname: z.string().optional(),
avatar: z.string().url().optional(),
email: z.string().email().optional(),
roles: z.array(z.string()).optional(),
})
/**
* 创建可选字段的 schema
*/
export function createPartialSchema<T extends z.ZodRawShape>(schema: z.ZodObject<T>) {
return schema.partial()
}
/**
* 重新导出 zod
*/
export { z }
export type { ZodError, ZodSchema, ZodType } from 'zod'

View File

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

View File

@@ -0,0 +1,29 @@
{
"name": "@cslab-dcs/utils",
"type": "module",
"version": "1.0.0",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./*": {
"types": "./src/*.ts",
"import": "./src/*.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"dayjs": "^1.11.13",
"mitt": "^3.0.1",
"ofetch": "^1.4.1"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}

123
packages/utils/src/date.ts Normal file
View File

@@ -0,0 +1,123 @@
import type { ConfigType, Dayjs, ManipulateType, OpUnitType } from 'dayjs'
/**
* 日期时间工具
* 基于 dayjs 封装
*/
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import 'dayjs/locale/zh-cn'
// 扩展插件
dayjs.extend(relativeTime)
dayjs.extend(duration)
dayjs.extend(utc)
dayjs.extend(timezone)
// 设置中文
dayjs.locale('zh-cn')
/**
* 默认日期格式
*/
export const DATE_FORMAT = 'YYYY-MM-DD'
export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
export const TIME_FORMAT = 'HH:mm:ss'
/**
* 格式化日期
*/
export function formatDate(date?: ConfigType, format = DATE_FORMAT): string {
return dayjs(date).format(format)
}
/**
* 格式化日期时间
*/
export function formatDateTime(date?: ConfigType, format = DATETIME_FORMAT): string {
return dayjs(date).format(format)
}
/**
* 格式化时间
*/
export function formatTime(date?: ConfigType, format = TIME_FORMAT): string {
return dayjs(date).format(format)
}
/**
* 获取相对时间3分钟前
*/
export function fromNow(date: ConfigType): string {
return dayjs(date).fromNow()
}
/**
* 获取到现在的相对时间
*/
export function toNow(date: ConfigType): string {
return dayjs(date).toNow()
}
/**
* 日期加减
*/
export function addTime(date: ConfigType, value: number, unit: ManipulateType): Dayjs {
return dayjs(date).add(value, unit)
}
/**
* 日期减
*/
export function subtractTime(date: ConfigType, value: number, unit: ManipulateType): Dayjs {
return dayjs(date).subtract(value, unit)
}
/**
* 获取两个日期之间的差值
*/
export function diff(date1: ConfigType, date2: ConfigType, unit: OpUnitType = 'day'): number {
return dayjs(date1).diff(dayjs(date2), unit)
}
/**
* 判断日期是否在某个日期之前
*/
export function isBefore(date1: ConfigType, date2: ConfigType): boolean {
return dayjs(date1).isBefore(dayjs(date2))
}
/**
* 判断日期是否在某个日期之后
*/
export function isAfter(date1: ConfigType, date2: ConfigType): boolean {
return dayjs(date1).isAfter(dayjs(date2))
}
/**
* 判断两个日期是否相同
*/
export function isSame(date1: ConfigType, date2: ConfigType, unit: OpUnitType = 'day'): boolean {
return dayjs(date1).isSame(dayjs(date2), unit)
}
/**
* 获取当前时间戳
*/
export function now(): number {
return dayjs().valueOf()
}
/**
* 解析日期
*/
export function parseDate(date: ConfigType): Dayjs {
return dayjs(date)
}
/**
* 导出 dayjs 实例供高级用法
*/
export { dayjs }

View File

@@ -0,0 +1,84 @@
import type { Emitter, EventType, Handler } from 'mitt'
/**
* 事件总线
* 基于 mitt 封装
*/
import mitt from 'mitt'
// 定义事件类型
export interface AppEvents {
// 通用事件
'app:ready': void
'app:error': Error
// 主题切换
'theme:change': 'light' | 'dark'
// 用户事件
'user:login': { userId: string, token: string }
'user:logout': void
// 文件事件
'file:open': { path: string }
'file:save': { path: string, content: string }
'file:close': { path: string }
// 编辑器事件
'editor:change': { content: string }
'editor:save': void
// 允许自定义事件
[key: string]: unknown
}
// 创建事件总线实例
const emitter: Emitter<AppEvents> = mitt<AppEvents>()
/**
* 发送事件
*/
export function emit<K extends keyof AppEvents>(type: K, event: AppEvents[K]): void {
emitter.emit(type, event)
}
/**
* 监听事件
*/
export function on<K extends keyof AppEvents>(type: K, handler: Handler<AppEvents[K]>): void {
emitter.on(type, handler as Handler<AppEvents[keyof AppEvents]>)
}
/**
* 取消监听事件
*/
export function off<K extends keyof AppEvents>(type: K, handler?: Handler<AppEvents[K]>): void {
emitter.off(type, handler as Handler<AppEvents[keyof AppEvents]>)
}
/**
* 监听所有事件
*/
export function onAll(handler: (type: EventType, event: unknown) => void): void {
emitter.on('*', handler)
}
/**
* 清除所有事件监听
*/
export function clear(): void {
emitter.all.clear()
}
/**
* 事件总线实例
*/
export const eventBus = {
emit,
on,
off,
onAll,
clear,
emitter,
}
export default eventBus

View File

@@ -0,0 +1,195 @@
/**
* 通用辅助函数
*/
/**
* 延迟执行
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* 防抖函数
*/
export function debounce<T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number,
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null
return function (this: unknown, ...args: Parameters<T>) {
if (timer)
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
/**
* 节流函数
*/
export function throttle<T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number,
): (...args: Parameters<T>) => void {
let lastTime = 0
return function (this: unknown, ...args: Parameters<T>) {
const now = Date.now()
if (now - lastTime >= delay) {
lastTime = now
fn.apply(this, args)
}
}
}
/**
* 深拷贝
*/
export function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj
}
if (obj instanceof Date) {
return new Date(obj.getTime()) as T
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item)) as T
}
if (obj instanceof Object) {
const copy = {} as T
Object.keys(obj).forEach((key) => {
;(copy as Record<string, unknown>)[key] = deepClone((obj as Record<string, unknown>)[key])
})
return copy
}
return obj
}
/**
* 生成唯一ID
*/
export function generateId(prefix = ''): string {
const timestamp = Date.now().toString(36)
const randomStr = Math.random().toString(36).substring(2, 10)
return `${prefix}${timestamp}${randomStr}`
}
/**
* 生成UUID
*/
export function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
/**
* 判断是否为空值null、undefined、空字符串、空数组、空对象
*/
export function isEmpty(value: unknown): boolean {
if (value === null || value === undefined)
return true
if (typeof value === 'string' && value.trim() === '')
return true
if (Array.isArray(value) && value.length === 0)
return true
if (typeof value === 'object' && Object.keys(value).length === 0)
return true
return false
}
/**
* 安全解析 JSON
*/
export function safeJsonParse<T>(str: string, defaultValue: T): T {
try {
return JSON.parse(str) as T
}
catch {
return defaultValue
}
}
/**
* 格式化文件大小
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0)
return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${units[i]}`
}
/**
* 驼峰转短横线
*/
export function camelToKebab(str: string): string {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
/**
* 短横线转驼峰
*/
export function kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
}
/**
* 首字母大写
*/
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* 复制到剪贴板
*/
export async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text)
return true
}
catch {
// 降级方案
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
const result = document.execCommand('copy')
document.body.removeChild(textarea)
return result
}
}
/**
* 下载文件
*/
export function downloadFile(content: string | Blob, filename: string, mimeType = 'text/plain'): void {
const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}

101
packages/utils/src/http.ts Normal file
View File

@@ -0,0 +1,101 @@
import type { FetchOptions } from 'ofetch'
/**
* HTTP 请求工具
* 基于 ofetch 封装
*/
import { ofetch } from 'ofetch'
export interface RequestConfig extends FetchOptions {
baseURL?: string
showError?: boolean
}
const defaultConfig: RequestConfig = {
baseURL: '',
timeout: 30000,
showError: true,
}
/**
* 创建请求实例
*/
export function createRequest(config: RequestConfig = {}) {
const mergedConfig = { ...defaultConfig, ...config }
const request = ofetch.create({
baseURL: mergedConfig.baseURL,
timeout: mergedConfig.timeout,
onRequest({ options }) {
// 请求拦截器 - 添加通用 headers
options.headers = {
...options.headers,
'Content-Type': 'application/json',
}
},
onRequestError({ error }) {
// 请求错误处理
console.error('[Request Error]', error)
},
onResponse({ response }) {
// 响应拦截器
return response._data
},
onResponseError({ response }) {
// 响应错误处理
const status = response.status
const message = response._data?.message || `HTTP Error: ${status}`
console.error('[Response Error]', message)
throw new Error(message)
},
})
return request
}
/**
* 默认请求实例
*/
export const http = createRequest()
/**
* GET 请求
*/
export async function get<T>(url: string, params?: Record<string, unknown>, config?: RequestConfig): Promise<T> {
return http<T>(url, {
method: 'GET',
query: params,
...config,
})
}
/**
* POST 请求
*/
export async function post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
return http<T>(url, {
method: 'POST',
body: data,
...config,
})
}
/**
* PUT 请求
*/
export async function put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
return http<T>(url, {
method: 'PUT',
body: data,
...config,
})
}
/**
* DELETE 请求
*/
export async function del<T>(url: string, config?: RequestConfig): Promise<T> {
return http<T>(url, {
method: 'DELETE',
...config,
})
}

View File

@@ -0,0 +1,10 @@
/**
* @cslab-dcs/utils
* 工具函数统一导出
*/
export * from './date'
export * from './event'
export * from './helpers'
export * from './http'
export * from './storage'

View File

@@ -0,0 +1,136 @@
/**
* 本地存储工具
* 抽象 localStorage 操作
*/
const PREFIX = 'cslab-dcs:'
/**
* 存储选项
*/
export interface StorageOptions {
prefix?: string
expire?: number // 过期时间(毫秒)
}
/**
* 存储数据结构
*/
interface StorageData<T> {
value: T
expire?: number
createTime: number
}
/**
* 设置存储
*/
export function setStorage<T>(key: string, value: T, options: StorageOptions = {}): void {
const { prefix = PREFIX, expire } = options
const data: StorageData<T> = {
value,
createTime: Date.now(),
}
if (expire) {
data.expire = Date.now() + expire
}
try {
localStorage.setItem(prefix + key, JSON.stringify(data))
}
catch (e) {
console.error('[Storage] setStorage error:', e)
}
}
/**
* 获取存储
*/
export function getStorage<T>(key: string, options: StorageOptions = {}): T | null {
const { prefix = PREFIX } = options
try {
const raw = localStorage.getItem(prefix + key)
if (!raw)
return null
const data: StorageData<T> = JSON.parse(raw)
// 检查是否过期
if (data.expire && data.expire < Date.now()) {
removeStorage(key, options)
return null
}
return data.value
}
catch (e) {
console.error('[Storage] getStorage error:', e)
return null
}
}
/**
* 移除存储
*/
export function removeStorage(key: string, options: StorageOptions = {}): void {
const { prefix = PREFIX } = options
localStorage.removeItem(prefix + key)
}
/**
* 清空所有存储
*/
export function clearStorage(options: StorageOptions = {}): void {
const { prefix = PREFIX } = options
const keys = Object.keys(localStorage).filter(k => k.startsWith(prefix))
keys.forEach(key => localStorage.removeItem(key))
}
/**
* 获取所有存储键
*/
export function getStorageKeys(options: StorageOptions = {}): string[] {
const { prefix = PREFIX } = options
return Object.keys(localStorage)
.filter(k => k.startsWith(prefix))
.map(k => k.replace(prefix, ''))
}
/**
* Session 存储
*/
export const sessionStorage = {
set<T>(key: string, value: T): void {
try {
window.sessionStorage.setItem(PREFIX + key, JSON.stringify(value))
}
catch (e) {
console.error('[SessionStorage] set error:', e)
}
},
get<T>(key: string): T | null {
try {
const raw = window.sessionStorage.getItem(PREFIX + key)
return raw ? JSON.parse(raw) : null
}
catch (e) {
console.error('[SessionStorage] get error:', e)
return null
}
},
remove(key: string): void {
window.sessionStorage.removeItem(PREFIX + key)
},
clear(): void {
const keys = Object.keys(window.sessionStorage).filter(k => k.startsWith(PREFIX))
keys.forEach(key => window.sessionStorage.removeItem(key))
},
}

View File

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

8085
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

7
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,7 @@
shellEmulator: true
trustPolicy: no-downgrade
packages:
- 'apps/*'
- 'packages/*'

37
tsconfig.base.json Normal file
View File

@@ -0,0 +1,37 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"jsxImportSource": "vue",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "bundler",
"paths": {
"@cslab-dcs/core": ["./packages/core/src"],
"@cslab-dcs/core/*": ["./packages/core/src/*"],
"@cslab-dcs/bridge": ["./packages/bridge/src"],
"@cslab-dcs/bridge/*": ["./packages/bridge/src/*"],
"@cslab-dcs/schema": ["./packages/schema/src"],
"@cslab-dcs/schema/*": ["./packages/schema/src/*"],
"@cslab-dcs/utils": ["./packages/utils/src"],
"@cslab-dcs/utils/*": ["./packages/utils/src/*"]
},
"resolveJsonModule": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"declaration": true,
"declarationMap": true,
"noEmit": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"skipLibCheck": true
},
"exclude": ["node_modules", "dist", "out"]
}