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

View File

@@ -0,0 +1,32 @@
{
"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.10.1",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.5",
"@tauri-apps/plugin-os": "^2.3.2"
},
"devDependencies": {
"electron": "^33.2.1",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,70 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
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
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return (await window.electron.ipcRenderer.invoke('app:version')) as string
},
async getPlatformInfo() {
return (await window.electron.ipcRenderer.invoke('app:info')) as any
},
async openExternal(url: string): Promise<void> {
await window.electron.ipcRenderer.invoke('shell:open', url)
},
}
}

View File

@@ -0,0 +1,86 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
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, version as osVersion, 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',
})
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return await getVersion()
},
async getPlatformInfo() {
return {
name: 'tauri',
version: await getTauriVersion(),
os: await platform(),
osVersion: await osVersion(),
arch: await arch(),
}
},
async openExternal(url: string): Promise<void> {
await openUrl(url)
},
}
}

View File

@@ -0,0 +1,96 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
ISystemService,
MessageOptions,
PlatformType,
SaveDialogOptions,
} from '../types'
const MAC_OS_VERSION_RE = /Mac OS X ([\d_]+)/
const WINDOWS_VERSION_RE = /Windows NT ([\d.]+)/
const UNDERSCORE_RE = /_/g
function parseOsVersion(userAgent: string): string | undefined {
const macMatch = userAgent.match(MAC_OS_VERSION_RE)
if (macMatch)
return macMatch[1].replace(UNDERSCORE_RE, '.')
const winMatch = userAgent.match(WINDOWS_VERSION_RE)
if (winMatch)
return winMatch[1]
return undefined
}
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}`)
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return '1.0.0'
},
async getPlatformInfo() {
const osVersion = parseOsVersion(navigator.userAgent)
return {
name: 'web',
version: navigator.userAgent,
os: navigator.platform,
osVersion,
arch: 'unknown',
}
},
async openExternal(url: string): Promise<void> {
window.open(url, '_blank')
},
}
}

View File

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

View File

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

View File

@@ -0,0 +1,70 @@
/**
* 平台桥接类型定义
*/
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
osVersion?: 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 ISystemService {
getAppVersion: () => Promise<string>
getPlatformInfo: () => Promise<PlatformInfo>
openExternal: (url: string) => Promise<void>
}
export interface IPlatformBridge {
readonly platform: PlatformType
file: IFileService
dialog: IDialogService
system: ISystemService
}

View File

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