Browse Source

feat(upload): 优化文件选择的逻辑

chendeben 9 months ago
parent
commit
38843c6b4f

+ 41 - 0
entry/src/main/ets/common/util/RcpSocketUtil.ets

@@ -207,6 +207,47 @@ export class RcpSocket {
     });
   }
 
+  public createDirectory(host: string, port: number, account: string, password: string, path: string,
+    enableHttps: boolean): Promise<void> {
+    return new Promise<void>((resolve, reject) => {
+      const url = this.buildRequestUrl(host, port, path, enableHttps);
+      const timeoutDuration: number = 10000;
+      console.info(UtilName, 'testTag', '发送MKCOL的url:' + url);
+
+      let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
+      let reqCfg: rcp.Configuration = {
+        security: secCfg,
+        transfer: {
+          timeout: {
+            connectMs: timeoutDuration
+          }
+        }
+      }
+      let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
+      let rcpSession = rcp.createSession(sessionCfg);
+      const encodedCredentials = buffer
+        .from(`${account}:${password}`)
+        .toString("base64");
+
+      const headers: rcp.RequestHeaders = {
+        Authorization: `Basic ${encodedCredentials}`,
+        'Content-Type': 'application/xml'
+      };
+      const req = new rcp.Request(url, "MKCOL", headers);
+      rcpSession
+        .fetch(req)
+        .then(() => {
+          rcpSession.close();
+          resolve();
+        })
+        .catch((err: BusinessError) => {
+          console.error(UtilName, "testTag", `${host} MKCOL失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
+          rcpSession?.close();
+          reject(err);
+        });
+    });
+  }
+
   public RcpSendMove(host: string, port: number, account: string, password: string, path: string, enableHttps: boolean,
     newPath: string): Promise<void> {
     return new Promise<void>((resolve, reject) => {

+ 94 - 1
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -797,6 +797,92 @@ export class RemoteDriveManager {
     return normalized || '/';
   }
 
+  private sanitizeRelativeUploadPath(path?: string): string {
+    if (!path || path.length === 0) {
+      return '';
+    }
+    let sanitized = path.replace(/\\/g, '/');
+    sanitized = sanitized.replace(/^\/+/, '');
+    sanitized = sanitized.replace(/\/+/g, '/');
+    return sanitized;
+  }
+
+  private combineRemoteUploadPath(basePath: string, relativePath: string): string {
+    const normalizedBase = this.normalizeFullPath(basePath);
+    const cleanedRelative = this.sanitizeRelativeUploadPath(relativePath);
+    if (!cleanedRelative || cleanedRelative.length === 0) {
+      return normalizedBase;
+    }
+    if (normalizedBase === '/') {
+      return this.normalizeFullPath(`/${cleanedRelative}`);
+    }
+    return this.normalizeFullPath(`${normalizedBase}/${cleanedRelative}`);
+  }
+
+  private getDirectoryFromRemotePath(remotePath: string): string {
+    if (!remotePath || remotePath === '/') {
+      return '/';
+    }
+    const normalized = this.normalizeFullPath(remotePath);
+    const lastSlash = normalized.lastIndexOf('/');
+    if (lastSlash <= 0) {
+      return '/';
+    }
+    return normalized.substring(0, lastSlash);
+  }
+
+  private async ensureRemoteDirectories(account: WebDavAccount, directoryPath: string): Promise<void> {
+    if (!directoryPath || directoryPath === '/') {
+      return;
+    }
+    const normalizedDir = this.normalizeFullPath(directoryPath);
+    if (normalizedDir === '/') {
+      return;
+    }
+    const segments = normalizedDir.split('/').filter(segment => segment.length > 0);
+    if (segments.length === 0) {
+      return;
+    }
+    let currentPath = '';
+    for (let i = 0; i < segments.length; i++) {
+      currentPath = `${currentPath}/${segments[i]}`;
+      const normalizedCurrent = this.normalizeFullPath(currentPath);
+      await this.ensureSingleRemoteDirectory(account, normalizedCurrent);
+    }
+  }
+
+  private async ensureSingleRemoteDirectory(account: WebDavAccount, directoryPath: string): Promise<void> {
+    try {
+      await this.rcpSocket.getFileList(
+        account.host,
+        account.localHost,
+        account.isUseLocalHost,
+        account.port,
+        directoryPath,
+        account.account,
+        account.password,
+        account.enableHttps
+      );
+    } catch (_error) {
+      const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+      try {
+        await this.rcpSocket.createDirectory(
+          host,
+          account.port,
+          account.account,
+          account.password,
+          directoryPath,
+          account.enableHttps
+        );
+        Logger.info(TAG, `创建远程目录: ${directoryPath}`);
+      } catch (createErr) {
+        const err = createErr as Error;
+        Logger.error(TAG, `创建远程目录失败 (${directoryPath}): ${err.message}`);
+        throw err;
+      }
+    }
+  }
+
   // 将 WebDAV 响应中的 href 统一转换为以 / 开头的相对路径
   private normalizeRemoteHref(rawHref: string): string {
     if (!rawHref || rawHref.trim().length === 0) {
@@ -1797,7 +1883,10 @@ export class RemoteDriveManager {
       // 构建远程路径 - 优先使用任务中的自定义路径,否则使用账户默认路径
       const uploadPath = task.customUploadPath || account.uploadFilePath || '/';
       const normalizedPath = this.normalizeFullPath(uploadPath);
-      const remotePath = `${normalizedPath === '/' ? '' : normalizedPath}/${song.fileName || song.name}`;
+      const relativeSegment = this.sanitizeRelativeUploadPath(song.remote_rel_path);
+      const fallbackFileName = song.fileName || song.name || `upload-${Date.now()}`;
+      const relativePath = relativeSegment.length > 0 ? relativeSegment : fallbackFileName;
+      const remotePath = this.combineRemoteUploadPath(normalizedPath, relativePath);
       Logger.info(TAG, '---------- 路径信息 ----------');
       Logger.info(TAG, `任务自定义路径: ${task.customUploadPath || '未指定'}`);
       Logger.info(TAG, `账户默认路径: ${account.uploadFilePath || '未配置'}`);
@@ -1805,6 +1894,7 @@ export class RemoteDriveManager {
       Logger.info(TAG, `规范化后的路径: ${normalizedPath}`);
       Logger.info(TAG, `文件名 (fileName): ${song.fileName}`);
       Logger.info(TAG, `文件名 (name): ${song.name}`);
+      Logger.info(TAG, `文件相对路径: ${relativePath}`);
       Logger.info(TAG, `最终远程路径: ${remotePath}`);
       Logger.info(TAG, `完整URL将是: ${account.enableHttps ? 'https' : 'http'}://${account.host}:${account.port}${remotePath}`);
       Logger.info(TAG, '------------------------------');
@@ -1830,6 +1920,9 @@ export class RemoteDriveManager {
         Logger.info(TAG, '远程文件不存在,可以上传');
       }
 
+      const remoteDirectory = this.getDirectoryFromRemotePath(remotePath);
+      await this.ensureRemoteDirectories(account, remoteDirectory);
+
       // 执行上传
       Logger.info(TAG, '开始上传文件数据...');
       const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;

+ 773 - 90
entry/src/main/ets/pages/UploadMusicPage.ets

@@ -7,13 +7,26 @@ import Logger from '../common/util/Logger';
 import { router } from '@kit.ArkUI';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { Constants } from '../Constants';
-import { picker } from '@kit.CoreFileKit';
+import { picker, fileUri } from '@kit.CoreFileKit';
 import { BusinessError } from '@kit.BasicServicesKit';
-import { fileUri } from '@kit.CoreFileKit';
-import { PreferencesUtil } from '@pura/harmony-utils';
+import { PreferencesUtil, AppUtil } from '@pura/harmony-utils';
 import { UploadTaskDataSource } from '../viewmodel/UploadTask';
+import { promptAction } from '@kit.ArkUI';
+import fs from '@ohos.file.fs';
 
-const TAG = 'UploadMusicPage';
+const TAG = 'heanup UploadMusicPage';
+
+interface LocalFileItem {
+  name: string;
+  path: string;
+  isDirectory: boolean;
+  isAudioFile: boolean;
+}
+
+interface LocalAudioFile {
+  fullPath: string;
+  relativePath: string;
+}
 
 @Entry
 @Component
@@ -26,7 +39,15 @@ export struct UploadMusicPage {
   // 页面状态变量
   @State selectedFiles: string[] = [];
   @State selectedFileNames: string[] = [];
+  @State selectedFileTypes: string[] = []; // 'file' 或 'folder'
   @State accounts: WebDavAccount[] = [];
+  
+  // 文件浏览器状态
+  @State isShowFileBrowser: boolean = false;
+  @State fileBrowserPath: string = '';
+  @State browserFiles: LocalFileItem[] = [];
+  @State selectedBrowserItems: Set<string> = new Set();
+  @State isLoadingFiles: boolean = false;
   @State selectedAccount: WebDavAccount | null = null;
   @State uploadPath: string = '/';
   @State availablePaths: string[] = [];
@@ -57,6 +78,7 @@ export struct UploadMusicPage {
   private eventHandler: (event: string) => void = (event: string) => {
     this.handleUploadEvent(event);
   };
+  private cachedDownloadRoot: string = '';
 
   aboutToAppear(): void {
     Logger.info(TAG, '页面即将显示');
@@ -118,6 +140,88 @@ export struct UploadMusicPage {
     }
   }
 
+  private getPreferredDownloadRoot(): string {
+    if (this.cachedDownloadRoot) {
+      return this.cachedDownloadRoot;
+    }
+    try {
+      let saved = PreferencesUtil.getStringSync('download_path', '');
+      if (saved && saved.length > 0) {
+        this.cachedDownloadRoot = this.normalizeLocalDirectory(saved);
+        if (this.cachedDownloadRoot) {
+          return this.cachedDownloadRoot;
+        }
+      }
+    } catch (error) {
+      const err = error as Error;
+      Logger.warn(TAG, `读取download_path失败: ${err.message}`);
+    }
+    const packName = AppUtil.getBundleName();
+    this.cachedDownloadRoot = `/storage/Users/currentUser/Download/${packName}`;
+    return this.cachedDownloadRoot;
+  }
+
+  private normalizeLocalDirectory(path: string): string {
+    if (!path || path.length === 0) {
+      return '';
+    }
+    let normalized = path.replace(/\\/g, '/');
+    normalized = normalized.replace(/\/+$/, '');
+    return normalized;
+  }
+
+  private ensureDirectoryAccessible(path: string): boolean {
+    if (!path || path.length === 0) {
+      return false;
+    }
+    try {
+      const stat = fs.statSync(path);
+      return stat.isDirectory();
+    } catch (_error) {
+      try {
+        fs.mkdirSync(path);
+        return true;
+      } catch (createErr) {
+        const err = createErr as Error;
+        Logger.warn(TAG, `创建目录失败(${path}): ${err.message}`);
+        return false;
+      }
+    }
+  }
+
+  private async requestDownloadRootFromSystem(): Promise<string> {
+    try {
+      const documentViewPicker = new picker.DocumentViewPicker();
+      const documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD });
+      if (documentSaveResult && documentSaveResult.length > 0) {
+        const resolvedPath = new fileUri.FileUri(documentSaveResult[0]).path;
+        const normalized = this.normalizeLocalDirectory(resolvedPath);
+        if (normalized) {
+          this.cachedDownloadRoot = normalized;
+          const storedValue = normalized.endsWith('/') ? normalized : `${normalized}/`;
+          PreferencesUtil.putSync('download_path', storedValue);
+          return normalized;
+        }
+      }
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `请求系统下载目录失败: ${err.message}`);
+    }
+    return '';
+  }
+
+  private async resolveInitialBrowserPath(): Promise<string> {
+    let preferred = this.getPreferredDownloadRoot();
+    if (preferred && this.ensureDirectoryAccessible(preferred)) {
+      return preferred;
+    }
+    const requested = await this.requestDownloadRootFromSystem();
+    if (requested && this.ensureDirectoryAccessible(requested)) {
+      return requested;
+    }
+    return '';
+  }
+
   /**
    * 预选指定账户
    * @param accountId 账户ID
@@ -318,16 +422,19 @@ export struct UploadMusicPage {
         
         // 提取文件名用于显示
         this.selectedFileNames = [];
+        this.selectedFileTypes = [];
         for (let i = 0; i < documentSelectResult.length; i++) {
           const uri = documentSelectResult[i];
           try {
             const fileUriObj = new fileUri.FileUri(uri);
             const fileName = fileUriObj.name;
             this.selectedFileNames.push(fileName);
+            this.selectedFileTypes.push('file');
           } catch (error) {
             const err = error as Error;
             Logger.error(TAG, `解析文件URI失败: ${err.message}`);
             this.selectedFileNames.push('未知文件');
+            this.selectedFileTypes.push('file');
           }
         }
         
@@ -339,6 +446,218 @@ export struct UploadMusicPage {
     }
   }
 
+  /**
+   * 打开文件浏览器
+   */
+  private async openFileBrowser(): Promise<void> {
+    try {
+      const initialPath = await this.resolveInitialBrowserPath();
+      if (!initialPath) {
+        promptAction.showToast({
+          message: '无法定位到下载目录,请授予文件访问权限',
+          duration: 2000
+        });
+        return;
+      }
+
+      this.fileBrowserPath = initialPath;
+      this.selectedBrowserItems.clear();
+      this.isShowFileBrowser = true;
+      await this.loadLocalFiles(initialPath);
+      Logger.info(TAG, `文件浏览器已定位到: ${initialPath}`);
+    } catch (error) {
+      const err = error as BusinessError;
+      Logger.error(TAG, `打开文件浏览器失败: ${err.message}`);
+      promptAction.showToast({
+        message: '打开文件浏览器失败',
+        duration: 2000
+      });
+    }
+  }
+
+  /**
+   * 加载本地文件列表
+   */
+  private async loadLocalFiles(path: string): Promise<void> {
+    try {
+      this.isLoadingFiles = true;
+      this.browserFiles = [];
+      
+      Logger.info(TAG, `加载目录: ${path}`);
+      
+      const files = fs.listFileSync(path);
+      
+      for (let i = 0; i < files.length; i++) {
+        const fileName = files[i];
+        const fullPath = `${path}/${fileName}`;
+        
+        try {
+          const stat = fs.statSync(fullPath);
+          const isDirectory = stat.isDirectory();
+          const lowerFileName = fileName.toLowerCase();
+          const isAudioFile = !isDirectory && Constants.AUDIO_EXTENSIONS.some(ext => lowerFileName.endsWith(ext));
+          
+          // 只显示文件夹和音频文件
+          if (isDirectory || isAudioFile) {
+            this.browserFiles.push({
+              name: fileName,
+              path: fullPath,
+              isDirectory: isDirectory,
+              isAudioFile: isAudioFile
+            });
+          }
+        } catch (error) {
+          Logger.warn(TAG, `无法访问: ${fullPath}`);
+        }
+      }
+      
+      // 排序:文件夹在前,文件在后
+      this.browserFiles.sort((a, b) => {
+        if (a.isDirectory && !b.isDirectory) return -1;
+        if (!a.isDirectory && b.isDirectory) return 1;
+        return a.name.localeCompare(b.name);
+      });
+      
+      Logger.info(TAG, `加载了 ${this.browserFiles.length} 个项目`);
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `加载文件列表失败: ${err.message}`);
+      promptAction.showToast({
+        message: `加载失败: ${err.message}`,
+        duration: 2000
+      });
+    } finally {
+      this.isLoadingFiles = false;
+    }
+  }
+
+  /**
+   * 进入子目录
+   */
+  private async enterDirectory(item: LocalFileItem): Promise<void> {
+    if (item.isDirectory) {
+      this.fileBrowserPath = item.path;
+      await this.loadLocalFiles(item.path);
+    }
+  }
+
+  /**
+   * 返回上级目录
+   */
+  private async goBackDirectory(): Promise<void> {
+    const lastSlash = this.fileBrowserPath.lastIndexOf('/');
+    if (lastSlash > 0) {
+      this.fileBrowserPath = this.fileBrowserPath.substring(0, lastSlash);
+      await this.loadLocalFiles(this.fileBrowserPath);
+    }
+  }
+
+  /**
+   * 切换项目选中状态
+   */
+  private toggleItemSelection(item: LocalFileItem): void {
+    if (this.selectedBrowserItems.has(item.path)) {
+      this.selectedBrowserItems.delete(item.path);
+    } else {
+      this.selectedBrowserItems.add(item.path);
+    }
+    // 触发UI更新
+    this.selectedBrowserItems = new Set(this.selectedBrowserItems);
+  }
+
+  /**
+   * 确认选择文件
+   */
+  private confirmFileSelection(): void {
+    if (this.selectedBrowserItems.size === 0) {
+      promptAction.showToast({
+        message: '请至少选择一个项目',
+        duration: 2000
+      });
+      return;
+    }
+
+    // 将选中的项目添加到已选列表
+    this.selectedBrowserItems.forEach(path => {
+      const item = this.browserFiles.find(f => f.path === path);
+      if (item) {
+        const uri = `file://${path}`;
+        this.selectedFiles.push(uri);
+        this.selectedFileNames.push(item.name);
+        this.selectedFileTypes.push(item.isDirectory ? 'folder' : 'file');
+      }
+    });
+
+    promptAction.showToast({
+      message: `已添加 ${this.selectedBrowserItems.size} 个项目`,
+      duration: 2000
+    });
+
+    this.closeFileBrowser();
+  }
+
+  /**
+   * 关闭文件浏览器
+   */
+  private closeFileBrowser(): void {
+    this.isShowFileBrowser = false;
+    this.selectedBrowserItems.clear();
+    this.browserFiles = [];
+  }
+
+  /**
+   * 递归扫描文件夹,获取所有音频文件
+   */
+  private async scanFolderForAudioFiles(folderPath: string, relativePrefix: string = ''): Promise<LocalAudioFile[]> {
+    const audioFiles: LocalAudioFile[] = [];
+    
+    try {
+      const files = fs.listFileSync(folderPath);
+      
+      for (let i = 0; i < files.length; i++) {
+        const fileName = files[i];
+        const fullPath = `${folderPath}/${fileName}`;
+        
+        try {
+          const stat = fs.statSync(fullPath);
+          
+          if (stat.isDirectory()) {
+            const nextPrefix = relativePrefix ? `${relativePrefix}/${fileName}` : fileName;
+            const subFiles = await this.scanFolderForAudioFiles(fullPath, nextPrefix);
+            audioFiles.push(...subFiles);
+          } else {
+            const lowerFileName = fileName.toLowerCase();
+            const isAudio = Constants.AUDIO_EXTENSIONS.some(ext => lowerFileName.endsWith(ext));
+            if (isAudio) {
+              const relativePath = relativePrefix ? `${relativePrefix}/${fileName}` : fileName;
+              audioFiles.push({
+                fullPath,
+                relativePath
+              });
+            }
+          }
+        } catch (error) {
+          const err = error as Error;
+          Logger.warn(TAG, `无法访问: ${fullPath}, 错误: ${err.message}`);
+        }
+      }
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `扫描文件夹失败: ${err.message}`);
+    }
+    
+    return audioFiles;
+  }
+
+  private getFileNameFromPath(fullPath: string): string {
+    if (!fullPath) {
+      return '';
+    }
+    const normalized = fullPath.replace(/\\/g, '/');
+    const lastSlash = normalized.lastIndexOf('/');
+    return lastSlash >= 0 ? normalized.substring(lastSlash + 1) : normalized;
+  }
+
   /**
    * 移除已选文件
    * @param index 文件索引
@@ -347,7 +666,8 @@ export struct UploadMusicPage {
     if (index >= 0 && index < this.selectedFiles.length) {
       this.selectedFiles.splice(index, 1);
       this.selectedFileNames.splice(index, 1);
-      Logger.info(TAG, `移除文件,剩余 ${this.selectedFiles.length} 个`);
+      this.selectedFileTypes.splice(index, 1);
+      Logger.info(TAG, `移除项目,剩余 ${this.selectedFiles.length} 个`);
     }
   }
 
@@ -370,8 +690,16 @@ export struct UploadMusicPage {
       return;
     }
 
+    Logger.info(TAG, '========== 打开路径浏览器 ==========');
+    Logger.info(TAG, `当前上传路径: ${this.uploadPath}`);
+    Logger.info(TAG, `选中账户: ${this.selectedAccount.name}`);
+    
     this.currentBrowsePath = this.uploadPath;
     this.isShowPathBrowser = true;
+    
+    Logger.info(TAG, `isShowPathBrowser 设置为: ${this.isShowPathBrowser}`);
+    Logger.info(TAG, '开始加载文件夹列表...');
+    
     this.loadWebDavFolders(this.currentBrowsePath);
   }
 
@@ -379,6 +707,7 @@ export struct UploadMusicPage {
    * 关闭路径浏览器
    */
   private closePathBrowser(): void {
+    Logger.info(TAG, '========== 关闭路径浏览器 ==========');
     this.isShowPathBrowser = false;
     this.browseFolders = [];
   }
@@ -475,54 +804,103 @@ export struct UploadMusicPage {
 
     try {
       Logger.info(TAG, '开始上传任务');
+      
+      // 显示处理提示
+      promptAction.showToast({
+        message: '正在处理文件...',
+        duration: 2000
+      });
 
       // 构建VideoItem列表
       const songs: VideoItem[] = [];
+      let totalFiles = 0;
+      let totalFolders = 0;
+      
       for (let i = 0; i < this.selectedFiles.length; i++) {
         const selectedUri = this.selectedFiles[i];
-        let displayName = this.selectedFileNames[i];
-        let resolvedPath = '';
+        const displayName = this.selectedFileNames[i];
+        const itemType = this.selectedFileTypes[i];
 
         try {
           const fileUriObj = new fileUri.FileUri(selectedUri);
-          resolvedPath = fileUriObj.path || '';
-          if (!displayName) {
-            displayName = fileUriObj.name || selectedUri;
+          const resolvedPath = fileUriObj.path || '';
+          
+          if (!resolvedPath) {
+            Logger.error(TAG, `无法获取本地路径,跳过: ${selectedUri}`);
+            continue;
+          }
+
+          if (itemType === 'folder') {
+            totalFolders++;
+            const folderName = displayName || this.getFileNameFromPath(resolvedPath);
+            Logger.info(TAG, `扫描文件夹: ${folderName}`);
+            const audioFiles = await this.scanFolderForAudioFiles(resolvedPath, folderName);
+            Logger.info(TAG, `文件夹 ${folderName} 包含 ${audioFiles.length} 个音频文件`);
+            
+            for (let j = 0; j < audioFiles.length; j++) {
+              const audioFile = audioFiles[j];
+              const audioFileName = this.getFileNameFromPath(audioFile.fullPath);
+              
+              const videoItem = new VideoItem(
+                audioFileName,
+                `file://${audioFile.fullPath}`,
+                audioFile.fullPath,
+                CommonConstants.TYPE_LOCAL,
+                0,
+                '',
+                undefined,
+                undefined,
+                undefined,
+                undefined,
+                undefined,
+                audioFileName
+              );
+              videoItem.remote_rel_path = audioFile.relativePath;
+              songs.push(videoItem);
+            }
+          } else {
+            // 处理单个文件
+            totalFiles++;
+            const videoItem = new VideoItem(
+              displayName,
+              selectedUri,
+              resolvedPath,
+              CommonConstants.TYPE_LOCAL,
+              0,
+              '',
+              undefined,
+              undefined,
+              undefined,
+              undefined,
+              undefined,
+              displayName
+            );
+            videoItem.remote_rel_path = displayName;
+            songs.push(videoItem);
           }
         } catch (error) {
           const err = error as Error;
-          Logger.error(TAG, `解析文件路径失败: ${err.message}`);
+          Logger.error(TAG, `处理项目失败: ${err.message}`);
           continue;
         }
-
-        if (!resolvedPath) {
-          Logger.error(TAG, `无法获取本地路径,跳过文件: ${selectedUri}`);
-          continue;
-        }
-
-        const videoItem = new VideoItem(
-          displayName,
-          selectedUri,
-          resolvedPath,
-          CommonConstants.TYPE_LOCAL,
-          0,
-          '',
-          undefined,
-          undefined,
-          undefined,
-          undefined,
-          undefined,
-          displayName
-        );
-
-        songs.push(videoItem);
       }
 
       if (songs.length === 0) {
+        promptAction.showToast({
+          message: '未找到可上传的音频文件',
+          duration: 2000
+        });
         Logger.warn(TAG, '未找到可上传的有效文件,取消任务');
         return;
       }
 
+      Logger.info(TAG, `处理完成: ${totalFiles} 个文件, ${totalFolders} 个文件夹, 共 ${songs.length} 个音频文件`);
+      
+      promptAction.showToast({
+        message: `准备上传 ${songs.length} 个文件`,
+        duration: 2000
+      });
+
       // 添加到上传队列,传递用户选择的上传路径
       Logger.info(TAG, `使用上传路径: ${this.uploadPath}`);
       this.webdavManager.addToUploadQueue(songs, this.selectedAccount, this.uploadPath);
@@ -536,6 +914,10 @@ export struct UploadMusicPage {
     } catch (error) {
       const err = error as Error;
       Logger.error(TAG, `启动上传失败: ${err.message}`);
+      promptAction.showToast({
+        message: `上传失败: ${err.message}`,
+        duration: 2000
+      });
     }
   }
 
@@ -900,6 +1282,13 @@ export struct UploadMusicPage {
             .borderRadius(6)
             .enabled(this.selectedAccount !== null)
             .opacity(this.selectedAccount !== null ? 1.0 : 0.4)
+            .bindContentCover(this.isShowPathBrowser, this.PathBrowserDialogBuilder(), {
+              modalTransition: ModalTransition.DEFAULT,
+              onDisappear: () => {
+                Logger.info(TAG, 'PathBrowser onDisappear 回调');
+                this.isShowPathBrowser = false;
+              }
+            })
             .onClick(() => {
               this.openPathBrowser();
             })
@@ -1375,29 +1764,57 @@ export struct UploadMusicPage {
       }
       .alignSelf(ItemAlign.Start)
 
-      Button({ type: ButtonType.Normal }) {
-        Row({ space: 8 }) {
-          Text('📂')
-            .fontSize(18)
-          Text('选择音频文件')
-            .fontSize(15)
-            .fontWeight(FontWeight.Medium)
+      // 按钮组
+      Row({ space: 10 }) {
+        Button({ type: ButtonType.Normal }) {
+          Row({ space: 6 }) {
+            Text('📄')
+              .fontSize(16)
+            Text('快速选择')
+              .fontSize(14)
+              .fontWeight(FontWeight.Medium)
+          }
+        }
+        .layoutWeight(1)
+        .height(48)
+        .backgroundColor(this.themeColor)
+        .fontColor('#FFFFFF')
+        .borderRadius(10)
+        .shadow({
+          radius: 8,
+          color: this.themeColor + '40',
+          offsetX: 0,
+          offsetY: 3
+        })
+        .onClick(() => {
+          this.openFilePicker();
+        })
+
+        Button({ type: ButtonType.Normal }) {
+          Row({ space: 6 }) {
+            Text('📁')
+              .fontSize(16)
+            Text('浏览选择')
+              .fontSize(14)
+              .fontWeight(FontWeight.Medium)
+          }
         }
+        .layoutWeight(1)
+        .height(48)
+        .backgroundColor(this.themeColor)
+        .fontColor('#FFFFFF')
+        .borderRadius(10)
+        .shadow({
+          radius: 8,
+          color: this.themeColor + '40',
+          offsetX: 0,
+          offsetY: 3
+        })
+        .onClick(() => {
+          this.openFileBrowser();
+        })
       }
       .width('100%')
-      .height(50)
-      .backgroundColor(this.themeColor)
-      .fontColor('#FFFFFF')
-      .borderRadius(10)
-      .shadow({
-        radius: 8,
-        color: this.themeColor + '40',
-        offsetX: 0,
-        offsetY: 3
-      })
-      .onClick(() => {
-        this.openFilePicker();
-      })
 
       if (this.selectedFiles.length > 0) {
         Row({ space: 10 }) {
@@ -1405,15 +1822,35 @@ export struct UploadMusicPage {
             .fontSize(18)
             .fontColor(this.themeColor)
           
-          Text(`已选择 ${this.selectedFiles.length} 个文件`)
-            .fontSize(14)
-            .fontWeight(FontWeight.Medium)
-            .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+          Column({ space: 2 }) {
+            Text(`已选择 ${this.selectedFiles.length} 项`)
+              .fontSize(14)
+              .fontWeight(FontWeight.Medium)
+              .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+            
+            Row({ space: 8 }) {
+              if (this.selectedFileTypes.filter(t => t === 'file').length > 0) {
+                Text(`${this.selectedFileTypes.filter(t => t === 'file').length} 个文件`)
+                  .fontSize(12)
+                  .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+              }
+              if (this.selectedFileTypes.filter(t => t === 'folder').length > 0) {
+                Text(`${this.selectedFileTypes.filter(t => t === 'folder').length} 个文件夹`)
+                  .fontSize(12)
+                  .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+              }
+            }
+          }
+          .alignItems(HorizontalAlign.Start)
         }
         .width('100%')
         .padding(12)
-        .backgroundColor(this.isDarkMode ? this.themeColor + '20' : this.themeColor + '10')
+        .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
         .borderRadius(8)
+        .border({
+          width: 1,
+          color: this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30'
+        })
       }
     }
     .width('100%')
@@ -1461,27 +1898,40 @@ export struct UploadMusicPage {
         ForEach(this.selectedFileNames, (fileName: string, index: number) => {
           ListItem() {
             Row({ space: 12 }) {
-              // 文件图标
-              Text('🎵')
+              // 文件/文件夹图标
+              Text(this.selectedFileTypes[index] === 'folder' ? '📁' : '🎵')
                 .fontSize(20)
 
-              Text(fileName)
-                .fontSize(14)
-                .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
-                .layoutWeight(1)
-                .maxLines(1)
-                .textOverflow({ overflow: TextOverflow.Ellipsis })
+              Column({ space: 2 }) {
+                Text(fileName)
+                  .fontSize(14)
+                  .fontWeight(FontWeight.Medium)
+                  .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+                  .maxLines(1)
+                  .textOverflow({ overflow: TextOverflow.Ellipsis })
+                
+                if (this.selectedFileTypes[index] === 'folder') {
+                  Text('文件夹')
+                    .fontSize(12)
+                    .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+                }
+              }
+              .alignItems(HorizontalAlign.Start)
+              .layoutWeight(1)
 
-              Button('移除')
-                .fontSize(12)
-                .height(32)
-                .padding({ left: 12, right: 12 })
-                .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30')
-                .fontColor('#FFFFFF')
-                .borderRadius(6)
-                .onClick(() => {
-                  this.removeSelectedFile(index);
-                })
+              Button({ type: ButtonType.Normal }) {
+                Text('移除')
+                  .fontSize(12)
+                  .fontWeight(FontWeight.Medium)
+              }
+              .height(32)
+              .padding({ left: 12, right: 12 })
+              .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30')
+              .fontColor('#FFFFFF')
+              .borderRadius(6)
+              .onClick(() => {
+                this.removeSelectedFile(index);
+              })
             }
             .width('100%')
             .padding(12)
@@ -1496,10 +1946,6 @@ export struct UploadMusicPage {
       }
       .width('100%')
       .constraintSize({ maxHeight: 300 })
-      .divider({
-        strokeWidth: 1,
-        color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
-      })
     }
     .width('100%')
     .padding(16)
@@ -1594,27 +2040,264 @@ export struct UploadMusicPage {
     .width('100%')
     .height('100%')
     .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
-    .bindContentCover(this.isShowPathBrowser, this.PathBrowserDialogBuilder(), {
+    .bindContentCover(this.isShowFileBrowser, this.FileBrowserDialogBuilder(), {
       modalTransition: ModalTransition.DEFAULT,
-      backgroundColor: 'rgba(0, 0, 0, 0.6)',
       onDisappear: () => {
-        this.isShowPathBrowser = false;
+        Logger.info(TAG, 'FileBrowser onDisappear 回调');
+        this.isShowFileBrowser = false;
       }
     })
   }
 
   @Builder
   PathBrowserDialogBuilder() {
-    Column() {
-      this.PathBrowserDialog()
+    Stack() {
+      // 背景层
+      Column()
+        .width('100%')
+        .height('100%')
+        .backgroundColor('rgba(0, 0, 0, 0.5)')
+      
+      // 对话框内容(居中显示)
+      Column() {
+        this.PathBrowserDialog()
+      }
+      .width('100%')
+      .height('100%')
+      .justifyContent(FlexAlign.Center)
+      .alignItems(HorizontalAlign.Center)
     }
     .width('100%')
     .height('100%')
-    .justifyContent(FlexAlign.Center)
-    .backgroundColor('rgba(0, 0, 0, 0.5)')
-    .onClick(() => {
-      // 点击背景关闭对话框
-      this.closePathBrowser();
+  }
+
+  /**
+   * 文件浏览器对话框
+   */
+  @Builder
+  FileBrowserDialog() {
+    Column() {
+      // 标题栏
+      Row({ space: 12 }) {
+        Text('浏览选择文件')
+          .fontSize(18)
+          .fontWeight(FontWeight.Bold)
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+          .layoutWeight(1)
+
+        Button('关闭')
+          .fontSize(14)
+          .height(36)
+          .padding({ left: 16, right: 16 })
+          .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+          .borderRadius(8)
+          .onClick(() => {
+            this.closeFileBrowser();
+          })
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+
+      // 当前路径和返回按钮
+      Row({ space: 12 }) {
+        Column({ space: 4 }) {
+          Text('当前目录')
+            .fontSize(12)
+            .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+          
+          Text(this.fileBrowserPath)
+            .fontSize(13)
+            .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+        }
+        .alignItems(HorizontalAlign.Start)
+        .layoutWeight(1)
+
+        if (this.fileBrowserPath.lastIndexOf('/') > 0) {
+          Button('返回上级')
+            .fontSize(12)
+            .height(36)
+            .padding({ left: 12, right: 12 })
+            .backgroundColor(this.themeColor)
+            .fontColor('#FFFFFF')
+            .borderRadius(8)
+            .onClick(() => {
+              this.goBackDirectory();
+            })
+        }
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
+
+      // 文件列表
+      if (this.isLoadingFiles) {
+        Column({ space: 12 }) {
+          LoadingProgress()
+            .width(48)
+            .height(48)
+            .color(this.themeColor)
+          
+          Text('加载中...')
+            .fontSize(14)
+            .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
+        }
+        .width('100%')
+        .height(400)
+        .justifyContent(FlexAlign.Center)
+        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+      } else if (this.browserFiles.length === 0) {
+        Column({ space: 12 }) {
+          Text('📂')
+            .fontSize(48)
+          
+          Text('当前目录为空')
+            .fontSize(14)
+            .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+        }
+        .width('100%')
+        .height(400)
+        .justifyContent(FlexAlign.Center)
+        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+      } else {
+        List({ space: 8 }) {
+          ForEach(this.browserFiles, (item: LocalFileItem, index: number) => {
+            ListItem() {
+              Row({ space: 12 }) {
+                // 复选框
+                Checkbox()
+                  .select(this.selectedBrowserItems.has(item.path))
+                  .selectedColor(this.themeColor)
+                  .onChange((checked: boolean) => {
+                    this.toggleItemSelection(item);
+                  })
+
+                // 图标
+                Text(item.isDirectory ? '📁' : '🎵')
+                  .fontSize(24)
+
+                // 文件名
+                Column({ space: 2 }) {
+                  Text(item.name)
+                    .fontSize(14)
+                    .fontWeight(FontWeight.Medium)
+                    .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+                    .maxLines(1)
+                    .textOverflow({ overflow: TextOverflow.Ellipsis })
+                  
+                  if (item.isDirectory) {
+                    Text('文件夹')
+                      .fontSize(12)
+                      .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+                  }
+                }
+                .alignItems(HorizontalAlign.Start)
+                .layoutWeight(1)
+
+                // 进入按钮(仅文件夹)
+                if (item.isDirectory) {
+                  Text('→')
+                    .fontSize(20)
+                    .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+                    .onClick(() => {
+                      this.enterDirectory(item);
+                    })
+                }
+              }
+              .width('100%')
+              .padding(12)
+              .backgroundColor(this.selectedBrowserItems.has(item.path) ?
+                (this.isDarkMode ? '#2E3238' : '#FFFFFF') :
+                (this.isDarkMode ? '#2E3238' : '#F5F7FA'))
+              .borderRadius(8)
+              .border({
+                width: this.selectedBrowserItems.has(item.path) ? 2 : 1,
+                color: this.selectedBrowserItems.has(item.path) ?
+                  this.themeColor :
+                  (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
+              })
+              .onClick(() => {
+                if (!item.isDirectory) {
+                  this.toggleItemSelection(item);
+                }
+              })
+            }
+          }, (item: LocalFileItem, index: number) => `${index}-${item.path}`)
+        }
+        .width('100%')
+        .height(400)
+        .padding(16)
+        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+      }
+
+      // 底部按钮
+      Row({ space: 12 }) {
+        Text(`已选择 ${this.selectedBrowserItems.size} 项`)
+          .fontSize(14)
+          .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
+          .layoutWeight(1)
+
+        Button('取消')
+          .fontSize(14)
+          .height(48)
+          .padding({ left: 20, right: 20 })
+          .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+          .borderRadius(8)
+          .onClick(() => {
+            this.closeFileBrowser();
+          })
+
+        Button('确定')
+          .fontSize(14)
+          .height(48)
+          .padding({ left: 20, right: 20 })
+          .backgroundColor(this.themeColor)
+          .fontColor('#FFFFFF')
+          .borderRadius(8)
+          .enabled(this.selectedBrowserItems.size > 0)
+          .opacity(this.selectedBrowserItems.size > 0 ? 1.0 : 0.4)
+          .onClick(() => {
+            this.confirmFileSelection();
+          })
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    }
+    .width('90%')
+    .constraintSize({ maxWidth: 700, maxHeight: '80%' })
+    .backgroundColor(this.isDarkMode ? '#191A1C' : '#FFFFFF')
+    .borderRadius(16)
+    .shadow({
+      radius: 24,
+      color: this.isDarkMode ? '#33000000' : '#19000000',
+      offsetX: 0,
+      offsetY: 8
     })
   }
+
+  @Builder
+  FileBrowserDialogBuilder() {
+    Stack() {
+      // 背景层(不添加 onClick,避免误关闭)
+      Column()
+        .width('100%')
+        .height('100%')
+        .backgroundColor('rgba(0, 0, 0, 0.5)')
+      
+      // 对话框内容(居中显示)
+      Column() {
+        this.FileBrowserDialog()
+      }
+      .width('100%')
+      .height('100%')
+      .justifyContent(FlexAlign.Center)
+    }
+    .width('100%')
+    .height('100%')
+  }
 }