Merge branch 'dev'
@@ -1,9 +1,20 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
# 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
|
||||
|
||||
57
.gitignore
vendored
@@ -1,7 +1,58 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
dist-web
|
||||
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
|
||||
.eslintcache
|
||||
*.log*
|
||||
*.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
|
||||
|
||||
4
.npmrc
@@ -1,3 +1,3 @@
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
|
||||
112
README.md
@@ -1,34 +1,114 @@
|
||||
# cslab-dcs-web
|
||||
# DCS Editor (cslab-dcs)
|
||||
|
||||
An Electron application with Vue and TypeScript
|
||||
DCS Editor 是一个跨平台的配置文件编辑器,旨在 Web、Windows 和 macOS 环境下提供一致的体验。本项目采用 **Monorepo** 架构,最大化地实现了跨平台代码复用。
|
||||
|
||||
## Recommended IDE Setup
|
||||
## 🏗 项目架构
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||
本项目使用 `pnpm` workspaces 进行管理:
|
||||
|
||||
## Project Setup
|
||||
### `apps/` (宿主应用)
|
||||
|
||||
### Install
|
||||
- **`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
|
||||
# 安装所有包的依赖
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Development
|
||||
### 开发模式
|
||||
|
||||
启动特定平台的开发环境:
|
||||
|
||||
```bash
|
||||
$ pnpm dev
|
||||
# Web 浏览器开发
|
||||
pnpm dev:web
|
||||
|
||||
# Electron 应用开发
|
||||
pnpm dev:electron
|
||||
|
||||
# Tauri 应用开发
|
||||
pnpm dev:tauri
|
||||
```
|
||||
|
||||
### Build
|
||||
### 构建 (Build)
|
||||
|
||||
构建生产环境版本:
|
||||
|
||||
```bash
|
||||
# For windows
|
||||
$ pnpm build:win
|
||||
# 构建 Web 版本 (输出: apps/web/dist)
|
||||
pnpm build:web
|
||||
|
||||
# For macOS
|
||||
$ pnpm build:mac
|
||||
# 构建 Windows Electron 版本 (输出: apps/electron/dist)
|
||||
pnpm build:electron:win
|
||||
|
||||
# For Linux
|
||||
$ pnpm build:linux
|
||||
# 构建 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` 进行构建优化。
|
||||
|
||||
319
apps/electron/.eslintrc-auto-import.json
Normal 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
|
||||
}
|
||||
}
|
||||
27
apps/electron/electron-builder.yml
Normal 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
|
||||
27
apps/electron/electron.vite.config.ts
Normal 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'),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
36
apps/electron/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
apps/electron/resources/icon.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
150
apps/electron/src/main/index.ts
Normal 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()
|
||||
}
|
||||
})
|
||||
22
apps/electron/src/preload/index.ts
Normal 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
|
||||
}
|
||||
12
apps/electron/src/renderer/index.html
Normal 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>
|
||||
5
apps/electron/src/renderer/src/main.ts
Normal 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/electron/tsconfig.json
Normal 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"]
|
||||
}
|
||||
319
apps/tauri/.eslintrc-auto-import.json
Normal 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
@@ -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
@@ -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
30
apps/tauri/src-tauri/Cargo.toml
Normal 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" ]
|
||||
3
apps/tauri/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
12
apps/tauri/src-tauri/capabilities/default.json
Normal 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"
|
||||
]
|
||||
}
|
||||
BIN
apps/tauri/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
apps/tauri/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
apps/tauri/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
apps/tauri/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
apps/tauri/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
apps/tauri/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
apps/tauri/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
apps/tauri/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
apps/tauri/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
apps/tauri/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
apps/tauri/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
apps/tauri/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
apps/tauri/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
apps/tauri/src-tauri/icons/icon.icns
Normal file
BIN
apps/tauri/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
apps/tauri/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
9
apps/tauri/src-tauri/src/lib.rs
Normal 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");
|
||||
}
|
||||
6
apps/tauri/src-tauri/src/main.rs
Normal 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()
|
||||
}
|
||||
39
apps/tauri/src-tauri/tauri.conf.json
Normal 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
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
})
|
||||
319
apps/web/.eslintrc-auto-import.json
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default antfu()
|
||||
69
package.json
@@ -1,54 +1,31 @@
|
||||
{
|
||||
"name": "cslab-dcs-web",
|
||||
"name": "cslab-dcs",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"description": "CSLAB DCS 编辑器",
|
||||
"author": "CSLAB FE",
|
||||
"main": "./out/main/index.js",
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev",
|
||||
"dev:web": "vite --config vite.config.web.ts",
|
||||
"build:web": "vite build --config vite.config.web.ts",
|
||||
"start": "electron-vite preview",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"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",
|
||||
"lint": "eslint --cache .",
|
||||
"format": "eslint --cache . --fix",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"description": "DCS Editor - A cross-platform configuration editor",
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"element-plus": "^2.13.1",
|
||||
"vue-router": "^4.6.4"
|
||||
"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",
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@types/node": "^22.19.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.3",
|
||||
"electron": "^39.2.6",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-vite": "^5.0.0",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^7.2.6",
|
||||
"vue": "^3.5.25",
|
||||
"vue-tsc": "^3.1.6"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"esbuild"
|
||||
]
|
||||
"@types/node": "^22.19.7",
|
||||
"eslint": "^9.39.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
31
packages/bridge/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
89
packages/bridge/src/adapters/electron.ts
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
103
packages/bridge/src/adapters/tauri.ts
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
95
packages/bridge/src/adapters/web.ts
Normal 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')
|
||||
},
|
||||
}
|
||||
}
|
||||
23
packages/bridge/src/index.ts
Normal 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()
|
||||
1
packages/bridge/src/tauri.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './adapters/tauri'
|
||||
77
packages/bridge/src/types.ts
Normal 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
|
||||
}
|
||||
11
packages/bridge/tsconfig.json
Normal 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
@@ -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 类型
|
||||
}
|
||||
46
packages/core/package.json
Normal 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
@@ -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>
|
||||
26
packages/core/src/assets/styles/index.scss
Normal 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;
|
||||
}
|
||||
18
packages/core/src/assets/styles/variables.scss
Normal 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;
|
||||
38
packages/core/src/index.ts
Normal 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'
|
||||
19
packages/core/src/router/index.ts
Normal 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
|
||||
17
packages/core/src/stores/app.ts
Normal 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,
|
||||
})
|
||||
8
packages/core/src/stores/index.ts
Normal 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
|
||||
95
packages/core/src/views/Home.vue
Normal 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>
|
||||
16
packages/core/tsconfig.json
Normal 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"]
|
||||
}
|
||||
43
packages/core/vite.shared.ts
Normal 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
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
27
packages/schema/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
99
packages/schema/src/dcs.ts
Normal 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)
|
||||
}
|
||||
8
packages/schema/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @cslab-dcs/schema
|
||||
* 数据模型和验证器统一导出
|
||||
*/
|
||||
|
||||
export * from './dcs'
|
||||
export * from './types'
|
||||
export * from './validators'
|
||||
82
packages/schema/src/types.ts
Normal 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
|
||||
}
|
||||
58
packages/schema/src/validators.ts
Normal 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'
|
||||
10
packages/schema/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
29
packages/utils/package.json
Normal 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
@@ -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 }
|
||||
84
packages/utils/src/event.ts
Normal 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
|
||||
195
packages/utils/src/helpers.ts
Normal 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
@@ -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,
|
||||
})
|
||||
}
|
||||
10
packages/utils/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @cslab-dcs/utils
|
||||
* 工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './date'
|
||||
export * from './event'
|
||||
export * from './helpers'
|
||||
export * from './http'
|
||||
export * from './storage'
|
||||
136
packages/utils/src/storage.ts
Normal 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))
|
||||
},
|
||||
}
|
||||
10
packages/utils/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
2499
pnpm-lock.yaml
generated
7
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
shellEmulator: true
|
||||
|
||||
trustPolicy: no-downgrade
|
||||
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
37
tsconfig.base.json
Normal 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"]
|
||||
}
|
||||