重构:Wails v3 迁移 + 前端目录规范化 + Sidebar滚动优化
- web/ → frontend/ 目录重命名(Wails v3 标准结构) - main.go: Middleware 修复 custom.js 404 + DevTools 延迟启动 - Sidebar: 收藏夹内部独立滚动 + 帮助区块固定底部 - useFavorites.ts: longPressTimer const→let 修复 TypeError - App.vue: Arco Tabs padding-top 覆盖 - build: config.yml / Taskfile.yml 对齐官方模板,devtools build tag - 新增 v3 bindings、vite.config.js、跨平台构建配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
199
frontend/src/api/connection-manager.ts
Normal file
199
frontend/src/api/connection-manager.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 连接管理器 — 管理本地/远程传输层切换
|
||||
*/
|
||||
|
||||
import type { FsTransport } from './transport'
|
||||
import { WailsTransport } from './wails-transport'
|
||||
import { HttpTransport } from './http-transport'
|
||||
|
||||
export type ConnectionType = 'local' | 'remote'
|
||||
|
||||
export interface ConnectionProfile {
|
||||
id: string
|
||||
name: string
|
||||
host: string
|
||||
port: number
|
||||
token: string
|
||||
type: ConnectionType
|
||||
lastConnected?: number
|
||||
}
|
||||
|
||||
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error'
|
||||
|
||||
const PROFILES_KEY = 'fs_connection_profiles'
|
||||
const ACTIVE_KEY = 'fs_active_connection'
|
||||
|
||||
class ConnectionManagerImpl {
|
||||
private _transport: FsTransport | null = null
|
||||
private _profiles: ConnectionProfile[] = []
|
||||
private _activeId: string | null = null
|
||||
private _state: ConnectionState = 'disconnected'
|
||||
private _stateChangeCallbacks: ((state: ConnectionState) => void)[] = []
|
||||
private _connectSeq = 0
|
||||
|
||||
constructor() {
|
||||
this.loadProfiles()
|
||||
this.initDefaultLocal()
|
||||
}
|
||||
|
||||
private initDefaultLocal() {
|
||||
const localProfile: ConnectionProfile = {
|
||||
id: 'local-default',
|
||||
name: '本地',
|
||||
host: '',
|
||||
port: 0,
|
||||
token: '',
|
||||
type: 'local',
|
||||
}
|
||||
if (!this._profiles.find(p => p.id === localProfile.id)) {
|
||||
this._profiles.unshift(localProfile)
|
||||
}
|
||||
// 默认连接本地
|
||||
if (!this._activeId) {
|
||||
this._activeId = localProfile.id
|
||||
}
|
||||
this.applyActive()
|
||||
}
|
||||
|
||||
private loadProfiles() {
|
||||
try {
|
||||
const raw = localStorage.getItem(PROFILES_KEY)
|
||||
if (raw) this._profiles = JSON.parse(raw)
|
||||
this._activeId = localStorage.getItem(ACTIVE_KEY)
|
||||
} catch { /* 首次使用 */ }
|
||||
}
|
||||
|
||||
private saveProfiles() {
|
||||
localStorage.setItem(PROFILES_KEY, JSON.stringify(this._profiles))
|
||||
if (this._activeId) {
|
||||
localStorage.setItem(ACTIVE_KEY, this._activeId)
|
||||
}
|
||||
}
|
||||
|
||||
private setState(state: ConnectionState) {
|
||||
this._state = state
|
||||
this.notifyChange()
|
||||
}
|
||||
|
||||
private notifyChange() {
|
||||
this._stateChangeCallbacks.forEach(cb => cb(this._state))
|
||||
}
|
||||
|
||||
onStateChange(cb: (state: ConnectionState) => void) {
|
||||
this._stateChangeCallbacks.push(cb)
|
||||
}
|
||||
|
||||
get state(): ConnectionState {
|
||||
return this._state
|
||||
}
|
||||
|
||||
get profiles(): ConnectionProfile[] {
|
||||
return [...this._profiles]
|
||||
}
|
||||
|
||||
get activeProfile(): ConnectionProfile | null {
|
||||
return this._profiles.find(p => p.id === this._activeId) ?? null
|
||||
}
|
||||
|
||||
getTransport(): FsTransport {
|
||||
if (!this._transport) {
|
||||
this.applyActive()
|
||||
}
|
||||
return this._transport!
|
||||
}
|
||||
|
||||
getFileServerBaseURL(): string {
|
||||
if (this._transport instanceof HttpTransport) {
|
||||
const profile = this.activeProfile
|
||||
if (!profile) return ''
|
||||
const scheme = profile.port === 443 ? 'https' : 'http'
|
||||
const port = (profile.port === 80 || profile.port === 443) ? '' : `:${profile.port}`
|
||||
return `${scheme}://${profile.host}${port}`
|
||||
}
|
||||
// Wails 模式返回空字符串,让 useFilePreview 走原有逻辑
|
||||
return ''
|
||||
}
|
||||
|
||||
isRemote(): boolean {
|
||||
return this.activeProfile?.type === 'remote'
|
||||
}
|
||||
|
||||
connect(profileId: string): void {
|
||||
const profile = this._profiles.find(p => p.id === profileId)
|
||||
if (!profile) return
|
||||
|
||||
this._activeId = profileId
|
||||
this.saveProfiles()
|
||||
this.applyActive()
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this._activeId = 'local-default'
|
||||
this.saveProfiles()
|
||||
this.applyActive()
|
||||
}
|
||||
|
||||
addProfile(profile: Omit<ConnectionProfile, 'id'>): ConnectionProfile {
|
||||
const newProfile: ConnectionProfile = {
|
||||
...profile,
|
||||
id: crypto.randomUUID(),
|
||||
}
|
||||
this._profiles.push(newProfile)
|
||||
this.saveProfiles()
|
||||
this.notifyChange()
|
||||
return newProfile
|
||||
}
|
||||
|
||||
updateProfile(id: string, updates: Partial<ConnectionProfile>): void {
|
||||
const idx = this._profiles.findIndex(p => p.id === id)
|
||||
if (idx >= 0) {
|
||||
this._profiles[idx] = { ...this._profiles[idx], ...updates }
|
||||
this.saveProfiles()
|
||||
// 仅当连接参数变化时重新应用(避免 lastConnected 等元数据更新触发死循环)
|
||||
const REAPPLY_KEYS = ['host', 'port', 'token'] as const
|
||||
const needsReapply = REAPPLY_KEYS.some(k => k in updates)
|
||||
if (needsReapply && id === this._activeId) {
|
||||
this.applyActive()
|
||||
}
|
||||
this.notifyChange()
|
||||
}
|
||||
}
|
||||
|
||||
removeProfile(id: string): void {
|
||||
if (id === 'local-default') return // 不允许删除本地配置
|
||||
this._profiles = this._profiles.filter(p => p.id !== id)
|
||||
if (this._activeId === id) {
|
||||
this._activeId = 'local-default'
|
||||
}
|
||||
this.saveProfiles()
|
||||
this.applyActive()
|
||||
this.notifyChange()
|
||||
}
|
||||
|
||||
private applyActive() {
|
||||
const profile = this.activeProfile
|
||||
const seq = ++this._connectSeq
|
||||
if (!profile || profile.type === 'local') {
|
||||
this._transport = new WailsTransport()
|
||||
this.setState('connected')
|
||||
} else {
|
||||
this.setState('connecting')
|
||||
try {
|
||||
this._transport = new HttpTransport(profile.host, profile.port, profile.token)
|
||||
// 快速连通性检查(用轻量 ping 代替 getCommonPaths)
|
||||
this._transport.getFileInfo('/').then(() => {
|
||||
if (seq !== this._connectSeq) return // 已被后续连接覆盖
|
||||
this.setState('connected')
|
||||
this.updateProfile(profile.id!, { lastConnected: Date.now() })
|
||||
}).catch(() => {
|
||||
if (seq !== this._connectSeq) return
|
||||
this.setState('error')
|
||||
})
|
||||
} catch {
|
||||
this.setState('error')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const connectionManager = new ConnectionManagerImpl()
|
||||
Reference in New Issue
Block a user