新增: 云OSS存储集成(七牛云+阿里云)+多桶导航+GBK编码自动转换
This commit is contained in:
@@ -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 读取文件
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,8 @@ export {
|
||||
};
|
||||
|
||||
export {
|
||||
OssConnectRequest,
|
||||
OssRenamePathRequest,
|
||||
RenamePathRequest,
|
||||
SaveAppConfigRequest,
|
||||
SaveBase64FileRequest,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
194
frontend/src/api/oss-transport.ts
Normal file
194
frontend/src/api/oss-transport.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 || '',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}文件加载失败,请检查网络连接或文件权限`
|
||||
}
|
||||
|
||||
@@ -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 = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新预览 URL(SFTP 模式会先下载到本地临时目录)
|
||||
* 更新预览 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 {
|
||||
// 下载失败,回退
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -170,6 +170,9 @@ export const FILE_ICONS = {
|
||||
// 文件夹
|
||||
FOLDER: '📁',
|
||||
|
||||
// OSS 桶
|
||||
BUCKET: '🪣',
|
||||
|
||||
// 默认文件
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user