Private
Public Access
1
0

新增: 云OSS存储集成(七牛云+阿里云)+多桶导航+GBK编码自动转换

This commit is contained in:
2026-05-05 03:18:47 +08:00
parent eb5b85e007
commit b4f4b4627d
34 changed files with 5225 additions and 48 deletions

View File

@@ -302,6 +302,115 @@ export function OpenPath(path: string): $CancellablePromise<void> {
return $Call.ByID(1591734570, path);
}
export function OssConnect(req: $models.OssConnectRequest): $CancellablePromise<string> {
return $Call.ByID(3667022538, req);
}
/**
* OssCreateDir OSS 创建目录
*/
export function OssCreateDir(connID: string, dirPath: string): $CancellablePromise<filesystem$0.FileOperationResult | null> {
return $Call.ByID(605668951, connID, dirPath).then(($result: any) => {
return $$createType2($result);
});
}
/**
* OssCreateFile OSS 创建文件
*/
export function OssCreateFile(connID: string, filePath: string): $CancellablePromise<filesystem$0.FileOperationResult | null> {
return $Call.ByID(4148593430, connID, filePath).then(($result: any) => {
return $$createType2($result);
});
}
/**
* OssDeletePath OSS 删除
*/
export function OssDeletePath(connID: string, key: string): $CancellablePromise<filesystem$0.FileOperationResult | null> {
return $Call.ByID(4285234744, connID, key).then(($result: any) => {
return $$createType2($result);
});
}
/**
* OssDisconnect 断开 OSS 连接
*/
export function OssDisconnect(connID: string): $CancellablePromise<void> {
return $Call.ByID(3427288622, connID);
}
/**
* OssDownloadToTemp OSS 下载到临时文件
*/
export function OssDownloadToTemp(connID: string, key: string): $CancellablePromise<string> {
return $Call.ByID(370656471, connID, key);
}
/**
* OssGetCommonPaths OSS 获取常用路径
*/
export function OssGetCommonPaths(connID: string): $CancellablePromise<{ [_ in string]?: string }> {
return $Call.ByID(3525024115, connID).then(($result: any) => {
return $$createType4($result);
});
}
/**
* OssGetFileInfo OSS 获取文件信息
*/
export function OssGetFileInfo(connID: string, key: string): $CancellablePromise<{ [_ in string]?: any }> {
return $Call.ByID(852430614, connID, key).then(($result: any) => {
return $$createType0($result);
});
}
/**
* OssGetSignedURL OSS 获取预签名 URL
*/
export function OssGetSignedURL(connID: string, key: string): $CancellablePromise<string> {
return $Call.ByID(1344953417, connID, key);
}
/**
* OssListDir OSS 列出目录
*/
export function OssListDir(connID: string, prefix: string): $CancellablePromise<{ [_ in string]?: any }[]> {
return $Call.ByID(3013212019, connID, prefix).then(($result: any) => {
return $$createType3($result);
});
}
/**
* OssReadFile OSS 读取文件
*/
export function OssReadFile(connID: string, key: string): $CancellablePromise<string> {
return $Call.ByID(1629576606, connID, key);
}
/**
* OssRenamePath OSS 重命名
*/
export function OssRenamePath(req: $models.OssRenamePathRequest): $CancellablePromise<filesystem$0.FileOperationResult | null> {
return $Call.ByID(4218061693, req).then(($result: any) => {
return $$createType2($result);
});
}
/**
* OssWriteBase64File OSS 写入 base64 编码文件
*/
export function OssWriteBase64File(connID: string, key: string, base64Content: string): $CancellablePromise<void> {
return $Call.ByID(1772140162, connID, key, base64Content);
}
/**
* OssWriteFile OSS 写入文件
*/
export function OssWriteFile(connID: string, key: string, content: string): $CancellablePromise<void> {
return $Call.ByID(39773277, connID, key, content);
}
/**
* ReadFile 读取文件
*/

View File

@@ -7,6 +7,8 @@ export {
};
export {
OssConnectRequest,
OssRenamePathRequest,
RenamePathRequest,
SaveAppConfigRequest,
SaveBase64FileRequest,

View File

@@ -9,6 +9,71 @@ import { Create as $Create } from "@wailsio/runtime";
// @ts-ignore: Unused imports
import * as api$0 from "./internal/api/models.js";
export class OssConnectRequest {
"provider": string;
"access_key": string;
"secret_key": string;
"endpoint": string;
/** Creates a new OssConnectRequest instance. */
constructor($$source: Partial<OssConnectRequest> = {}) {
if (!("provider" in $$source)) {
this["provider"] = "";
}
if (!("access_key" in $$source)) {
this["access_key"] = "";
}
if (!("secret_key" in $$source)) {
this["secret_key"] = "";
}
if (!("endpoint" in $$source)) {
this["endpoint"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new OssConnectRequest instance from a string or object.
*/
static createFrom($$source: any = {}): OssConnectRequest {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new OssConnectRequest($$parsedSource as Partial<OssConnectRequest>);
}
}
/**
* OssRenamePathRequest OSS 重命名请求
*/
export class OssRenamePathRequest {
"conn_id": string;
"old_path": string;
"new_path": string;
/** Creates a new OssRenamePathRequest instance. */
constructor($$source: Partial<OssRenamePathRequest> = {}) {
if (!("conn_id" in $$source)) {
this["conn_id"] = "";
}
if (!("old_path" in $$source)) {
this["old_path"] = "";
}
if (!("new_path" in $$source)) {
this["new_path"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new OssRenamePathRequest instance from a string or object.
*/
static createFrom($$source: any = {}): OssRenamePathRequest {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new OssRenamePathRequest($$parsedSource as Partial<OssRenamePathRequest>);
}
}
/**
* RenamePathRequest 重命名文件或目录请求结构体
*/
@@ -119,6 +184,11 @@ export class SaveProfileRequest {
"key_path": string;
"type": string;
"token": string;
"access_key": string;
"secret_key": string;
"bucket": string;
"region": string;
"endpoint": string;
"last_connected": number | null;
/** Creates a new SaveProfileRequest instance. */
@@ -150,6 +220,21 @@ export class SaveProfileRequest {
if (!("token" in $$source)) {
this["token"] = "";
}
if (!("access_key" in $$source)) {
this["access_key"] = "";
}
if (!("secret_key" in $$source)) {
this["secret_key"] = "";
}
if (!("bucket" in $$source)) {
this["bucket"] = "";
}
if (!("region" in $$source)) {
this["region"] = "";
}
if (!("endpoint" in $$source)) {
this["endpoint"] = "";
}
if (!("last_connected" in $$source)) {
this["last_connected"] = null;
}

View File

@@ -7,13 +7,14 @@ import type { FsTransport } from './transport'
import { WailsTransport } from './wails-transport'
import { HttpTransport } from './http-transport'
import { SftpTransport } from './sftp-transport'
import { OssTransport } from './oss-transport'
import { getFileServerBaseURL } from './file-server'
import {
LoadConnectionProfiles, SaveConnectionProfile, DeleteConnectionProfile,
SftpGetSystemInfo, GetLocalSystemInfo,
} from '@bindings/u-desk/app'
export type ConnectionType = 'local' | 'remote' | 'sftp'
export type ConnectionType = 'local' | 'remote' | 'sftp' | 'qiniu' | 'aliyun'
export interface ConnectionProfile {
id: string | number
@@ -25,6 +26,11 @@ export interface ConnectionProfile {
username?: string
password?: string
keyPath?: string
accessKey?: string
secretKey?: string
bucket?: string
region?: string
endpoint?: string
lastConnected?: number
}
@@ -124,6 +130,11 @@ class ConnectionManagerImpl {
keyPath: profile.keyPath || '',
type: profile.type,
token: profile.token || '',
access_key: profile.accessKey || '',
secret_key: profile.secretKey || '',
bucket: profile.bucket || '',
region: profile.region || '',
endpoint: profile.endpoint || '',
lastConnected: profile.lastConnected ? Math.floor(profile.lastConnected / 1000) : undefined,
})
}
@@ -189,7 +200,7 @@ class ConnectionManagerImpl {
isRemote(): boolean {
const t = this.activeProfile?.type
return t === 'remote' || t === 'sftp'
return t === 'remote' || t === 'sftp' || t === 'qiniu' || t === 'aliyun'
}
getSystemInfo(profileId: string): SystemInfo | undefined {
@@ -210,6 +221,11 @@ class ConnectionManagerImpl {
const data = await SftpGetSystemInfo(t.sessionId)
if (data) Object.assign(info, snakeToCamel(data))
}
} else if (profile.type === 'qiniu' || profile.type === 'aliyun') {
// OSS 无系统信息可采集
info.diskUsage = '-'
info.cpuUsage = '-'
info.memUsage = '-'
} else if (profile.type === 'remote') {
const t = this.getTransportFor(profileId)
if (t instanceof HttpTransport) {
@@ -243,16 +259,16 @@ class ConnectionManagerImpl {
const profile = this._profiles.find(p => p.id === profileId)
if (!profile) return Promise.reject(new Error(`连接配置不存在: ${profileId}`))
this._activeId = profileId
// 池中已有,直接复用
if (this._pool.has(profileId)) {
this._activeId = profileId
this.setState('connected')
return
}
// 新建连接并入池
// 新建连接并入池(成功后再设 activeId
await this.buildAndPool(profileId, profile)
this._activeId = profileId
}
/** 断开指定 profile 并从池移除 */
@@ -260,14 +276,15 @@ class ConnectionManagerImpl {
if (profileId === 'local-default') return
const t = this._pool.get(profileId)
if (t instanceof SftpTransport) { t.disconnect() }
if (t instanceof OssTransport) { t.disconnect() }
this._pool.delete(profileId)
}
/** 断开所有远程连接(保留 local */
disconnectAll(): void {
for (const [id, t] of this._pool) {
if (id !== 'local-default' && t instanceof SftpTransport) {
t.disconnect()
if (id !== 'local-default') {
if (t instanceof SftpTransport || t instanceof OssTransport) t.disconnect()
}
}
this._pool.clear()
@@ -294,7 +311,7 @@ class ConnectionManagerImpl {
this._profiles[idx] = { ...this._profiles[idx], ...updates }
await this.persistProfile(this._profiles[idx])
// 连接参数变更 → 淘汰旧连接,下次 connect 时重建
const EVICT_KEYS = ['host', 'port', 'token', 'username', 'password', 'keyPath'] as const
const EVICT_KEYS = ['host', 'port', 'token', 'username', 'password', 'keyPath', 'accessKey', 'secretKey', 'bucket', 'region', 'endpoint'] as const
if (EVICT_KEYS.some(k => k in updates)) {
this.disconnectProfile(id)
if (id === this._activeId) {
@@ -346,6 +363,23 @@ class ConnectionManagerImpl {
return
}
// OSS (qiniu / aliyun)
if (profile.type === 'qiniu' || profile.type === 'aliyun') {
this.setState('connecting')
try {
const t = new OssTransport(profile)
await t.connect()
this._pool.set(profileId, t)
this.setState('connected')
this.updateProfile(profileId, { lastConnected: Date.now() })
} catch (err) {
this._pool.delete(profileId)
this.setState('error')
throw err
}
return
}
// remote / http agent
this.setState('connecting')
try {

View File

@@ -0,0 +1,194 @@
/**
* OSS Transport — 通过 Wails IPC 调用 Go 后端 OSS 客户端
*/
import type {
FsTransport, FileItem, FileOperationResult, DetectTypeResult,
} from './transport'
import type { ConnectionProfile } from './connection-manager'
import {
OssConnect, OssDisconnect, OssListDir, OssReadFile,
OssWriteFile, OssGetFileInfo, OssCreateDir, OssCreateFile,
OssDeletePath, OssRenamePath, OssDownloadToTemp, OssGetCommonPaths,
OssWriteBase64File, OssGetSignedURL,
} from '@bindings/u-desk/app'
function transformFile(file: any): FileItem {
return { ...file, is_dir: file.is_dir, mod_time: file.mod_time }
}
function transformFileList(files: any[]): FileItem[] {
return files.map(transformFile)
}
const PREVIEW_CACHE_MAX = 50
export class OssTransport implements FsTransport {
private profile: ConnectionProfile
private connID: string | null = null
private previewCache = new Map<string, string>()
private previewOrder: string[] = []
constructor(profile: ConnectionProfile) {
this.profile = profile
}
async connect(): Promise<string> {
const result = await OssConnect({
provider: this.profile.type,
access_key: this.profile.accessKey || '',
secret_key: this.profile.secretKey || '',
endpoint: this.profile.endpoint || '',
})
this.connID = result
return result
}
get sessionId(): string | null { return this.connID }
async disconnect(): Promise<void> {
if (this.connID) {
try { await OssDisconnect(this.connID) } catch (e) {
console.warn('[OSS] disconnect error:', e)
}
this.connID = null
}
this.previewCache.clear()
this.previewOrder = []
}
private requireConn(): string {
if (!this.connID) throw new Error('OSS 未连接')
return this.connID
}
// ====== 文件列表与信息 ======
async listDir(path: string): Promise<FileItem[]> {
return transformFileList(await OssListDir(this.requireConn(), path))
}
async getFileInfo(path: string): Promise<Record<string, any>> {
return OssGetFileInfo(this.requireConn(), path)
}
// ====== 文件读写 ======
async readFile(path: string): Promise<string> {
return OssReadFile(this.requireConn(), path)
}
async writeFile(path: string, content: string): Promise<void> {
await OssWriteFile(this.requireConn(), path, content)
}
async saveBase64File(path: string, content: string): Promise<void> {
if (!content) throw new Error('无效的 base64 内容')
await OssWriteBase64File(this.requireConn(), path, content)
}
// ====== 文件操作 ======
async createFile(dirPath: string, filename: string): Promise<FileOperationResult> {
const fullPath = dirPath.replace(/\/$/, '') + '/' + filename
return OssCreateFile(this.requireConn(), fullPath)
}
async createDir(parentPath: string, dirname: string): Promise<FileOperationResult> {
const fullPath = parentPath.replace(/\/$/, '') + '/' + dirname
return OssCreateDir(this.requireConn(), fullPath)
}
async deletePath(path: string): Promise<FileOperationResult> {
return OssDeletePath(this.requireConn(), path)
}
async renamePath(oldPath: string, newPath: string): Promise<FileOperationResult> {
return OssRenamePath({
conn_id: this.requireConn(),
old_path: oldPath,
new_path: newPath,
})
}
// ====== ZIP 操作(不支持)======
async listZipContents(_zipPath: string): Promise<FileItem[]> {
throw new Error('ZIP 操作在 OSS 模式暂未实现')
}
async extractFileFromZip(_zipPath: string, _filePath: string): Promise<string> {
throw new Error('ZIP 操作在 OSS 模式暂未实现')
}
async extractFileFromZipToTemp(_zipPath: string, _filePath: string): Promise<string> {
throw new Error('ZIP 操作在 OSS 模式暂未实现')
}
async getZipFileInfo(_zipPath: string, _filePath: string): Promise<FileItem> {
throw new Error('ZIP 操作在 OSS 模式暂未实现')
}
// ====== 系统操作 ======
async openPath(_path: string): Promise<void> {
throw new Error('OSS 模式不支持打开本地路径')
}
async getFileServerURL(): Promise<string> {
return ''
}
getPreviewToken(): string {
return ''
}
async resolveShortcut(_lnkPath: string): Promise<any> {
return null
}
async detectFileTypeByContent(_path: string): Promise<DetectTypeResult> {
return { extension: '', category: 'unknown', mime_type: '', confidence: 0 }
}
async getCommonPaths(): Promise<Record<string, string>> {
return OssGetCommonPaths(this.requireConn())
}
// ====== 回收站(无)======
async getRecycleBinEntries(): Promise<any[]> {
return []
}
async restoreFromRecycleBin(_path: string): Promise<void> {}
async deletePermanently(_path: string): Promise<void> {}
async emptyRecycleBin(): Promise<void> {}
// ====== 预览辅助 ======
async downloadForPreview(remotePath: string): Promise<string> {
if (this.previewCache.has(remotePath)) {
this.previewOrder = this.previewOrder.filter(p => p !== remotePath)
this.previewOrder.push(remotePath)
return this.previewCache.get(remotePath)!
}
const localPath = await OssDownloadToTemp(this.requireConn(), remotePath)
if (this.previewOrder.length >= PREVIEW_CACHE_MAX) {
const oldest = this.previewOrder.shift()!
this.previewCache.delete(oldest)
}
this.previewCache.set(remotePath, localPath)
this.previewOrder.push(remotePath)
return localPath
}
/** 获取预签名 URL用于直接预览 */
async getSignedUrl(key: string): Promise<string> {
return OssGetSignedURL(this.requireConn(), key)
}
}

View File

@@ -4,9 +4,18 @@
<!-- 连接类型 -->
<div style="display: flex; align-items: center; gap: 8px">
<label style="font-size: 13px; width: 36px; flex-shrink: 0">类型</label>
<a-radio-group v-model="form.type" type="button" size="small">
<a-radio-group v-model="category" type="button" size="small">
<a-radio value="sftp">SFTP</a-radio>
<a-radio value="remote">HTTP Agent</a-radio>
<a-radio value="oss">云OSS</a-radio>
</a-radio-group>
</div>
<!-- OSS 厂商表单行仅在选中云OSS时显示 -->
<div v-if="category === 'oss'" style="display: flex; align-items: center; gap: 8px">
<label style="font-size: 13px; width: 36px; flex-shrink: 0">厂商</label>
<a-radio-group v-model="form.type" type="button" size="small">
<a-radio value="qiniu">七牛云</a-radio>
<a-radio value="aliyun">阿里云</a-radio>
</a-radio-group>
</div>
@@ -16,13 +25,26 @@
<a-input v-model="form.name" placeholder="如:生产服务器" style="flex: 1" />
</div>
<!-- 地址 -->
<div style="display: flex; align-items: center; gap: 8px">
<!-- 地址SFTP / HTTP Agent -->
<div v-if="form.type === 'sftp' || form.type === 'remote'" style="display: flex; align-items: center; gap: 8px">
<label style="font-size: 13px; width: 36px; flex-shrink: 0">地址</label>
<a-input v-model="form.host" :placeholder="form.type === 'sftp' ? '192.168.1.100' : '192.168.1.100'" style="flex: 1" />
<a-input-number v-model="form.port" :min="1" :max="65535" :placeholder="form.type === 'sftp' ? '22' : '9876'" style="width: 90px" hide-button />
</div>
<!-- OSS 认证字段 -->
<template v-if="form.type === 'qiniu' || form.type === 'aliyun'">
<div style="display: flex; align-items: center; gap: 8px">
<label style="font-size: 13px; width: 36px; flex-shrink: 0">AK</label>
<a-input v-model="form.accessKey" placeholder="AccessKey" style="flex: 1" />
</div>
<div style="display: flex; align-items: center; gap: 8px">
<label style="font-size: 13px; width: 36px; flex-shrink: 0">SK</label>
<a-input v-model="form.secretKey" type="password" placeholder="SecretKey" allow-clear style="flex: 1" />
</div>
</template>
<!-- SFTP 认证字段 -->
<template v-if="form.type === 'sftp'">
<div style="display: flex; align-items: center; gap: 8px">
@@ -66,7 +88,7 @@
</template>
<script setup lang="ts">
import { reactive, ref, watch, onMounted } from 'vue'
import { reactive, ref, computed, watch, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { Dialogs } from '@wailsio/runtime'
import { GetEnvVars } from '@bindings/u-desk/app'
@@ -94,6 +116,22 @@ const form = reactive({
username: 'root',
password: '',
keyPath: '',
accessKey: '',
secretKey: '',
bucket: '',
region: '',
endpoint: '',
})
const category = computed({
get: () => {
if (form.type === 'qiniu' || form.type === 'aliyun') return 'oss'
return form.type
},
set: (v: string) => {
if (v === 'oss') form.type = 'qiniu'
else form.type = v as ConnectionType
},
})
watch(() => props.visible, (val) => {
@@ -103,16 +141,25 @@ watch(() => props.visible, (val) => {
name: '', host: '', port: 22, token: '',
type: 'sftp' as ConnectionType,
username: 'root', password: '', keyPath: '',
accessKey: '', secretKey: '', bucket: '', region: '', endpoint: '',
})
})
watch(() => form.type, (t) => {
form.port = t === 'sftp' ? 22 : 9876
if (t === 'sftp') form.port = 22
else if (t === 'remote') form.port = 9876
else form.port = 0
})
async function handleOk(): Promise<boolean> {
if (!form.name?.trim()) { Message.warning('请输入名称'); return false }
if (!form.host?.trim()) { Message.warning('请输入地址'); return false }
const isOss = form.type === 'qiniu' || form.type === 'aliyun'
if (isOss) {
if (!form.accessKey?.trim()) { Message.warning('请输入 AccessKey'); return false }
if (!form.secretKey?.trim()) { Message.warning('请输入 SecretKey'); return false }
} else {
if (!form.host?.trim()) { Message.warning('请输入地址'); return false }
}
submitting.value = true
try {
@@ -156,6 +203,11 @@ function editProfile(id: string) {
username: profile.username || 'root',
password: profile.password || '',
keyPath: profile.keyPath || '',
accessKey: profile.accessKey || '',
secretKey: profile.secretKey || '',
bucket: profile.bucket || '',
region: profile.region || '',
endpoint: profile.endpoint || '',
})
}

View File

@@ -120,6 +120,7 @@ function handleDelete(p: { id: string; name: string }) {
function dotClass(p: { type: string }): string {
if (p.type === 'sftp') return 'sftp'
if (p.type === 'remote') return 'remote'
if (p.type === 'qiniu' || p.type === 'aliyun') return 'oss'
return 'local'
}
</script>
@@ -175,6 +176,7 @@ function dotClass(p: { type: string }): string {
.dot.local { background: var(--color-text-3); }
.dot.remote { background: var(--color-primary-6); }
.dot.sftp { background: #7c3aed; }
.dot.oss { background: #ff7d00; }
.label {
max-width: 70px;

View File

@@ -74,7 +74,7 @@
<!-- 视频预览 -->
<div v-else-if="config.isVideoView" class="media-preview">
<video :src="config.previewUrl" controls class="preview-video" @error="handleMediaError('视频')"></video>
<video :src="config.previewUrl" controls class="preview-video" @error="handleMediaError('视频')" @canplay="mediaErrorMsg = ''"></video>
<div v-if="mediaErrorMsg" class="media-error">{{ mediaErrorMsg }}</div>
<div class="media-meta">
<a-tag color="arcoblue">🎬 视频</a-tag>
@@ -83,7 +83,7 @@
<!-- 音频预览 -->
<div v-else-if="config.isAudioView" class="media-preview">
<audio :src="config.previewUrl" controls class="preview-audio" @error="handleMediaError('音频')"></audio>
<audio :src="config.previewUrl" controls class="preview-audio" @error="handleMediaError('音频')" @canplay="mediaErrorMsg = ''"></audio>
<div v-if="mediaErrorMsg" class="media-error">{{ mediaErrorMsg }}</div>
<div class="media-meta">
<a-tag color="green">🎵 音频</a-tag>
@@ -566,6 +566,7 @@ const handleImageError = () => {
}
const mediaErrorMsg = ref('')
watch(() => props.config.previewUrl, () => { mediaErrorMsg.value = '' })
const handleMediaError = (type: string) => {
mediaErrorMsg.value = `${type}文件加载失败,请检查网络连接或文件权限`
}

View File

@@ -9,6 +9,7 @@ import { normalizeFilePath, getExt } from '@/utils/fileUtils'
import { detectFileTypeByContent } from '@/api/system'
import { connectionManager } from '@/api/connection-manager'
import { SftpTransport } from '@/api/sftp-transport'
import { OssTransport } from '@/api/oss-transport'
import { getFileServerBaseURL } from '@/api/file-server'
import {
isImageFile, isVideoFile, isAudioFile, isPdfFile,
@@ -106,22 +107,25 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
}
/**
* 更新预览 URLSFTP 模式会先下载到本地临时目录)
* 更新预览 URL
* SFTP/OSS下载到本地临时目录后用本地文件服务器预览
*/
const updatePreviewUrl = async (path: string) => {
if (!path) { previewUrl.value = ''; return }
const transport = connectionManager.getTransport()
// SFTP 模式:下载到本地临时目录后用本地文件服务器预览
if (connectionManager.isSftp()) {
const transport = connectionManager.getTransport()
if (transport instanceof SftpTransport) {
try {
const tempPath = await transport.downloadForPreview(path)
previewUrl.value = getPreviewUrl(tempPath)
return
} catch {
// 下载失败,回退显示原始路径(会无法预览但不会崩溃)
}
// SFTP / OSS:下载到本地临时目录后用本地文件服务器预览
if (transport instanceof SftpTransport || transport instanceof OssTransport) {
try {
const tempPath = await transport.downloadForPreview(path)
// 临时文件通过本地文件服务器提供,始终用 localfs 路径
const base = getLocalServerURL()
const normalized = normalizeFilePath(tempPath, true)
const sep = base.endsWith('/') ? '' : '/'
previewUrl.value = `${base}${sep}localfs/${normalized}`
return
} catch {
// 下载失败,回退
}
}

View File

@@ -1371,7 +1371,9 @@ onMounted(async () => {
// 初始化加载:远程模式强制用根路径,避免 localStorage 残留 Windows 路径
const startPath = connectionManager.isRemote() ? '/'
: (commonPaths.value.length > 0 ? commonPaths.value[0].path : 'C:/')
if (filePath.value && !connectionManager.isRemote()) {
// 本地模式下只恢复 Windows 路径,跳过 Linux/OSS 路径残留
const isLocalPath = filePath.value && /^[A-Za-z]:/.test(filePath.value)
if (isLocalPath && !connectionManager.isRemote()) {
await loadDirectory(filePath.value)
} else {
filePath.value = startPath

View File

@@ -16,8 +16,13 @@ export const useConnectionStore = defineStore('connection', () => {
const isRemote = computed(() => connectionManager.isRemote())
const fileServerBaseURL = computed(() => connectionManager.getFileServerBaseURL())
function connect(id: string) {
connectionManager.connect(id)
async function connect(id: string) {
try {
await connectionManager.connect(id)
} catch {
// 连接失败,回退到本地
connectionManager.disconnect()
}
activeProfile.value = connectionManager.activeProfile
}

View File

@@ -170,6 +170,9 @@ export const FILE_ICONS = {
// 文件夹
FOLDER: '📁',
// OSS 桶
BUCKET: '🪣',
// 默认文件
FILE: '📄',
}

View File

@@ -124,6 +124,11 @@ export function getFileName(path) {
export function getFileIcon(fileInfo) {
if (!fileInfo) return FILE_ICONS.FILE
// OSS 桶
if (fileInfo.is_bucket) {
return FILE_ICONS.BUCKET
}
// 如果是目录
if (fileInfo.is_dir) {
return FILE_ICONS.FOLDER