chore: starter
This commit is contained in:
31
packages/bridge/package.json
Normal file
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
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
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
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
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
1
packages/bridge/src/tauri.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './adapters/tauri'
|
||||
77
packages/bridge/src/types.ts
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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"]
|
||||
}
|
||||
Reference in New Issue
Block a user