chore: starter
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user