Browse Source

Merge branch 'refs/heads/feature/webdav_upload'

chendeben 9 months ago
parent
commit
27836e1d8a

+ 29 - 0
AGENTS.md

@@ -0,0 +1,29 @@
+# Repository Guidelines
+# 回复语言
+所有回复必须使用中文
+
+## 项目结构与模块划分
+核心 ArkTS 代码位于 `entry/src/main/ets`,其中 `view/` 负责 UI 组件(如 `LocalMusic.ets`),`viewmodel/` 管理数据模型(如 `VideoItem`),`common/` 存放工具类与常量(`RemoteDriveManager`、`RcpSocketUtil` 等),`controller/` 承接 Ability 调度。鸿蒙资源位于 `entry/src/main/resources`。本地播放器内核与 FFmpeg 构建脚本存放在 `ijkplayer/`,复用的二进制库放在 `lib/`。流程文档、架构图与配置说明集中在 `doc/`。三方 OpenHarmony 依赖通过 `oh_modules/` 与 `ohpm` 管理。
+
+## 构建、测试与开发命令
+- `ohpm install`:根据 `oh-package.json5` 安装 ArkTS 依赖,新增/升级模块后必跑。
+- `hvigor --mode module assemble entry`:编译 `entry` 模块并生成默认 HAP,必要时追加 `--product-name default`。
+- `hvigor --mode module clean`:清理构建缓存,确保可重复构建。
+- DevEco Studio 的 “Run > Run Entry” 等价于 assemble 并自动部署到真机/模拟器。
+
+## 代码风格与命名规范
+ArkTS/ETS 采用两个空格缩进、PascalCase 文件名(如 `UploadMusicPage.ets`)、camelCase 成员命名。UI 逻辑保持声明式,业务流程尽量下沉到 `common/` 或 `viewmodel/`。除非调试底层连接,优先使用 `Logger` 封装而非裸 `console`。原生模块遵循仓库 `.clang-format`(LLVM 风格,两空格)与 `.clang-tidy` 配置,禁止混用 Tab/空格。提交前通过 DevEco Studio 运行 ESLint/ArkTS 检查。
+
+## 测试指南
+Hypium 自动化尚未完备,现阶段以真机冒烟为主:播放本地音频、执行 WebDAV 上传(`UploadMusicPage`)、验证后台任务。新增复杂逻辑时,在 `entry/src/main/test/` 下补充 Hypium 用例,命名为 `<Feature>Spec.ets`。CI 覆盖前,请在 PR 描述中列出手工测试步骤和结果。
+
+## 提交与 PR 规范
+历史提交常见中文简述配合 Conventional Commits 前缀(如 `feat(remote-drive): ...`、`fix: ...`)。主题维持祈使句、72 字符内,涉及特定模块请加 scope。提交 PR 时需:
+1. 说明用户可见改动及涉及模块。
+2. 关联 Issue/需求编号。
+3. UI 或网络行为变更附上截图/日志(如 WebDAV 日志)。
+4. 列出执行的手工或自动化测试。
+避免在一次 PR 中混入无关重构,必要时拆分。
+
+## 安全与配置提示
+切勿提交真实 WebDAV 凭据或 VIP 秘钥,统一通过 `PreferencesUtil` 注入或在 `doc/` 示例中使用假数据。大体积媒体应放在 `entry/src/main/resources/rawfile` 或 `.gitignore` 指定的外部包。修改 `ijkplayer/` 相关内容时,本地执行 `prebuild.sh` 生成产物,勿直接提交二进制。

+ 7 - 0
entry/src/main/ets/common/enums/RemoteDriveManagerStates.ets

@@ -34,4 +34,11 @@ export enum RemoteDriveManagerStates{
   ChangeUploadQueue = "ChangeUploadQueue",
   ChangeFinishUploadQueue = "ChangeFinishUploadQueue",
   SetIsPauseUpload = "SetIsPauseUpload",
+  UploadStart = "UploadStart",
+  UploadProgress = "UploadProgress",
+  UploadSuccess = "UploadSuccess",
+  UploadFailed = "UploadFailed",
+  UploadPaused = "UploadPaused",
+  UploadResumed = "UploadResumed",
+  UploadCancelled = "UploadCancelled",
 }

+ 29 - 0
entry/src/main/ets/common/enums/UploadTaskStatus.ets

@@ -0,0 +1,29 @@
+/**
+ * 上传任务状态枚举
+ */
+export enum UploadTaskStatus {
+  /**
+   * 等待中 - 任务在队列中等待执行
+   */
+  Pending = 0,
+
+  /**
+   * 上传中 - 任务正在执行上传
+   */
+  Uploading = 1,
+
+  /**
+   * 成功 - 任务上传成功
+   */
+  Success = 2,
+
+  /**
+   * 失败 - 任务上传失败
+   */
+  Failed = 3,
+
+  /**
+   * 已暂停 - 任务被用户暂停
+   */
+  Paused = 4
+}

+ 3 - 3
entry/src/main/ets/common/util/ConfigManager.ets

@@ -49,7 +49,7 @@ export class ConfigManager {
   public static async initConfig(): Promise<boolean> {
     try {
       Logger.info(ConfigManager.TAG, '开始初始化配置...');
-      
+
       const configData = await ConfigManager.fetchConfigFromAPI();
       if (!configData) {
         Logger.error(ConfigManager.TAG, '获取配置数据失败');
@@ -87,7 +87,7 @@ export class ConfigManager {
       if (response.responseCode === 200) {
         const responseData = response.result as string;
         const configResponse: ConfigResponse = JSON.parse(responseData);
-        
+
         if (configResponse.code === 0) {
           Logger.info(ConfigManager.TAG, `成功获取${configResponse.data.length}个配置项`);
           return configResponse.data;
@@ -113,7 +113,7 @@ export class ConfigManager {
     try {
       configData.forEach(config => {
         let processedValue: ConfigValue = config.value;
-        
+
         // 根据类型处理值
         switch (config.type) {
           case 'json':

+ 20 - 0
entry/src/main/ets/common/util/FileManager.ets

@@ -116,6 +116,26 @@ class FileManagerClass {
       throw error;
     }
   }
+
+  // 读取文件内容为ArrayBuffer
+  public async readFileToArrayBuffer(filePath: string): Promise<ArrayBuffer> {
+    try {
+      if (!await this.isExist(filePath)) {
+        throw new Error(`文件不存在: ${filePath}`);
+      }
+      const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
+      const stat = await fileIo.stat(filePath);
+      const buffer = new ArrayBuffer(stat.size);
+      await fileIo.read(file.fd, buffer);
+      fileIo.closeSync(file);
+      Logger.info(TAG, `读取文件成功: ${filePath}, 大小: ${stat.size}`);
+      return buffer;
+    } catch (err) {
+      const error = err as Error;
+      Logger.error(TAG, `读取文件失败: ${error.message}`);
+      throw error;
+    }
+  }
 }
 
 // 合并两个路径的工具函数

+ 426 - 6
entry/src/main/ets/common/util/RcpSocketUtil.ets

@@ -20,6 +20,43 @@ export class RcpSocket {
     console.info(UtilName, 'testTag', 'RcpSocketUtil单例已创建')
   }
 
+  private encodeUrlPath(path?: string): string {
+    if (!path || path.length === 0) {
+      return '/';
+    }
+    let normalized = path.replace(/\\/g, '/');
+    if (!normalized.startsWith('/')) {
+      normalized = `/${normalized}`;
+    }
+    normalized = normalized.replace(/\/+/g, '/');
+    const segments = normalized.split('/').map((segment) => {
+      if (!segment || segment.length === 0) {
+        return '';
+      }
+      let decoded = segment;
+      try {
+        decoded = decodeURIComponent(segment);
+      } catch (_err) {
+        // ignore decode errors and keep raw segment
+      }
+      return encodeURIComponent(decoded);
+    });
+    let encodedPath = segments.join('/');
+    if (!encodedPath.startsWith('/')) {
+      encodedPath = `/${encodedPath}`;
+    }
+    if (encodedPath.length === 0) {
+      encodedPath = '/';
+    }
+    return encodedPath;
+  }
+
+  private buildRequestUrl(host: string, port: number, path: string, enableHttps: boolean): string {
+    const protocol = enableHttps ? "https" : "http";
+    const encodedPath = this.encodeUrlPath(path);
+    return `${protocol}://${host}:${port}${encodedPath}`;
+  }
+
   static getInstance(): RcpSocket {
     if (!RcpSocket.instance) {
       RcpSocket.instance = new RcpSocket();
@@ -30,7 +67,7 @@ export class RcpSocket {
   public RcpSendHead(host: string, port: number, account: string, password: string, path: string,
     enableHttps: boolean): Promise<number> {
     return new Promise<number>((resolve, reject) => {
-      const url = `${enableHttps ? "https" : "http"}://${host}:${port}${path}`;
+      const url = this.buildRequestUrl(host, port, path, enableHttps);
       const timeoutDuration: number = 10000;
       const speedThreshold: number = 5000; // 设置速度测试的时间阈值
       console.info(UtilName, 'testTag', '发送HEAD的url:' + url)
@@ -110,7 +147,7 @@ export class RcpSocket {
   public RcpSendDelete(host: string, port: number, account: string, password: string, path: string,
     enableHttps: boolean): Promise<void> {
     return new Promise<void>((resolve, reject) => {
-      const url = `${enableHttps ? "https" : "http"}://${host}:${port}${path}`;
+      const url = this.buildRequestUrl(host, port, path, enableHttps);
       const timeoutDuration: number = 10000;
       console.info(UtilName, 'testTag', '发送Delete的url:' + url)
       // 创建 RCP 会话配置
@@ -170,11 +207,52 @@ 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) => {
-      const url = `${enableHttps ? "https" : "http"}://${host}:${port}${path}`;
-      const destinationUrl = `${enableHttps ? "https" : "http"}://${host}:${port}${newPath}`
+      const url = this.buildRequestUrl(host, port, path, enableHttps);
+      const destinationUrl = this.buildRequestUrl(host, port, newPath, enableHttps)
       const timeoutDuration: number = 10000;
       console.info(UtilName, 'testTag', '发送MOVE的url:' + url)
       // 创建 RCP 会话配置
@@ -245,7 +323,7 @@ export class RcpSocket {
     enableHttps: boolean
   ): Promise<FileInfo[]> {
     return new Promise(async (resolve, reject) => {
-      const url = `${enableHttps ? "https" : "http"}://${host}:${port.toString()}${path}`;
+      const url = this.buildRequestUrl(host, port, path, enableHttps);
       const timeoutDuration: number = 10000;
       console.info(UtilName, 'testTag', '发送PROPFIND请求的url:' + url)
       // 创建 RCP 会话配置
@@ -373,7 +451,7 @@ export class RcpSocket {
     cachePath: string
   ): Promise<FileInfo[]> {
     return new Promise(async (resolve, reject) => {
-      const url = `${enableHttps ? "https" : "http"}://${host}:${port.toString()}${path}`;
+      const url = this.buildRequestUrl(host, port, path, enableHttps);
       const timeoutDuration: number = 10000;
       console.info(UtilName, 'testTag', '发送PROPFIND递归请求的url:' + url)
       let cacheFileInfos_str: string = ''
@@ -714,4 +792,346 @@ export class RcpSocket {
 
     return result;
   }
+
+  /**
+   * 上传文件到WebDAV服务器
+   * @param localPath 本地文件路径
+   * @param remotePath 远程文件路径
+   * @param host 主机地址
+   * @param port 端口
+   * @param account 账户名
+   * @param password 密码
+   * @param enableHttps 是否启用HTTPS
+   * @param onProgress 进度回调
+   * @param maxRetries 最大重试次数,默认3次
+   */
+  public async uploadFile(
+    localPath: string,
+    remotePath: string,
+    host: string,
+    port: number,
+    account: string,
+    password: string,
+    enableHttps: boolean,
+    onProgress?: (uploaded: number, total: number) => void,
+    maxRetries: number = 3
+  ): Promise<void> {
+    let retryCount = 0;
+    let lastError: BusinessError | null = null;
+
+    // 详细日志:上传开始
+    console.info(UtilName, 'testTag', '========== RcpSocket上传开始 ==========');
+    console.info(UtilName, 'testTag', `本地路径: ${localPath}`);
+    console.info(UtilName, 'testTag', `远程路径: ${remotePath}`);
+    console.info(UtilName, 'testTag', `目标主机: ${host}:${port}`);
+    console.info(UtilName, 'testTag', `使用HTTPS: ${enableHttps}`);
+    console.info(UtilName, 'testTag', `最大重试次数: ${maxRetries}`);
+
+    while (retryCount <= maxRetries) {
+      try {
+        if (retryCount > 0) {
+          console.info(UtilName, 'testTag', `第${retryCount}次重试上传...`);
+        }
+        
+        await this.uploadFileInternal(
+          localPath,
+          remotePath,
+          host,
+          port,
+          account,
+          password,
+          enableHttps,
+          onProgress
+        );
+        
+        console.info(UtilName, 'testTag', '========== RcpSocket上传成功 ==========');
+        console.info(UtilName, 'testTag', `文件: ${remotePath}`);
+        console.info(UtilName, 'testTag', `重试次数: ${retryCount}`);
+        console.info(UtilName, 'testTag', '==========================================');
+        return;
+      } catch (err) {
+        lastError = err as BusinessError;
+        retryCount++;
+        
+        // 详细错误日志
+        console.error(UtilName, 'testTag', '---------- 上传失败 ----------');
+        console.error(UtilName, 'testTag', `文件: ${remotePath}`);
+        console.error(UtilName, 'testTag', `错误码: ${lastError.code}`);
+        console.error(UtilName, 'testTag', `错误信息: ${lastError.message}`);
+        console.error(UtilName, 'testTag', `当前重试次数: ${retryCount}/${maxRetries}`);
+        
+        if (retryCount <= maxRetries) {
+          const delayMs = Math.min(1000 * Math.pow(2, retryCount - 1), 10000);
+          console.warn(UtilName, 'testTag', `将在${delayMs}ms后重试...`);
+          // 等待一段时间后重试,使用指数退避策略
+          await this.delay(delayMs);
+        } else {
+          console.error(UtilName, 'testTag', '已达到最大重试次数,放弃上传');
+        }
+      }
+    }
+
+    // 所有重试都失败
+    console.error(UtilName, 'testTag', '========== RcpSocket上传失败 ==========');
+    console.error(UtilName, 'testTag', `文件: ${remotePath}`);
+    console.error(UtilName, 'testTag', `已重试: ${maxRetries}次`);
+    console.error(UtilName, 'testTag', `最终错误: ${lastError?.message || '未知错误'}`);
+    console.error(UtilName, 'testTag', '==========================================');
+    
+    if (lastError) {
+      const error = new Error(lastError.message);
+      throw error;
+    }
+  }
+
+  /**
+   * 内部上传文件实现
+   */
+  private async uploadFileInternal(
+    localPath: string,
+    remotePath: string,
+    host: string,
+    port: number,
+    account: string,
+    password: string,
+    enableHttps: boolean,
+    onProgress?: (uploaded: number, total: number) => void
+  ): Promise<void> {
+    return new Promise<void>(async (resolve, reject) => {
+      const url = this.buildRequestUrl(host, port, remotePath, enableHttps);
+      const timeoutDuration: number = 120000; // 上传超时时间设置为120秒
+      console.info(UtilName, 'testTag', '开始上传文件到:', url);
+
+      let rcpSession: rcp.Session | null = null;
+
+      try {
+        // 检查文件是否存在
+        console.info(UtilName, 'testTag', '检查本地文件是否存在...');
+        const fileExists = await FileManager.isExist(localPath);
+        if (!fileExists) {
+          const errorMsg = `本地文件不存在: ${localPath}`;
+          console.error(UtilName, 'testTag', `文件读取错误: ${errorMsg}`);
+          const error = new Error(errorMsg);
+          throw error;
+        }
+        console.info(UtilName, 'testTag', '本地文件存在,继续上传');
+
+        // 获取文件大小
+        console.info(UtilName, 'testTag', '获取文件大小...');
+        const fileSize = await FileManager.getFileSize(localPath);
+        console.info(UtilName, 'testTag', `文件大小: ${fileSize} 字节 (${(fileSize / 1024 / 1024).toFixed(2)} MB)`);
+
+        if (fileSize === 0) {
+          const errorMsg = `文件大小为0: ${localPath}`;
+          console.error(UtilName, 'testTag', `文件读取错误: ${errorMsg}`);
+          const error = new Error(errorMsg);
+          throw error;
+        }
+
+        // 流式读取文件内容
+        console.info(UtilName, 'testTag', '读取文件内容...');
+        const fileContent = await this.readFileStream(localPath, fileSize);
+        console.info(UtilName, 'testTag', '文件内容读取完成');
+
+        // 创建 RCP 会话配置
+        let uploadedSize = 0;
+        let lastProgressTime = Date.now();
+        const progressThrottle = 500; // 进度更新节流,每500ms更新一次
+        let progressCallCount = 0; // 进度回调计数
+
+        const customHttpEventsHandler: rcp.HttpEventsHandler = {
+          onDataReceive: async (incomingData: ArrayBuffer) => {
+            // 上传响应数据接收
+            uploadedSize += incomingData.byteLength;
+            progressCallCount++;
+            const currentTime = Date.now();
+            const timeSinceLastUpdate = currentTime - lastProgressTime;
+            const isComplete = uploadedSize >= fileSize;
+            
+            // 节流策略:
+            // 1. 时间间隔超过阈值
+            // 2. 上传完成
+            // 3. 每100次回调强制更新一次(防止长时间无更新)
+            const shouldUpdate = timeSinceLastUpdate >= progressThrottle || 
+                                isComplete || 
+                                (progressCallCount % 100 === 0);
+            
+            if (onProgress && shouldUpdate) {
+              onProgress(uploadedSize, fileSize);
+              lastProgressTime = currentTime;
+            }
+            
+            // 后台任务更新也进行节流
+            if (timeSinceLastUpdate >= 1000) {
+              await this.backgroundManager.updateDataTransferContinuousTask();
+            }
+          },
+          onDataEnd: () => {
+            console.info(UtilName, 'testTag', '文件数据传输完成');
+            // 确保最后一次进度更新
+            if (onProgress) {
+              onProgress(fileSize, fileSize);
+            }
+          }
+        };
+
+        const tracingConfig: rcp.TracingConfiguration = {
+          verbose: true,
+          infoToCollect: {
+            textual: true,
+            incomingData: true,
+            outgoingData: true,
+          },
+          collectTimeInfo: true,
+          httpEventsHandler: customHttpEventsHandler
+        };
+
+        let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' };
+        let reqCfg: rcp.Configuration = {};
+
+        if (enableHttps) {
+          reqCfg = {
+            security: secCfg,
+            tracing: tracingConfig,
+            transfer: {
+              timeout: {
+                connectMs: timeoutDuration,
+                transferMs: timeoutDuration
+              }
+            }
+          };
+        } else {
+          reqCfg = {
+            tracing: tracingConfig,
+            transfer: {
+              timeout: {
+                connectMs: timeoutDuration,
+                transferMs: timeoutDuration
+              }
+            }
+          };
+        }
+
+        let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg };
+        rcpSession = rcp.createSession(sessionCfg);
+
+        // 构造基本认证头部
+        const encodedCredentials = buffer
+          .from(`${account}:${password}`)
+          .toString("base64");
+
+        const headers: rcp.RequestHeaders = {
+          Authorization: `Basic ${encodedCredentials}`,
+          'Content-Type': 'application/octet-stream',
+          'Content-Length': fileSize.toString()
+        };
+
+        // 创建PUT请求对象
+        const req = new rcp.Request(url, "PUT", headers, fileContent);
+
+        // 发起上传请求
+        console.info(UtilName, 'testTag', '发起HTTP PUT请求...');
+        await rcpSession.fetch(req);
+        
+        console.info(UtilName, 'testTag', '上传请求完成,关闭会话');
+        if (rcpSession) {
+          rcpSession.close();
+        }
+        resolve();
+      } catch (err) {
+        console.error(UtilName, 'testTag', '上传过程中发生错误');
+        if (rcpSession) {
+          console.info(UtilName, 'testTag', '关闭RCP会话');
+          rcpSession.close();
+        }
+        
+        const error = err as BusinessError;
+        
+        // 详细错误分类和日志
+        if (error.code) {
+          const errorCode = error.code.toString();
+          
+          if (errorCode.includes('2300002') || errorCode.includes('2300003')) {
+            // 网络连接错误
+            console.error(UtilName, 'testTag', `网络连接错误: 错误码 ${error.code}`);
+            console.error(UtilName, 'testTag', '可能原因: 网络不可达、主机不可达或连接超时');
+          } else if (errorCode.includes('2300008')) {
+            // DNS解析错误
+            console.error(UtilName, 'testTag', `DNS解析错误: 错误码 ${error.code}`);
+            console.error(UtilName, 'testTag', '可能原因: 主机名无法解析');
+          } else if (errorCode.includes('2300028')) {
+            // 连接超时
+            console.error(UtilName, 'testTag', `连接超时: 错误码 ${error.code}`);
+            console.error(UtilName, 'testTag', '可能原因: 服务器响应缓慢或网络不稳定');
+          } else if (errorCode.includes('401')) {
+            // 认证失败
+            console.error(UtilName, 'testTag', `认证失败: 错误码 ${error.code}`);
+            console.error(UtilName, 'testTag', '可能原因: 用户名或密码错误');
+          } else if (errorCode.includes('403')) {
+            // 权限不足
+            console.error(UtilName, 'testTag', `权限不足: 错误码 ${error.code}`);
+            console.error(UtilName, 'testTag', '可能原因: 没有写入权限');
+          } else if (errorCode.includes('404')) {
+            // 路径不存在
+            console.error(UtilName, 'testTag', `路径不存在: 错误码 ${error.code}`);
+            console.error(UtilName, 'testTag', '可能原因: 目标路径不存在');
+          } else if (errorCode.includes('500') || errorCode.includes('503')) {
+            // 服务器错误
+            console.error(UtilName, 'testTag', `服务器错误: 错误码 ${error.code}`);
+            console.error(UtilName, 'testTag', '可能原因: 服务器内部错误或服务不可用');
+          } else if (errorCode.includes('507')) {
+            // 存储空间不足
+            console.error(UtilName, 'testTag', `存储空间不足: 错误码 ${error.code}`);
+            console.error(UtilName, 'testTag', '可能原因: 服务器磁盘空间已满');
+          } else {
+            console.error(UtilName, 'testTag', `未知错误: 错误码 ${error.code}`);
+          }
+        }
+        
+        console.error(UtilName, "testTag", `错误详情: ${JSON.stringify(error)}`);
+        reject(error);
+      }
+    });
+  }
+
+  /**
+   * 流式读取文件
+   * @param filePath 文件路径
+   * @param fileSize 文件大小
+   */
+  private async readFileStream(filePath: string, fileSize: number): Promise<ArrayBuffer> {
+    try {
+      // 性能优化:根据文件大小选择合适的读取策略
+      const LARGE_FILE_THRESHOLD = 50 * 1024 * 1024; // 50MB阈值
+      
+      if (fileSize > LARGE_FILE_THRESHOLD) {
+        // 大文件:使用流式读取避免内存溢出
+        console.info(UtilName, 'testTag', `使用流式读取大文件 (${(fileSize / 1024 / 1024).toFixed(2)} MB)`);
+        // 注意:当前实现仍使用FileManager.readFileToArrayBuffer
+        // 在实际生产环境中,应该实现真正的分块流式读取
+        // 这里保留接口以便未来扩展
+        return await FileManager.readFileToArrayBuffer(filePath);
+      } else {
+        // 小文件:直接读取到内存
+        console.info(UtilName, 'testTag', `直接读取小文件 (${(fileSize / 1024).toFixed(2)} KB)`);
+        return await FileManager.readFileToArrayBuffer(filePath);
+      }
+    } catch (err) {
+      const error = err as Error;
+      console.error(UtilName, 'testTag', `读取文件失败: ${error.message}`);
+      throw error;
+    }
+  }
+
+  /**
+   * 延迟函数
+   * @param ms 延迟毫秒数
+   */
+  private delay(ms: number): Promise<void> {
+    return new Promise<void>((resolve) => {
+      setTimeout(() => {
+        resolve();
+      }, ms);
+    });
+  }
 }

+ 709 - 17
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -2,6 +2,7 @@
 import { VideoItem } from '../../viewmodel/VideoItem';
 import { common } from '@kit.AbilityKit';
 import { FileInfo } from '../../viewmodel/FileInfo';
+import { connection } from '@kit.NetworkKit';
 import FileManager, { merge2paths } from './FileManager';
 import { BusinessError } from '@kit.BasicServicesKit';
 import { RemoteDriveManagerStates } from '../enums/RemoteDriveManagerStates';
@@ -13,7 +14,7 @@ import { Constants } from '../../Constants';
 import Logger from './Logger';
 import { buffer } from '@kit.ArkTS';
 import { CommonConstants } from '../constants/CommonConstants';
-import { GlobalContext } from '@pura/harmony-utils';
+import { GlobalContext, PreferencesUtil } from '@pura/harmony-utils';
 import { MusicInfo, parseMusicFileName, Utility } from './Utility';
 import { RemoteDriveType } from '../enums/RemoteDriveType';
 import { listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
@@ -45,6 +46,8 @@ export interface StreamAuthInfo {
 export interface TransferTask {
   song: VideoItem;
   account: WebDavAccount;
+  retryCount?: number; // 已重试次数
+  customUploadPath?: string; // 自定义上传路径(可选)
 }
 
 @Observed
@@ -87,13 +90,22 @@ export class RemoteDriveManager {
   // 上传队列
   public uploadQueue: TransferTask[] = [];
   public finishUploadQueue: TransferTask[] = [];
+  public isProcessingUploadQueue: boolean = false;
+  public currentUploadTask: TransferTask | null = null;
+  public isPauseUpload: boolean = true;
+  public uploadReceivedSize: number = 0;
+  public uploadTotalSize: number = 0;
+  
+  // 性能优化相关
+  // 限制队列大小避免内存溢出:每个任务包含VideoItem对象,200个任务约占用几MB内存
+  private readonly MAX_UPLOAD_QUEUE_SIZE: number = 200; // 最大上传队列大小
+  // 进度更新节流:避免频繁UI刷新导致性能下降,每500ms最多更新一次
+  private lastProgressNotifyTime: number = 0; // 上次进度通知时间
+  private readonly PROGRESS_THROTTLE_MS: number = 500; // 进度更新节流时间(毫秒)
 
   private navidromeApi: NavidromeApi = new NavidromeApi();
   private pathDisplayNames: Map<string, string> = new Map();
   private lastAccountId: number | null = null;
-  public isProcessingUploadQueue: boolean = false;
-  public currentUploadTask: TransferTask | null = null;
-  public isPauseUpload: boolean = true;
 
   public currentAccount:WebDavAccount = new WebDavAccount();
 
@@ -817,6 +829,105 @@ export class RemoteDriveManager {
     return normalized || '/';
   }
 
+  private getSongKey(song: VideoItem): string {
+    if (!song) {
+      return '';
+    }
+    if (song.filePath && song.filePath.length > 0) {
+      return song.filePath;
+    }
+    if (song.name && song.name.length > 0) {
+      return song.name;
+    }
+    return '';
+  }
+
+  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) {
@@ -1585,29 +1696,610 @@ export class RemoteDriveManager {
 
   // ==================== 上传队列管理 ====================
 
-  // 添加到上传队列
-  public addToUploadQueue(song: VideoItem, account: WebDavAccount): void {
-    const task: TransferTask = { song, account };
-    this.uploadQueue.push(task);
-    Logger.info(TAG, '添加到上传队列:', song.name);
+  /**
+   * 添加歌曲到上传队列
+   * @param songs 要上传的歌曲列表
+   * @param account 目标WebDAV账户
+   * @param customUploadPath 自定义上传路径(可选,如果不提供则使用账户默认路径)
+   */
+  public addToUploadQueue(songs: VideoItem[], account: WebDavAccount, customUploadPath?: string): void {
+    Logger.info(TAG, '========== 添加上传任务 ==========');
+    Logger.info(TAG, `请求添加: ${songs.length} 个文件`);
+    Logger.info(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
+    Logger.info(TAG, `自定义上传路径: ${customUploadPath || '未指定,使用账户默认路径'}`);
+    Logger.info(TAG, `当前队列: ${this.uploadQueue.length}/${this.MAX_UPLOAD_QUEUE_SIZE}`);
+    
+    // 检查队列大小限制
+    const availableSlots = this.MAX_UPLOAD_QUEUE_SIZE - this.uploadQueue.length;
+    if (availableSlots <= 0) {
+      Logger.warn(TAG, `上传队列已满(最大${this.MAX_UPLOAD_QUEUE_SIZE}个任务),无法添加更多任务`);
+      Logger.warn(TAG, '====================================');
+      return;
+    }
+    
+    // 限制添加数量
+    const songsToAdd = songs.length > availableSlots ? songs.slice(0, availableSlots) : songs;
+    const existingKeys = new Set<string>();
+    for (let i = 0; i < this.uploadQueue.length; i++) {
+      const key = this.getSongKey(this.uploadQueue[i].song);
+      if (key) {
+        existingKeys.add(key);
+      }
+    }
+    const filteredSongs: VideoItem[] = [];
+    for (let i = 0; i < songsToAdd.length; i++) {
+      const song = songsToAdd[i];
+      const key = this.getSongKey(song);
+      if (key && existingKeys.has(key)) {
+        Logger.warn(TAG, `检测到重复任务,已跳过: ${song.name}`);
+        continue;
+      }
+      if (key) {
+        existingKeys.add(key);
+      }
+      filteredSongs.push(song);
+    }
+    
+    Logger.info(TAG, `实际添加: ${filteredSongs.length} 个文件`);
+    
+    for (let i = 0; i < filteredSongs.length; i++) {
+      const song = filteredSongs[i];
+      const task: TransferTask = { 
+        song, 
+        account,
+        customUploadPath: customUploadPath // 保存自定义上传路径
+      };
+      this.uploadQueue.push(task);
+      Logger.info(TAG, `[${i + 1}/${songsToAdd.length}] ${song.name}`);
+    }
+    
+    if (songs.length > songsToAdd.length) {
+      Logger.warn(TAG, `队列空间不足,仅处理前${songsToAdd.length}/${songs.length}个任务`);
+    }
+    
+    Logger.info(TAG, `新队列大小: ${this.uploadQueue.length}/${this.MAX_UPLOAD_QUEUE_SIZE}`);
+    Logger.info(TAG, '====================================');
+    
     this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
+    this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
+  }
+
+  /**
+   * 开始处理上传队列
+   */
+  public async startUploadQueue(): Promise<void> {
+    if (this.isProcessingUploadQueue) {
+      Logger.warn(TAG, '上传队列正在处理中');
+      return;
+    }
+
+    if (this.uploadQueue.length === 0) {
+      Logger.info(TAG, '上传队列为空');
+      return;
+    }
+
+    this.isProcessingUploadQueue = true;
+    this.isPauseUpload = false;
+    Logger.info(TAG, '开始处理上传队列,共', this.uploadQueue.length.toString(), '个任务');
+
+    await this.processNextUploadTask();
+  }
+
+  /**
+   * 处理下一个上传任务
+   */
+  private async processNextUploadTask(): Promise<void> {
+    if (this.isPauseUpload) {
+      Logger.info(TAG, '========== 上传队列已暂停 ==========');
+      Logger.info(TAG, `剩余任务数: ${this.uploadQueue.length}`);
+      Logger.info(TAG, '====================================');
+      this.isProcessingUploadQueue = false;
+      return;
+    }
+
+    if (this.uploadQueue.length === 0) {
+      Logger.info(TAG, '========== 上传队列已完成 ==========');
+      Logger.info(TAG, `成功上传: ${this.finishUploadQueue.length} 个文件`);
+      Logger.info(TAG, '====================================');
+      
+      this.isProcessingUploadQueue = false;
+      this.currentUploadTask = null;
+      this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
+      
+      // 检查是否需要自动清理完成队列
+      const autoClear = PreferencesUtil.getBooleanSync('webdavUploadAutoClear', false);
+      if (autoClear && this.finishUploadQueue.length > 0) {
+        Logger.info(TAG, '自动清理完成队列');
+        this.finishUploadQueue = [];
+        this.notifyObservers(RemoteDriveManagerStates.ChangeFinishUploadQueue);
+      }
+      
+      return;
+    }
+
+    const task = this.uploadQueue[0];
+    this.currentUploadTask = task;
+    
+    Logger.info(TAG, '========== 处理上传队列 ==========');
+    Logger.info(TAG, `当前任务: ${task.song.name}`);
+    Logger.info(TAG, `队列位置: 1/${this.uploadQueue.length}`);
+    Logger.info(TAG, `已完成: ${this.finishUploadQueue.length}`);
+    Logger.info(TAG, '====================================');
+    
+    this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
+
+    try {
+      await this.uploadSingleFile(task);
+      
+      // 上传成功,移到完成队列
+      Logger.info(TAG, `任务成功,移至完成队列: ${task.song.name}`);
+      this.uploadQueue.shift();
+      this.finishUploadQueue.push(task);
+      this.notifyObservers(RemoteDriveManagerStates.UploadSuccess);
+      this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
+      this.notifyObservers(RemoteDriveManagerStates.ChangeFinishUploadQueue);
+
+      // 继续处理下一个任务
+      await this.processNextUploadTask();
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, '---------- 任务失败 ----------');
+      Logger.error(TAG, `文件: ${task.song.name}`);
+      Logger.error(TAG, `错误: ${err.message}`);
+      
+      // 获取重试次数设置
+      const maxRetryCount = PreferencesUtil.getNumberSync('webdavUploadRetryCount', 3);
+      const currentRetryCount = task.retryCount || 0;
+      
+      if (currentRetryCount < maxRetryCount) {
+        // 还可以重试
+        task.retryCount = currentRetryCount + 1;
+        Logger.info(TAG, `准备重试上传 (${task.retryCount}/${maxRetryCount}): ${task.song.name}`);
+        Logger.info(TAG, '将任务移至队列末尾');
+        
+        // 将任务移到队列末尾重试
+        this.uploadQueue.shift();
+        this.uploadQueue.push(task);
+        this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
+      } else {
+        // 已达到最大重试次数,放弃上传
+        Logger.error(TAG, `上传失败,已达到最大重试次数 (${maxRetryCount}): ${task.song.name}`);
+        Logger.error(TAG, '从队列中移除该任务');
+        this.uploadQueue.shift();
+        this.notifyObservers(RemoteDriveManagerStates.UploadFailed);
+        this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
+      }
+      
+      Logger.error(TAG, '------------------------------');
+
+      // 继续处理下一个任务
+      await this.processNextUploadTask();
+    }
   }
 
-  // 从上传队列移除
-  public removeFromUploadQueue(index: number): void {
-    if (index >= 0 && index < this.uploadQueue.length) {
-      const task = this.uploadQueue[index];
+  /**
+   * 暂停上传队列
+   */
+  public pauseUploadQueue(): void {
+    Logger.info(TAG, '========== 暂停上传队列 ==========');
+    
+    if (!this.isProcessingUploadQueue) {
+      Logger.warn(TAG, '上传队列未在处理中,无需暂停');
+      Logger.info(TAG, '====================================');
+      return;
+    }
+
+    this.isPauseUpload = true;
+    Logger.info(TAG, `当前任务: ${this.currentUploadTask?.song.name || '无'}`);
+    Logger.info(TAG, `剩余任务: ${this.uploadQueue.length}`);
+    Logger.info(TAG, '上传队列已暂停');
+    Logger.info(TAG, '====================================');
+    
+    this.notifyObservers(RemoteDriveManagerStates.UploadPaused);
+    this.notifyObservers(RemoteDriveManagerStates.SetIsPauseUpload);
+  }
+
+  /**
+   * 恢复上传队列
+   */
+  public async resumeUploadQueue(): Promise<void> {
+    Logger.info(TAG, '========== 恢复上传队列 ==========');
+    
+    if (!this.isPauseUpload) {
+      Logger.warn(TAG, '上传队列未暂停,无需恢复');
+      Logger.info(TAG, '====================================');
+      return;
+    }
+
+    this.isPauseUpload = false;
+    Logger.info(TAG, `剩余任务: ${this.uploadQueue.length}`);
+    Logger.info(TAG, '上传队列已恢复');
+    Logger.info(TAG, '====================================');
+    
+    this.notifyObservers(RemoteDriveManagerStates.UploadResumed);
+    this.notifyObservers(RemoteDriveManagerStates.SetIsPauseUpload);
+
+    if (!this.isProcessingUploadQueue) {
+      this.isProcessingUploadQueue = true;
+      await this.processNextUploadTask();
+    }
+  }
+
+  /**
+   * 从队列中移除任务
+   * @param task 要移除的任务
+   */
+  public removeFromUploadQueue(task: TransferTask): void {
+    const index = this.uploadQueue.indexOf(task);
+    if (index >= 0) {
+      Logger.info(TAG, '========== 移除上传任务 ==========');
+      Logger.info(TAG, `文件名: ${task.song.name}`);
+      Logger.info(TAG, `队列位置: ${index + 1}/${this.uploadQueue.length}`);
+      
       this.uploadQueue.splice(index, 1);
-      Logger.info(TAG, '从上传队列移除:', task.song.name);
+      
+      Logger.info(TAG, `新队列大小: ${this.uploadQueue.length}`);
+      Logger.info(TAG, '====================================');
+      
       this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
+      this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
+    } else {
+      Logger.warn(TAG, `任务不在队列中: ${task.song.name}`);
     }
   }
-
-  // 清空上传队列
+  
+  /**
+   * 清空上传队列
+   */
   public clearUploadQueue(): void {
+    Logger.info(TAG, '========== 清空上传队列 ==========');
+    Logger.info(TAG, `清空前队列大小: ${this.uploadQueue.length}`);
+    
     this.uploadQueue = [];
-    Logger.info(TAG, '清空上传队列');
+    this.currentUploadTask = null;
+    
+    Logger.info(TAG, '上传队列已清空');
+    Logger.info(TAG, '====================================');
+    
     this.notifyObservers(RemoteDriveManagerStates.UploadQueueChanged);
+    this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
+    this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
+  }
+
+  /**
+   * 检查是否允许在当前网络下上传
+   */
+  private async checkNetworkAllowed(): Promise<boolean> {
+    try {
+      Logger.info(TAG, '检查网络连接状态...');
+      const allowMobile = PreferencesUtil.getBooleanSync('webdavUploadAllowMobile', false);
+      Logger.info(TAG, `用户设置 - 允许移动网络上传: ${allowMobile}`);
+      
+      // 如果允许移动网络上传,直接返回true
+      if (allowMobile) {
+        Logger.info(TAG, '用户已允许移动网络上传,跳过网络类型检查');
+        return true;
+      }
+
+      // 检查当前网络类型
+      Logger.info(TAG, '获取当前网络类型...');
+      const netHandle = await connection.getDefaultNet();
+      const netCapabilities = await connection.getNetCapabilities(netHandle);
+      
+      // 检查是否为移动网络
+      const isCellular = netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_CELLULAR);
+      const isWifi = netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_WIFI);
+      const isEthernet = netCapabilities.bearerTypes.includes(connection.NetBearType.BEARER_ETHERNET);
+      
+      Logger.info(TAG, `网络类型 - 移动网络: ${isCellular}, WiFi: ${isWifi}, 以太网: ${isEthernet}`);
+      
+      if (isCellular) {
+        Logger.warn(TAG, '当前为移动网络,且未允许移动网络上传');
+        Logger.warn(TAG, '请连接WiFi或在设置中允许移动网络上传');
+        return false;
+      }
+      
+      Logger.info(TAG, '网络检查通过,允许上传');
+      return true;
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, '检查网络类型失败:', err.message);
+      Logger.warn(TAG, '网络检查失败,默认允许上传');
+      // 检查失败时默认允许上传
+      return true;
+    }
+  }
+
+  /**
+   * 上传单个文件
+   * @param task 上传任务
+   */
+  private async uploadSingleFile(task: TransferTask): Promise<void> {
+    const song = task.song;
+    const account = task.account;
+    const startTime = Date.now();
+
+    // 详细日志:上传开始
+    Logger.info(TAG, '========== 上传任务开始 ==========');
+    Logger.info(TAG, `文件名: ${song.name}`);
+    Logger.info(TAG, `文件路径: ${song.filePath}`);
+    Logger.info(TAG, `文件大小: ${song.videoSize || 0} 字节`);
+    Logger.info(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
+    Logger.info(TAG, `目标主机: ${account.host}:${account.port}`);
+    Logger.info(TAG, `使用HTTPS: ${account.enableHttps}`);
+    Logger.info(TAG, `重试次数: ${task.retryCount || 0}`);
+    
+    try {
+      // 检查网络是否允许上传
+      Logger.info(TAG, '检查网络连接状态...');
+      const networkAllowed = await this.checkNetworkAllowed();
+      if (!networkAllowed) {
+        const errorMsg = '当前网络不允许上传,请连接WiFi或在设置中允许移动网络上传';
+        Logger.error(TAG, `网络检查失败: ${errorMsg}`);
+        throw new Error(errorMsg);
+      }
+      Logger.info(TAG, '网络检查通过');
+      
+      this.notifyObservers(RemoteDriveManagerStates.UploadStart);
+
+      // 构建远程路径 - 优先使用任务中的自定义路径,否则使用账户默认路径
+      const uploadPath = task.customUploadPath || account.uploadFilePath || '/';
+      const normalizedPath = this.normalizeFullPath(uploadPath);
+      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 || '未配置'}`);
+      Logger.info(TAG, `实际使用路径: ${uploadPath}`);
+      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, '------------------------------');
+
+      // 检查文件是否存在
+      Logger.info(TAG, '检查远程文件是否存在...');
+      const exists = await this.checkFileExists(account, remotePath);
+      if (exists) {
+        // 根据用户配置的重复文件处理方式来处理
+        const duplicateAction = PreferencesUtil.getStringSync('webdavUploadDuplicateAction', 'skip');
+        Logger.info(TAG, `文件已存在,处理方式: ${duplicateAction}`);
+        
+        if (duplicateAction === 'skip') {
+          Logger.warn(TAG, '文件已存在,跳过上传:', remotePath);
+          return;
+        } else if (duplicateAction === 'overwrite') {
+          Logger.warn(TAG, '文件已存在,将覆盖:', remotePath);
+        } else if (duplicateAction === 'rename') {
+          Logger.warn(TAG, '文件已存在,将重命名上传');
+          // 重命名逻辑可以在这里实现
+        }
+      } else {
+        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;
+      await this.rcpSocket.uploadFile(
+        song.filePath,
+        remotePath,
+        host,
+        account.port,
+        account.account,
+        account.password,
+        account.enableHttps,
+        (uploaded: number, total: number) => {
+          this.uploadReceivedSize = uploaded;
+          this.uploadTotalSize = total;
+          
+          // 进度更新节流:只在间隔超过阈值或上传完成时通知
+          const currentTime = Date.now();
+          const shouldNotify = (currentTime - this.lastProgressNotifyTime >= this.PROGRESS_THROTTLE_MS) || 
+                              (uploaded >= total);
+          
+          if (shouldNotify) {
+            this.lastProgressNotifyTime = currentTime;
+            // 详细日志:上传进度
+            const progress = total > 0 ? ((uploaded / total) * 100).toFixed(2) : '0.00';
+            const speedMBps = uploaded > 0 ? (uploaded / (1024 * 1024) / ((currentTime - startTime) / 1000)).toFixed(2) : '0.00';
+            Logger.info(TAG, `上传进度: ${progress}%, 已上传: ${uploaded}/${total} 字节, 速度: ${speedMBps} MB/s`);
+            this.notifyObservers(RemoteDriveManagerStates.UploadProgress);
+          }
+        }
+      );
+
+      // 上传成功
+      const endTime = Date.now();
+      const duration = ((endTime - startTime) / 1000).toFixed(2);
+      const avgSpeed = song.videoSize > 0 ? ((song.videoSize / (1024 * 1024)) / parseFloat(duration)).toFixed(2) : '0.00';
+      
+      Logger.info(TAG, '========== 上传任务成功 ==========');
+      Logger.info(TAG, `文件名: ${song.name}`);
+      Logger.info(TAG, `耗时: ${duration} 秒`);
+      Logger.info(TAG, `平均速度: ${avgSpeed} MB/s`);
+      Logger.info(TAG, `目标路径: ${remotePath}`);
+      Logger.info(TAG, '====================================');
+      
+    } catch (error) {
+      const err = error as Error;
+      const endTime = Date.now();
+      const duration = ((endTime - startTime) / 1000).toFixed(2);
+      
+      // 详细错误日志
+      Logger.error(TAG, '========== 上传任务失败 ==========');
+      Logger.error(TAG, `文件名: ${song.name}`);
+      Logger.error(TAG, `文件路径: ${song.filePath}`);
+      Logger.error(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
+      Logger.error(TAG, `目标主机: ${account.host}:${account.port}`);
+      Logger.error(TAG, `耗时: ${duration} 秒`);
+      Logger.error(TAG, `错误类型: ${this.getErrorType(err)}`);
+      Logger.error(TAG, `错误信息: ${err.message}`);
+      Logger.error(TAG, `错误堆栈: ${err.stack || '无堆栈信息'}`);
+      Logger.error(TAG, `重试次数: ${task.retryCount || 0}`);
+      Logger.error(TAG, '====================================');
+      
+      // 根据错误类型进行分类处理
+      this.handleUploadError(err, task);
+      
+      // 重新抛出错误以便上层处理
+      throw err;
+    }
+  }
+  
+  /**
+   * 获取错误类型
+   * @param error 错误对象
+   * @returns 错误类型字符串
+   */
+  private getErrorType(error: Error): string {
+    const message = error.message.toLowerCase();
+    
+    if (message.includes('network') || message.includes('连接') || message.includes('timeout') || message.includes('超时')) {
+      return '网络错误';
+    } else if (message.includes('auth') || message.includes('认证') || message.includes('401') || message.includes('403')) {
+      return '认证错误';
+    } else if (message.includes('file') || message.includes('文件') || message.includes('not found') || message.includes('不存在')) {
+      return '文件错误';
+    } else if (message.includes('server') || message.includes('服务器') || message.includes('500') || message.includes('503')) {
+      return '服务器错误';
+    } else if (message.includes('space') || message.includes('空间') || message.includes('disk') || message.includes('磁盘')) {
+      return '存储空间错误';
+    } else {
+      return '未知错误';
+    }
+  }
+  
+  /**
+   * 处理上传错误
+   * @param error 错误对象
+   * @param task 上传任务
+   */
+  private handleUploadError(error: Error, task: TransferTask): void {
+    const errorType = this.getErrorType(error);
+    
+    Logger.info(TAG, `处理上传错误,类型: ${errorType}`);
+    
+    // 根据错误类型决定是否自动暂停
+    if (errorType === '网络错误') {
+      Logger.warn(TAG, '检测到网络错误,自动暂停上传队列');
+      this.pauseUploadQueue();
+    } else if (errorType === '认证错误') {
+      Logger.error(TAG, '检测到认证错误,请检查账户密码是否正确');
+      // 认证错误通常不需要暂停整个队列,但应该提示用户
+    } else if (errorType === '服务器错误') {
+      Logger.error(TAG, '检测到服务器错误,服务器可能暂时不可用');
+      // 服务器错误可能是临时的,可以继续尝试其他文件
+    } else if (errorType === '存储空间错误') {
+      Logger.error(TAG, '检测到存储空间不足,暂停上传队列');
+      this.pauseUploadQueue();
+    }
+  }
+
+  /**
+   * 检查文件是否存在
+   * @param account WebDAV账户
+   * @param remotePath 远程路径
+   */
+  private async checkFileExists(account: WebDavAccount, remotePath: string): Promise<boolean> {
+    try {
+      Logger.info(TAG, `检查远程文件是否存在: ${remotePath}`);
+      const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+      const fileSize = await this.rcpSocket.RcpSendHead(
+        host,
+        account.port,
+        account.account,
+        account.password,
+        remotePath,
+        account.enableHttps
+      );
+      
+      const exists = fileSize > 0;
+      Logger.info(TAG, `文件${exists ? '存在' : '不存在'}, 大小: ${fileSize} 字节`);
+      return exists;
+    } catch (error) {
+      const err = error as Error;
+      // HEAD请求失败,说明文件不存在或网络错误
+      Logger.info(TAG, `HEAD请求失败: ${err.message}, 假定文件不存在`);
+      return false;
+    }
+  }
+
+  /**
+   * 处理重复文件
+   * @param account WebDAV账户
+   * @param remotePath 远程路径
+   * @param action 处理动作:'skip' | 'overwrite' | 'rename'
+   */
+  private async handleDuplicateFile(
+    account: WebDavAccount,
+    remotePath: string,
+    action: string
+  ): Promise<string> {
+    if (action === 'skip') {
+      throw new Error('文件已存在,跳过上传');
+    }
+
+    if (action === 'overwrite') {
+      // 删除现有文件
+      const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
+      await this.rcpSocket.RcpSendDelete(
+        host,
+        account.port,
+        account.account,
+        account.password,
+        remotePath,
+        account.enableHttps
+      );
+      return remotePath;
+    }
+
+    if (action === 'rename') {
+      // 生成新文件名
+      const lastDotIndex = remotePath.lastIndexOf('.');
+      const lastSlashIndex = remotePath.lastIndexOf('/');
+      let basePath = '';
+      let fileName = '';
+      let extension = '';
+
+      if (lastSlashIndex >= 0) {
+        basePath = remotePath.substring(0, lastSlashIndex + 1);
+        fileName = remotePath.substring(lastSlashIndex + 1);
+      } else {
+        fileName = remotePath;
+      }
+
+      if (lastDotIndex > lastSlashIndex) {
+        extension = fileName.substring(lastDotIndex);
+        fileName = fileName.substring(0, lastDotIndex - lastSlashIndex - 1);
+      }
+
+      // 尝试添加序号
+      let counter = 1;
+      let newPath = '';
+      let exists = true;
+
+      while (exists && counter < 100) {
+        newPath = `${basePath}${fileName}(${counter})${extension}`;
+        exists = await this.checkFileExists(account, newPath);
+        counter++;
+      }
+
+      if (exists) {
+        throw new Error('无法生成唯一文件名');
+      }
+
+      return newPath;
+    }
+
+    return remotePath;
   }
 
   // ==================== 安全认证方法 ====================

+ 1 - 4
entry/src/main/ets/common/util/ReqPermissionUtil.ets

@@ -25,9 +25,6 @@ class ReqPermission {
   async persistPermission(uri: string): Promise<boolean> {
     try {
       if (canIUse('SystemCapability.FileManagement.AppFileService.FolderAuthorization')) {
-        if(uri.startsWith('file://media/Photo')){
-          uri = new fileUri.FileUri(uri).path
-        }
         console.info('onecold 持久化权限persistPermission uri : ', uri);
         let policyInfo: fileShare.PolicyInfo = {
           uri: uri,
@@ -49,4 +46,4 @@ class ReqPermission {
   }
 }
 
-export default new ReqPermission()
+export default new ReqPermission()

+ 162 - 0
entry/src/main/ets/pages/SettingPage.ets

@@ -30,11 +30,19 @@ export struct SettingPage {
   @State isNoJumpToHome: boolean = false //网盘播放不跳转首页
   @State iconCurrentID: string = 'default';//图标的id
   @State iconArray: Array<Icon> = []
+  @State webdavUploadDuplicateAction: string = 'skip' // WebDAV上传重复文件处理方式
+  @State webdavUploadAutoClear: boolean = false // WebDAV上传完成后自动清理队列
+  @State webdavUploadAllowMobile: boolean = false // WebDAV是否允许移动网络上传
+  @State webdavUploadRetryCount: number = 3 // WebDAV上传失败自动重试次数
   @StorageProp('isLandscape')  isLandscape: boolean = false;
   @State isCustomizeICONSheet: boolean = false //自定义背景界面
   @StorageProp('topSafeHeight') topSafeHeight: number = 0;
   @State isCopyFileToDownLoad: boolean = false
   static readonly IS_COPYFILE_TO_DOWNLOAD: string = 'isCopyFileToDownLoad';
+  static readonly WEBDAV_UPLOAD_DUPLICATE_ACTION: string = 'webdavUploadDuplicateAction';
+  static readonly WEBDAV_UPLOAD_AUTO_CLEAR: string = 'webdavUploadAutoClear';
+  static readonly WEBDAV_UPLOAD_ALLOW_MOBILE: string = 'webdavUploadAllowMobile';
+  static readonly WEBDAV_UPLOAD_RETRY_COUNT: string = 'webdavUploadRetryCount';
   @State fastForwardSeconds: string = '10'
   @State isShowBackFast: boolean = true//快进快退按钮
   @State isClearingCache: boolean = false
@@ -279,6 +287,10 @@ export struct SettingPage {
     this.fastForwardSeconds  = PreferencesUtil.getStringSync('fastForwardSeconds', '10')
     this.isCopyFileToDownLoad = PreferencesUtil.getBooleanSync(SettingPage.IS_COPYFILE_TO_DOWNLOAD, false)
     this.isNoJumpToHome = PreferencesUtil.getBooleanSync('isNoJumpToHome', true)
+    this.webdavUploadDuplicateAction = PreferencesUtil.getStringSync(SettingPage.WEBDAV_UPLOAD_DUPLICATE_ACTION, 'skip')
+    this.webdavUploadAutoClear = PreferencesUtil.getBooleanSync(SettingPage.WEBDAV_UPLOAD_AUTO_CLEAR, false)
+    this.webdavUploadAllowMobile = PreferencesUtil.getBooleanSync(SettingPage.WEBDAV_UPLOAD_ALLOW_MOBILE, false)
+    this.webdavUploadRetryCount = PreferencesUtil.getNumberSync(SettingPage.WEBDAV_UPLOAD_RETRY_COUNT, 3)
 
     // 只用 themeMode 控制主题
     this.themeMode = PreferencesUtil.getNumberSync(SettingPage.THEME_MODE, 0)
@@ -1635,6 +1647,156 @@ export struct SettingPage {
             .onClick(() => {
               this.handleClearWebDavCache();
             })
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+            
+            // WebDAV上传默认重复文件处理方式
+            Row() {
+              SymbolGlyph($r('sys.symbol.doc'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .margin({ left: 15 })
+              Text('上传重复文件处理')
+                .margin({ left: 8 })
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .fontWeight(480)
+                .layoutWeight(1)
+              Select([
+                { value: '跳过' },
+                { value: '覆盖' },
+                { value: '重命名' }])
+                .font({ size: 15, weight: FontWeight.Medium })
+                .fontColor(Color.Gray)
+                .margin({ right: 18 })
+                .selected(this.webdavUploadDuplicateAction === 'skip' ? 0 : 
+                  this.webdavUploadDuplicateAction === 'overwrite' ? 1 : 2)
+                .value(this.webdavUploadDuplicateAction === 'skip' ? '跳过' : 
+                  this.webdavUploadDuplicateAction === 'overwrite' ? '覆盖' : '重命名')
+                .onSelect((_index: number, text?: string | undefined) => {
+                  if (_index === 0) {
+                    this.webdavUploadDuplicateAction = 'skip';
+                  } else if (_index === 1) {
+                    this.webdavUploadDuplicateAction = 'overwrite';
+                  } else {
+                    this.webdavUploadDuplicateAction = 'rename';
+                  }
+                  PreferencesUtil.put(SettingPage.WEBDAV_UPLOAD_DUPLICATE_ACTION, this.webdavUploadDuplicateAction)
+                })
+            }
+            .height(55)
+            .clickEffect({ level: ClickEffectLevel.HEAVY })
+
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+            
+            // WebDAV上传完成后自动清理队列
+            Row() {
+              SymbolGlyph($r('sys.symbol.trash'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .margin({ left: 15 })
+              Text('上传完成自动清理队列')
+                .margin({ left: 8 })
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .fontWeight(480)
+                .layoutWeight(1)
+              Toggle({ type: ToggleType.Switch, isOn: this.webdavUploadAutoClear })
+                .selectedColor(this.themeColor)
+                .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
+                .switchPointColor(Color.White)
+                .margin({ right: 18 })
+                .onChange((checked: boolean) => {
+                  this.webdavUploadAutoClear = checked;
+                  PreferencesUtil.put(SettingPage.WEBDAV_UPLOAD_AUTO_CLEAR, this.webdavUploadAutoClear)
+                })
+                .width(50)
+                .height(30);
+            }
+            .height(55)
+            .clickEffect({ level: ClickEffectLevel.HEAVY })
+
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+            
+            // WebDAV是否允许移动网络上传
+            Row() {
+              SymbolGlyph($r('sys.symbol.wifi'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .margin({ left: 15 })
+              Text('允许移动网络上传')
+                .margin({ left: 8 })
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .fontWeight(480)
+                .layoutWeight(1)
+              Toggle({ type: ToggleType.Switch, isOn: this.webdavUploadAllowMobile })
+                .selectedColor(this.themeColor)
+                .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
+                .switchPointColor(Color.White)
+                .margin({ right: 18 })
+                .onChange((checked: boolean) => {
+                  this.webdavUploadAllowMobile = checked;
+                  PreferencesUtil.put(SettingPage.WEBDAV_UPLOAD_ALLOW_MOBILE, this.webdavUploadAllowMobile)
+                })
+                .width(50)
+                .height(30);
+            }
+            .height(55)
+            .clickEffect({ level: ClickEffectLevel.HEAVY })
+
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+            
+            // WebDAV上传失败自动重试次数
+            Row() {
+              SymbolGlyph($r('sys.symbol.arrow_clockwise'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .margin({ left: 15 })
+              Text('上传失败重试次数')
+                .margin({ left: 8 })
+                .fontSize(15)
+                .fontColor(Color.Gray)
+                .fontWeight(480)
+                .layoutWeight(1)
+              Select([
+                { value: '0次' },
+                { value: '1次' },
+                { value: '2次' },
+                { value: '3次' },
+                { value: '5次' }])
+                .font({ size: 15, weight: FontWeight.Medium })
+                .fontColor(Color.Gray)
+                .margin({ right: 18 })
+                .selected(this.webdavUploadRetryCount === 0 ? 0 :
+                  this.webdavUploadRetryCount === 1 ? 1 :
+                  this.webdavUploadRetryCount === 2 ? 2 :
+                  this.webdavUploadRetryCount === 3 ? 3 : 4)
+                .value(this.webdavUploadRetryCount === 0 ? '0次' :
+                  this.webdavUploadRetryCount === 1 ? '1次' :
+                  this.webdavUploadRetryCount === 2 ? '2次' :
+                  this.webdavUploadRetryCount === 3 ? '3次' : '5次')
+                .onSelect((_index: number, text?: string | undefined) => {
+                  if (_index === 0) {
+                    this.webdavUploadRetryCount = 0;
+                  } else if (_index === 1) {
+                    this.webdavUploadRetryCount = 1;
+                  } else if (_index === 2) {
+                    this.webdavUploadRetryCount = 2;
+                  } else if (_index === 3) {
+                    this.webdavUploadRetryCount = 3;
+                  } else {
+                    this.webdavUploadRetryCount = 5;
+                  }
+                  PreferencesUtil.put(SettingPage.WEBDAV_UPLOAD_RETRY_COUNT, this.webdavUploadRetryCount)
+                })
+            }
+            .height(55)
+            .clickEffect({ level: ClickEffectLevel.HEAVY })
+
             Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
             // 长歌名滚动
             Row() {

+ 2443 - 0
entry/src/main/ets/pages/UploadMusicPage.ets

@@ -0,0 +1,2443 @@
+import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
+import { WebDavAccount } from '../viewmodel/WebDavAccount';
+import { VideoItem } from '../viewmodel/VideoItem';
+import { FileInfo } from '../viewmodel/FileInfo';
+import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
+import Logger from '../common/util/Logger';
+import { router } from '@kit.ArkUI';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { Constants } from '../Constants';
+import { picker, fileUri } from '@kit.CoreFileKit';
+import { BusinessError } from '@kit.BasicServicesKit';
+import { PreferencesUtil } from '@pura/harmony-utils';
+import { UploadTaskDataSource } from '../viewmodel/UploadTask';
+import { promptAction } from '@kit.ArkUI';
+import fs from '@ohos.file.fs';
+import ReqPermissionUtil from '../common/util/ReqPermissionUtil';
+
+const TAG = 'heanup UploadMusicPage';
+
+interface LocalFileItem {
+  name: string;
+  path: string;
+  isDirectory: boolean;
+  isAudioFile: boolean;
+}
+
+interface LocalAudioFile {
+  fullPath: string;
+  relativePath: string;
+}
+
+@Entry
+@Component
+export struct UploadMusicPage {
+  @StorageProp('topSafeHeight') topSafeHeight: number = 0;
+  @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
+  @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  @StorageProp('isDarkMode') isDarkMode: boolean = false;
+
+  // 页面状态变量
+  @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[] = [];
+  @State duplicateAction: string = 'skip';
+  @State uploadProgress: number = 0;
+  @State uploadSpeed: string = '0 KB/s';
+  @State uploadedCount: number = 0;
+  @State totalUploadCount: number = 0;
+  @State isUploading: boolean = false;
+  @State uploadQueue: VideoItem[] = [];
+  @State finishQueue: VideoItem[] = [];
+  @State currentTask: VideoItem | null = null;
+  @State isShowPathBrowser: boolean = false;
+  @State currentBrowsePath: string = '/';
+  @State browseFolders: FileInfo[] = [];
+  @State isLoadingFolders: boolean = false;
+  @State pendingCount: number = 0;
+  @State finishedCount: number = 0;
+
+  // RemoteDriveManager实例
+  private webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance();
+  
+  // 性能优化:使用LazyDataSource优化队列列表渲染
+  // LazyForEach配合IDataSource可以实现按需加载,避免一次性渲染大量列表项
+  // 对于包含数百个上传任务的队列,可以显著提升UI响应性能
+  private uploadQueueDataSource: UploadTaskDataSource = new UploadTaskDataSource();
+  private finishQueueDataSource: UploadTaskDataSource = new UploadTaskDataSource();
+  
+  // 事件处理器引用
+  private eventHandler: (event: string) => void = (event: string) => {
+    this.handleUploadEvent(event);
+  };
+  private cachedDownloadRoot: string = '';
+  private localBrowseRoot: string = '';
+
+  aboutToAppear(): void {
+    Logger.info(TAG, '页面即将显示');
+
+    // 加载用户设置
+    this.loadUserSettings();
+
+    // 加载WebDAV账户列表
+    this.loadAccounts();
+
+    // 从路由参数获取预选账户ID
+    const params = router.getParams() as Record<string, Object>;
+    if (params && params['accountId']) {
+      const accountId = params['accountId'] as number;
+      this.preselectAccount(accountId);
+    }
+
+    // 订阅RemoteDriveManager的上传事件
+    this.subscribeUploadEvents();
+  }
+
+  /**
+   * 加载用户设置
+   */
+  private loadUserSettings(): void {
+    try {
+      // 加载默认重复文件处理方式
+      this.duplicateAction = PreferencesUtil.getStringSync('webdavUploadDuplicateAction', 'skip');
+      Logger.info(TAG, `加载用户设置 - 重复文件处理: ${this.duplicateAction}`);
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `加载用户设置失败: ${err.message}`);
+    }
+  }
+
+  aboutToDisappear(): void {
+    Logger.info(TAG, '页面即将销毁');
+
+    // 取消订阅事件
+    this.unsubscribeUploadEvents();
+  }
+
+  /**
+   * 加载WebDAV账户列表
+   */
+  private loadAccounts(): void {
+    try {
+      this.accounts = this.webdavManager.getAllWebDavAccounts();
+      Logger.info(TAG, `加载了 ${this.accounts.length} 个WebDAV账户`);
+
+      // 如果没有选中账户且有可用账户,默认选择第一个
+      if (!this.selectedAccount && this.accounts.length > 0) {
+        this.selectedAccount = this.accounts[0];
+        this.applyDefaultUploadPath(this.selectedAccount);
+      }
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `加载账户列表失败: ${err.message}`);
+    }
+  }
+
+  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}`);
+    }
+    return '';
+  }
+
+  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(force: boolean = false): 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);
+          await ReqPermissionUtil.persistPermission(documentSaveResult[0]);
+          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;
+    }
+    promptAction.showToast({
+      message: '请先在本地音乐页授权下载目录后再使用此功能',
+      duration: 2000
+    });
+    throw new Error('未授权任何本地目录');
+  }
+
+  private normalizeBrowsePath(path: string): string {
+    if (!path || path.trim().length === 0) {
+      return '/';
+    }
+    let normalized = path.trim().replace(/\\/g, '/');
+    if (!normalized.startsWith('/')) {
+      normalized = `/${normalized}`;
+    }
+    normalized = normalized.replace(/\/+/g, '/');
+    if (normalized.length > 1 && normalized.endsWith('/')) {
+      normalized = normalized.slice(0, -1);
+    }
+    return normalized || '/';
+  }
+
+  /**
+   * 预选指定账户
+   * @param accountId 账户ID
+   */
+  private preselectAccount(accountId: number): void {
+    try {
+      for (let i = 0; i < this.accounts.length; i++) {
+        const account = this.accounts[i];
+        if (account.id === accountId) {
+          this.selectedAccount = account;
+          this.applyDefaultUploadPath(account);
+          Logger.info(TAG, `预选账户: ${account.name}`);
+          break;
+        }
+      }
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `预选账户失败: ${err.message}`);
+    }
+  }
+
+  /**
+   * 订阅RemoteDriveManager的上传事件
+   */
+  private subscribeUploadEvents(): void {
+    this.webdavManager.subscribe(this.eventHandler);
+    Logger.info(TAG, '已订阅上传事件');
+  }
+
+  /**
+   * 取消订阅RemoteDriveManager的上传事件
+   */
+  private unsubscribeUploadEvents(): void {
+    this.webdavManager.unsubscribe(this.eventHandler);
+    Logger.info(TAG, '已取消订阅上传事件');
+  }
+
+  /**
+   * 处理上传事件
+   * @param event 事件类型
+   */
+  private handleUploadEvent(event: string): void {
+    Logger.info(TAG, `收到上传事件: ${event}`);
+
+    switch (event) {
+      case RemoteDriveManagerStates.UploadStart:
+        this.handleUploadStart();
+        break;
+      case RemoteDriveManagerStates.UploadProgress:
+        this.handleUploadProgress();
+        break;
+      case RemoteDriveManagerStates.UploadSuccess:
+        this.handleUploadSuccess();
+        break;
+      case RemoteDriveManagerStates.UploadFailed:
+        this.handleUploadFailed();
+        break;
+      case RemoteDriveManagerStates.UploadPaused:
+        this.handleUploadPaused();
+        break;
+      case RemoteDriveManagerStates.UploadResumed:
+        this.handleUploadResumed();
+        break;
+      case RemoteDriveManagerStates.SetCurrentUploadTask:
+        this.handleCurrentTaskChanged();
+        break;
+      case RemoteDriveManagerStates.ChangeUploadQueue:
+        this.handleUploadQueueChanged();
+        break;
+      case RemoteDriveManagerStates.ChangeFinishUploadQueue:
+        this.handleFinishQueueChanged();
+        break;
+    }
+  }
+
+  /**
+   * 处理上传开始事件
+   */
+  private handleUploadStart(): void {
+    this.isUploading = true;
+    Logger.info(TAG, '上传开始');
+  }
+
+  /**
+   * 处理上传进度更新事件
+   */
+  private handleUploadProgress(): void {
+    const uploaded = this.webdavManager.uploadReceivedSize;
+    const total = this.webdavManager.uploadTotalSize;
+
+    if (total > 0) {
+      this.uploadProgress = Math.floor((uploaded / total) * 100);
+
+      // 计算上传速度(简化版,实际应该基于时间差计算)
+      const speedMBps = uploaded / (1024 * 1024);
+      this.uploadSpeed = `${speedMBps.toFixed(2)} MB/s`;
+    }
+
+    Logger.info(TAG, `上传进度: ${this.uploadProgress}%`);
+  }
+
+  /**
+   * 处理上传成功事件
+   */
+  private handleUploadSuccess(): void {
+    this.uploadedCount++;
+    Logger.info(TAG, `上传成功,已完成: ${this.uploadedCount}/${this.totalUploadCount}`);
+  }
+
+  /**
+   * 处理上传失败事件
+   */
+  private handleUploadFailed(): void {
+    Logger.error(TAG, '上传失败');
+  }
+
+  /**
+   * 处理上传暂停事件
+   */
+  private handleUploadPaused(): void {
+    Logger.info(TAG, '上传已暂停');
+  }
+
+  /**
+   * 处理上传恢复事件
+   */
+  private handleUploadResumed(): void {
+    Logger.info(TAG, '上传已恢复');
+  }
+
+  /**
+   * 处理当前任务变更事件
+   */
+  private handleCurrentTaskChanged(): void {
+    const task = this.webdavManager.currentUploadTask;
+    if (task) {
+      this.currentTask = task.song;
+      Logger.info(TAG, `当前上传任务: ${task.song.name}`);
+    } else {
+      this.currentTask = null;
+    }
+  }
+
+  /**
+   * 处理上传队列变更事件
+   */
+  private handleUploadQueueChanged(): void {
+    const tasks = this.webdavManager.uploadQueue;
+    this.uploadQueue = [];
+    for (let i = 0; i < tasks.length; i++) {
+      this.uploadQueue.push(tasks[i].song);
+    }
+    
+    this.pendingCount = this.uploadQueue.length;
+    
+    if (this.pendingCount === 0) {
+      this.isUploading = false;
+      this.currentTask = null;
+      this.uploadProgress = 0;
+      this.uploadSpeed = '0 KB/s';
+    }
+    
+    this.uploadQueueDataSource.updateTasks(this.uploadQueue);
+    
+    Logger.info(TAG, `上传队列更新,当前数量: ${this.pendingCount}`);
+  }
+
+  /**
+   * 处理完成队列变更事件
+   */
+  private handleFinishQueueChanged(): void {
+    const tasks = this.webdavManager.finishUploadQueue;
+    this.finishQueue = [];
+    for (let i = 0; i < tasks.length; i++) {
+      this.finishQueue.push(tasks[i].song);
+    }
+    
+    this.finishedCount = this.finishQueue.length;
+    
+    this.finishQueueDataSource.updateTasks(this.finishQueue);
+    
+    Logger.info(TAG, `完成队列更新,当前数量: ${this.finishedCount}`);
+  }
+
+  /**
+   * 打开文件选择器
+   */
+  private async openFilePicker(): Promise<void> {
+    try {
+      const documentSelectOptions = new picker.DocumentSelectOptions();
+      
+      // 设置音频文件过滤器
+      documentSelectOptions.fileSuffixFilters = Constants.AUDIO_EXTENSIONS;
+      
+      // 支持多选
+      documentSelectOptions.maxSelectNumber = 100;
+
+      const documentPicker = new picker.DocumentViewPicker();
+      
+      const documentSelectResult = await documentPicker.select(documentSelectOptions);
+      
+      if (documentSelectResult && documentSelectResult.length > 0) {
+        for (let i = 0; i < documentSelectResult.length; i++) {
+          try {
+            await ReqPermissionUtil.persistPermission(documentSelectResult[i]);
+          } catch (error) {
+            const err = error as Error;
+            Logger.error(TAG, `持久化授权失败: ${err.message}`);
+          }
+        }
+        Logger.info(TAG, `选择了 ${documentSelectResult.length} 个文件`);
+        
+        // 保存选中的文件URI
+        this.selectedFiles = documentSelectResult;
+        
+        // 提取文件名用于显示
+        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');
+          }
+        }
+        
+        Logger.info(TAG, `文件列表: ${JSON.stringify(this.selectedFileNames)}`);
+      }
+    } catch (error) {
+      const err = error as BusinessError;
+      Logger.error(TAG, `文件选择失败: ${err.message}`);
+    }
+  }
+
+  /**
+   * 打开文件浏览器
+   */
+  private async openFileBrowser(): Promise<void> {
+    try {
+      const initialPath = await this.resolveInitialBrowserPath();
+      if (!initialPath) {
+        promptAction.showToast({
+          message: '无法定位到下载目录,请授予文件访问权限',
+          duration: 2000
+        });
+        return;
+      }
+
+      this.fileBrowserPath = initialPath;
+      this.localBrowseRoot = initialPath;
+      this.selectedBrowserItems.clear();
+      this.isShowFileBrowser = true;
+      await this.loadLocalFiles(initialPath);
+      Logger.info(TAG, `文件浏览器已定位到: ${initialPath}`);
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `打开文件浏览器失败: ${err.message}`);
+      promptAction.showToast({
+        message: err.message || '打开文件浏览器失败',
+        duration: 2000
+      });
+    }
+  }
+
+  /**
+   * 加载本地文件列表
+   */
+  private async loadLocalFiles(path: string): Promise<void> {
+    try {
+      this.isLoadingFiles = true;
+      this.browserFiles = [];
+      let targetPath = path;
+      if (this.localBrowseRoot && !targetPath.startsWith(this.localBrowseRoot)) {
+        targetPath = this.localBrowseRoot;
+      }
+      Logger.info(TAG, `加载目录: ${targetPath}`);
+      
+      const files = fs.listFileSync(targetPath);
+      
+      for (let i = 0; i < files.length; i++) {
+        const fileName = files[i];
+        const fullPath = `${targetPath}/${fileName}`;
+        
+        try {
+          const stat = fs.statSync(fullPath);
+          const isDirectory = stat.isDirectory();
+          if (fileName.startsWith('.')) {
+            continue;
+          }
+          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) {
+      return;
+    }
+    if (this.localBrowseRoot && !item.path.startsWith(this.localBrowseRoot)) {
+      promptAction.showToast({
+        message: '没有此目录的访问权限',
+        duration: 2000
+      });
+      return;
+    }
+    this.fileBrowserPath = item.path;
+    await this.loadLocalFiles(item.path);
+  }
+
+  /**
+   * 返回上级目录
+   */
+  private async goBackDirectory(): Promise<void> {
+    if (!this.localBrowseRoot) {
+      return;
+    }
+    if (this.fileBrowserPath === this.localBrowseRoot) {
+      return;
+    }
+    const lastSlash = this.fileBrowserPath.lastIndexOf('/');
+    if (lastSlash > 0) {
+      const parent = this.fileBrowserPath.substring(0, lastSlash);
+      if (!parent.startsWith(this.localBrowseRoot)) {
+        this.fileBrowserPath = this.localBrowseRoot;
+        await this.loadLocalFiles(this.localBrowseRoot);
+        return;
+      }
+      this.fileBrowserPath = parent;
+      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 async confirmFileSelection(): Promise<void> {
+    if (this.selectedBrowserItems.size === 0) {
+      promptAction.showToast({
+        message: '请至少选择一个项目',
+        duration: 2000
+      });
+      return;
+    }
+
+    // 将选中的项目添加到已选列表
+    const selectedItems: LocalFileItem[] = [];
+    const selectedSet = new Set<string>();
+    this.selectedBrowserItems.forEach((path) => {
+      const found = this.browserFiles.find((f) => f.path === path);
+      if (found && !selectedSet.has(found.path)) {
+        selectedSet.add(found.path);
+        selectedItems.push(found);
+      }
+    });
+
+    for (let i = 0; i < selectedItems.length; i++) {
+      const item = selectedItems[i];
+      const uri = `file://${item.path}`;
+      try {
+        await ReqPermissionUtil.persistPermission(uri);
+      } catch (error) {
+        const err = error as Error;
+        Logger.error(TAG, `持久化授权失败: ${err.message}`);
+      }
+      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 = [];
+    this.localBrowseRoot = '';
+  }
+
+  /**
+   * 递归扫描文件夹,获取所有音频文件
+   */
+  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 文件索引
+   */
+  private removeSelectedFile(index: number): void {
+    if (index >= 0 && index < this.selectedFiles.length) {
+      this.selectedFiles.splice(index, 1);
+      this.selectedFileNames.splice(index, 1);
+      this.selectedFileTypes.splice(index, 1);
+      Logger.info(TAG, `移除项目,剩余 ${this.selectedFiles.length} 个`);
+    }
+  }
+
+  /**
+   * 选择上传账户
+   * @param account 选中的账户
+   */
+  private selectUploadAccount(account: WebDavAccount): void {
+    this.selectedAccount = account;
+    this.applyDefaultUploadPath(account);
+    Logger.info(TAG, `选择账户: ${account.name}, 上传路径: ${this.uploadPath}`);
+  }
+
+  private applyDefaultUploadPath(account: WebDavAccount | null): void {
+    if (!account) {
+      return;
+    }
+    const managerAccount = this.webdavManager.currentAccount;
+    const managerPath = this.webdavManager.currentPath;
+    if (
+      managerPath &&
+      managerPath.length > 0 &&
+      managerAccount &&
+      managerAccount.id === account.id
+    ) {
+      this.uploadPath = managerPath;
+    } else {
+      this.uploadPath = account.uploadFilePath || '/';
+    }
+  }
+
+  /**
+   * 打开路径浏览器
+   */
+  private openPathBrowser(): void {
+    if (!this.selectedAccount) {
+      Logger.warn(TAG, '请先选择账户');
+      return;
+    }
+
+    Logger.info(TAG, '========== 打开路径浏览器 ==========');
+    Logger.info(TAG, `当前上传路径: ${this.uploadPath}`);
+    Logger.info(TAG, `选中账户: ${this.selectedAccount.name}`);
+
+    const managerAccount = this.webdavManager.currentAccount;
+    const defaultPath =
+      managerAccount &&
+        managerAccount.id === this.selectedAccount.id &&
+        this.webdavManager.currentPath &&
+        this.webdavManager.currentPath.length > 0
+        ? this.webdavManager.currentPath
+        : this.uploadPath;
+    
+    this.currentBrowsePath = defaultPath;
+    this.isShowPathBrowser = true;
+    
+    Logger.info(TAG, `isShowPathBrowser 设置为: ${this.isShowPathBrowser}`);
+    Logger.info(TAG, '开始加载文件夹列表...');
+    
+    this.loadWebDavFolders(this.currentBrowsePath);
+  }
+
+  /**
+   * 关闭路径浏览器
+   */
+  private closePathBrowser(): void {
+    Logger.info(TAG, '========== 关闭路径浏览器 ==========');
+    this.isShowPathBrowser = false;
+    this.browseFolders = [];
+  }
+
+  /**
+   * 加载WebDAV文件夹列表
+   * @param path 路径
+   */
+  private async loadWebDavFolders(path: string): Promise<void> {
+    if (!this.selectedAccount) {
+      return;
+    }
+
+    try {
+      this.isLoadingFolders = true;
+      Logger.info(TAG, `加载文件夹列表: ${path}`);
+
+      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, path);
+      
+      // 过滤出文件夹
+      const allFiles = this.webdavManager.webDavFiles;
+      this.browseFolders = [];
+      const normalizedCurrent = this.normalizeBrowsePath(this.currentBrowsePath);
+      for (let i = 0; i < allFiles.length; i++) {
+        const file = allFiles[i];
+        if (file.isDirectory) {
+          const folderPath = this.normalizeBrowsePath(file.href);
+          if (folderPath !== normalizedCurrent) {
+            this.browseFolders.push(file);
+          }
+        }
+      }
+
+      Logger.info(TAG, `加载了 ${this.browseFolders.length} 个文件夹`);
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `加载文件夹失败: ${err.message}`);
+    } finally {
+      this.isLoadingFolders = false;
+    }
+  }
+
+  /**
+   * 进入子文件夹
+   * @param folder 文件夹信息
+   */
+  private async enterBrowseFolder(folder: FileInfo): Promise<void> {
+    this.currentBrowsePath = folder.href;
+    await this.loadWebDavFolders(folder.href);
+  }
+
+  /**
+   * 返回上级文件夹
+   */
+  private async goBackBrowseFolder(): Promise<void> {
+    if (this.currentBrowsePath === '/') {
+      return;
+    }
+
+    const lastSlashIndex = this.currentBrowsePath.lastIndexOf('/');
+    if (lastSlashIndex > 0) {
+      this.currentBrowsePath = this.currentBrowsePath.substring(0, lastSlashIndex);
+    } else {
+      this.currentBrowsePath = '/';
+    }
+
+    await this.loadWebDavFolders(this.currentBrowsePath);
+  }
+
+  /**
+   * 确认选择路径
+   */
+  private confirmPathSelection(): void {
+    this.uploadPath = this.currentBrowsePath;
+    Logger.info(TAG, `选择上传路径: ${this.uploadPath}`);
+    this.closePathBrowser();
+  }
+
+  /**
+   * 开始上传
+   */
+  private async startUpload(): Promise<void> {
+    // 验证
+    if (!this.selectedAccount) {
+      Logger.warn(TAG, '请先选择账户');
+      return;
+    }
+
+    if (this.selectedFiles.length === 0) {
+      Logger.warn(TAG, '请先选择文件');
+      return;
+    }
+
+    if (!this.uploadPath) {
+      Logger.warn(TAG, '请先选择上传路径');
+      return;
+    }
+
+    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];
+        const displayName = this.selectedFileNames[i];
+        const itemType = this.selectedFileTypes[i];
+
+        try {
+          const fileUriObj = new fileUri.FileUri(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}`);
+          continue;
+        }
+      }
+
+      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}`);
+    const uniqueSongs = this.deduplicateSongs(songs);
+    this.webdavManager.addToUploadQueue(uniqueSongs, this.selectedAccount, this.uploadPath);
+    this.totalUploadCount = uniqueSongs.length;
+      this.uploadedCount = 0;
+
+      // 开始处理上传队列
+      await this.webdavManager.startUploadQueue();
+
+      Logger.info(TAG, '上传任务已启动');
+      this.selectedFiles = [];
+      this.selectedFileNames = [];
+      this.selectedFileTypes = [];
+      this.pendingCount = this.webdavManager.uploadQueue.length;
+      this.selectedFiles = [];
+      this.selectedFileNames = [];
+      this.selectedFileTypes = [];
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `启动上传失败: ${err.message}`);
+      promptAction.showToast({
+        message: `上传失败: ${err.message}`,
+        duration: 2000
+      });
+    }
+  }
+
+  private deduplicateSongs(songs: VideoItem[]): VideoItem[] {
+    const unique: VideoItem[] = [];
+    const seen = new Set<string>();
+    for (let i = 0; i < songs.length; i++) {
+      const song = songs[i];
+      const key = song.filePath || song.name;
+      if (!key) {
+        continue;
+      }
+      if (seen.has(key)) {
+        Logger.warn(TAG, `跳过重复文件: ${song.name}`);
+        continue;
+      }
+      seen.add(key);
+      unique.push(song);
+    }
+    return unique;
+  }
+
+  /**
+   * 暂停上传
+   */
+  private pauseUpload(): void {
+    this.webdavManager.pauseUploadQueue();
+    Logger.info(TAG, '暂停上传');
+  }
+
+  /**
+   * 恢复上传
+   */
+  private async resumeUpload(): Promise<void> {
+    await this.webdavManager.resumeUploadQueue();
+    Logger.info(TAG, '恢复上传');
+  }
+
+  /**
+   * 取消上传
+   */
+  private cancelUpload(): void {
+    this.webdavManager.pauseUploadQueue();
+    this.webdavManager.clearUploadQueue();
+    this.isUploading = false;
+    this.currentTask = null;
+    Logger.info(TAG, '取消上传');
+  }
+
+  /**
+   * 上传进度显示组件
+   */
+  @Builder
+  UploadProgressView() {
+    if (this.isUploading && this.currentTask) {
+      Column({ space: 16 }) {
+        // 标题行
+        Row({ space: 8 }) {
+          Text('⏫')
+            .fontSize(20)
+          Text('上传中')
+            .fontSize(16)
+            .fontWeight(FontWeight.Medium)
+            .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+        }
+        .alignSelf(ItemAlign.Start)
+
+        // 当前文件信息卡片
+        Column({ space: 10 }) {
+          Row({ space: 8 }) {
+            Text('🎵')
+              .fontSize(18)
+            Text(this.currentTask.name)
+              .fontSize(14)
+              .fontWeight(FontWeight.Medium)
+              .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+              .layoutWeight(1)
+              .maxLines(1)
+              .textOverflow({ overflow: TextOverflow.Ellipsis })
+          }
+          .width('100%')
+
+          // 进度条
+          Progress({ value: this.uploadProgress, total: 100, type: ProgressType.Linear })
+            .width('100%')
+            .color(this.themeColor)
+            .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA')
+            .style({ strokeWidth: 6 })
+
+          // 进度信息行
+          Row() {
+            Text(`${this.uploadProgress}%`)
+              .fontSize(15)
+              .fontWeight(FontWeight.Bold)
+              .fontColor(this.themeColor)
+
+            Blank()
+
+            Text(`${this.uploadSpeed}`)
+              .fontSize(13)
+              .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
+          }
+          .width('100%')
+
+          // 已上传数量
+          Row({ space: 6 }) {
+            Text('✓')
+              .fontSize(14)
+              .fontColor(this.themeColor)
+            Text(`已完成 ${this.uploadedCount}/${this.totalUploadCount}`)
+              .fontSize(13)
+              .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93')
+          }
+        }
+        .width('100%')
+        .padding(14)
+        .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+        .borderRadius(10)
+
+        // 控制按钮
+        Row({ space: 10 }) {
+          Button({ type: ButtonType.Normal }) {
+            Row({ space: 6 }) {
+              Text(this.webdavManager.isPauseUpload ? '▶️' : '⏸️')
+                .fontSize(16)
+              Text(this.webdavManager.isPauseUpload ? '恢复' : '暂停')
+                .fontSize(14)
+                .fontWeight(FontWeight.Medium)
+            }
+          }
+          .height(44)
+          .layoutWeight(1)
+          .backgroundColor(this.themeColor)
+          .fontColor('#FFFFFF')
+          .borderRadius(10)
+          .onClick(() => {
+            if (this.webdavManager.isPauseUpload) {
+              this.resumeUpload();
+            } else {
+              this.pauseUpload();
+            }
+          })
+
+          Button({ type: ButtonType.Normal }) {
+            Row({ space: 6 }) {
+              Text('❌')
+                .fontSize(16)
+              Text('取消')
+                .fontSize(14)
+                .fontWeight(FontWeight.Medium)
+            }
+          }
+          .height(44)
+          .layoutWeight(1)
+          .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30')
+          .fontColor('#FFFFFF')
+          .borderRadius(10)
+          .onClick(() => {
+            this.cancelUpload();
+          })
+        }
+        .width('100%')
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+      .borderRadius(12)
+      .shadow({
+        radius: this.isDarkMode ? 8 : 12,
+        color: this.isDarkMode ? '#0C000000' : '#19000000',
+        offsetX: 0,
+        offsetY: 2
+      })
+      .margin({ bottom: 16 })
+    }
+  }
+
+  /**
+   * 上传队列管理组件(使用LazyDataSource优化性能)
+   */
+  @Builder
+  UploadQueueView() {
+    Column({ space: 16 }) {
+      // 标题行
+      Row({ space: 8 }) {
+        Text('📊')
+          .fontSize(20)
+        Text('任务队列')
+          .fontSize(16)
+          .fontWeight(FontWeight.Medium)
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+      }
+      .alignSelf(ItemAlign.Start)
+
+      Tabs({ barPosition: BarPosition.Start }) {
+        TabContent() {
+          this.QueueList(this.uploadQueueDataSource, 'pending')
+        }
+        .tabBar(this.TabBarBuilder('⏳', '待上传', this.pendingCount))
+
+        TabContent() {
+          this.QueueList(this.finishQueueDataSource, 'finished')
+        }
+        .tabBar(this.TabBarBuilder('✅', '已完成', this.finishedCount))
+      }
+      .width('100%')
+      .height(300)
+      .barMode(BarMode.Fixed)
+      .barBackgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+      .animationDuration(300)
+    }
+    .width('100%')
+    .padding(16)
+    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .borderRadius(12)
+    .shadow({
+      radius: this.isDarkMode ? 8 : 12,
+      color: this.isDarkMode ? '#0C000000' : '#19000000',
+      offsetX: 0,
+      offsetY: 2
+    })
+    .margin({ bottom: 16 })
+  }
+
+  /**
+   * 自定义Tab标签构建器
+   */
+  @Builder
+  TabBarBuilder(icon: string, title: string, count: number) {
+    Row({ space: 6 }) {
+      Text(icon)
+        .fontSize(16)
+      Text(title)
+        .fontSize(14)
+        .fontWeight(FontWeight.Medium)
+      Text(`${count}`)
+        .fontSize(12)
+        .fontWeight(FontWeight.Medium)
+        .fontColor('#FFFFFF')
+        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
+        .backgroundColor(this.themeColor)
+        .borderRadius(8)
+    }
+    .padding({ left: 4, right: 4 })
+  }
+
+  /**
+   * 队列列表组件(使用LazyForEach优化性能)
+   */
+  @Builder
+  QueueList(dataSource: UploadTaskDataSource, type: string) {
+    if (dataSource.totalCount() === 0) {
+      Column() {
+        Text(type === 'pending' ? '暂无待上传任务' : '暂无已完成任务')
+          .fontSize(14)
+          .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+      }
+      .width('100%')
+      .height('100%')
+      .justifyContent(FlexAlign.Center)
+    } else {
+      List({ space: 8 }) {
+        LazyForEach(dataSource, (item: VideoItem, index: number) => {
+          ListItem() {
+            Row({ space: 12 }) {
+              // 文件图标
+              Text('🎵')
+                .fontSize(20)
+
+              Text(item.name)
+                .fontSize(14)
+                .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+                .layoutWeight(1)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+              if (type === 'pending') {
+                Button('移除')
+                  .fontSize(12)
+                  .height(32)
+                  .padding({ left: 12, right: 12 })
+                  .backgroundColor(this.isDarkMode ? '#D94838' : '#E84026')
+                  .fontColor('#FFFFFF')
+                  .borderRadius(6)
+                  .onClick(() => {
+                    // 从队列移除
+                    const tasks = this.webdavManager.uploadQueue;
+                    if (index < tasks.length) {
+                      this.webdavManager.removeFromUploadQueue(tasks[index]);
+                    }
+                  })
+              } else {
+                Text('✓')
+                  .fontSize(20)
+                  .fontColor(this.isDarkMode ? '#5BA854' : '#64BB5C')
+              }
+            }
+            .width('100%')
+            .padding(12)
+            .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
+            .borderRadius(8)
+          }
+        }, (item: VideoItem, index: number) => `${type}-${index}-${item.name}`)
+      }
+      .width('100%')
+      .height('100%')
+      .cachedCount(5) // 缓存5个列表项以提升滚动性能
+      .divider({
+        strokeWidth: 1,
+        color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
+      })
+    }
+  }
+
+  /**
+   * 开始上传按钮组件
+   */
+  @Builder
+  StartUploadButton() {
+    Button({ type: ButtonType.Normal }) {
+      Row({ space: 10 }) {
+        Text('🚀')
+          .fontSize(20)
+        Text('开始上传')
+          .fontSize(16)
+          .fontWeight(FontWeight.Bold)
+      }
+    }
+    .width('100%')
+    .height(54)
+    .backgroundColor(this.themeColor)
+    .fontColor('#FFFFFF')
+    .borderRadius(12)
+    .enabled(!this.isUploading && this.selectedFiles.length > 0 && this.selectedAccount !== null)
+    .opacity((!this.isUploading && this.selectedFiles.length > 0 && this.selectedAccount !== null) ? 1.0 : 0.4)
+    .shadow({
+      radius: 12,
+      color: this.themeColor + '50',
+      offsetX: 0,
+      offsetY: 4
+    })
+    .onClick(() => {
+      this.startUpload();
+    })
+    .margin({ bottom: 16 })
+  }
+
+  /**
+   * 远程配置区域组件
+   */
+  @Builder
+  UploadConfigSection() {
+    Column({ space: 16 }) {
+      // 标题行
+      Row({ space: 8 }) {
+        Text('📁')
+          .fontSize(20)
+        Text('远程配置')
+          .fontSize(16)
+          .fontWeight(FontWeight.Medium)
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+      }
+      .alignSelf(ItemAlign.Start)
+
+      // 上传路径选择卡片
+      Column({ space: 12 }) {
+        Row({ space: 8 }) {
+          Text('远程目录')
+            .fontSize(13)
+            .fontWeight(FontWeight.Medium)
+            .fontColor(this.isDarkMode ? '#99FFFFFF' : '#66000000')
+          
+          Blank()
+          
+          Button('浏览')
+            .fontSize(13)
+            .height(32)
+            .padding({ left: 12, right: 12 })
+            .backgroundColor(this.themeColor)
+            .fontColor('#FFFFFF')
+            .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();
+            })
+        }
+        .width('100%')
+        
+        Row({ space: 8 }) {
+          Text('📂')
+            .fontSize(18)
+          
+          Text(this.uploadPath || '请选择远程目录')
+            .fontSize(15)
+            .fontColor(this.uploadPath ? 
+              (this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') : 
+              (this.isDarkMode ? '#66FFFFFF' : '#99000000'))
+            .fontWeight(this.uploadPath ? FontWeight.Medium : FontWeight.Regular)
+            .layoutWeight(1)
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+        }
+        .width('100%')
+      }
+      .width('100%')
+      .padding(14)
+      .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+      .borderRadius(10)
+      .border({
+        width: 1,
+        color: this.uploadPath ? 
+          (this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30') : 
+          (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
+      })
+
+      // 重复文件处理卡片
+      Column({ space: 12 }) {
+        Text('重复文件处理')
+          .fontSize(13)
+          .fontWeight(FontWeight.Medium)
+          .fontColor(this.isDarkMode ? '#99FFFFFF' : '#66000000')
+          .alignSelf(ItemAlign.Start)
+
+        Column({ space: 10 }) {
+          // 跳过选项
+          Row({ space: 10 }) {
+            Radio({ value: 'skip', group: 'duplicateAction' })
+              .checked(this.duplicateAction === 'skip')
+              .radioStyle({
+                checkedBackgroundColor: this.themeColor,
+                uncheckedBorderColor: this.isDarkMode ? '#4D4D4D' : '#C7C7CC'
+              })
+              .onChange((checked: boolean) => {
+                if (checked) {
+                  this.duplicateAction = 'skip';
+                }
+              })
+            
+            Column({ space: 2 }) {
+              Text('跳过')
+                .fontSize(14)
+                .fontWeight(FontWeight.Medium)
+                .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+              Text('保留远程文件,不上传')
+                .fontSize(12)
+                .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+            }
+            .alignItems(HorizontalAlign.Start)
+            .layoutWeight(1)
+          }
+          .width('100%')
+          .padding(12)
+          .backgroundColor(this.duplicateAction === 'skip' ? 
+            (this.isDarkMode ? '#2E3238' : '#FFFFFF') : 
+            'transparent')
+          .borderRadius(8)
+          .border({
+            width: this.duplicateAction === 'skip' ? 2 : 1,
+            color: this.duplicateAction === 'skip' ? 
+              this.themeColor : 
+              (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
+          })
+          .onClick(() => {
+            this.duplicateAction = 'skip';
+          })
+
+          // 覆盖选项
+          Row({ space: 10 }) {
+            Radio({ value: 'overwrite', group: 'duplicateAction' })
+              .checked(this.duplicateAction === 'overwrite')
+              .radioStyle({
+                checkedBackgroundColor: this.themeColor,
+                uncheckedBorderColor: this.isDarkMode ? '#4D4D4D' : '#C7C7CC'
+              })
+              .onChange((checked: boolean) => {
+                if (checked) {
+                  this.duplicateAction = 'overwrite';
+                }
+              })
+            
+            Column({ space: 2 }) {
+              Text('覆盖')
+                .fontSize(14)
+                .fontWeight(FontWeight.Medium)
+                .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+              Text('替换远程文件')
+                .fontSize(12)
+                .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+            }
+            .alignItems(HorizontalAlign.Start)
+            .layoutWeight(1)
+          }
+          .width('100%')
+          .padding(12)
+          .backgroundColor(this.duplicateAction === 'overwrite' ? 
+            (this.isDarkMode ? '#2E3238' : '#FFFFFF') : 
+            'transparent')
+          .borderRadius(8)
+          .border({
+            width: this.duplicateAction === 'overwrite' ? 2 : 1,
+            color: this.duplicateAction === 'overwrite' ? 
+              this.themeColor : 
+              (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
+          })
+          .onClick(() => {
+            this.duplicateAction = 'overwrite';
+          })
+
+          // 重命名选项
+          Row({ space: 10 }) {
+            Radio({ value: 'rename', group: 'duplicateAction' })
+              .checked(this.duplicateAction === 'rename')
+              .radioStyle({
+                checkedBackgroundColor: this.themeColor,
+                uncheckedBorderColor: this.isDarkMode ? '#4D4D4D' : '#C7C7CC'
+              })
+              .onChange((checked: boolean) => {
+                if (checked) {
+                  this.duplicateAction = 'rename';
+                }
+              })
+            
+            Column({ space: 2 }) {
+              Text('重命名')
+                .fontSize(14)
+                .fontWeight(FontWeight.Medium)
+                .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+              Text('自动重命名后上传')
+                .fontSize(12)
+                .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+            }
+            .alignItems(HorizontalAlign.Start)
+            .layoutWeight(1)
+          }
+          .width('100%')
+          .padding(12)
+          .backgroundColor(this.duplicateAction === 'rename' ? 
+            (this.isDarkMode ? '#2E3238' : '#FFFFFF') : 
+            'transparent')
+          .borderRadius(8)
+          .border({
+            width: this.duplicateAction === 'rename' ? 2 : 1,
+            color: this.duplicateAction === 'rename' ? 
+              this.themeColor : 
+              (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
+          })
+          .onClick(() => {
+            this.duplicateAction = 'rename';
+          })
+        }
+        .width('100%')
+      }
+      .width('100%')
+      .padding(14)
+      .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+      .borderRadius(10)
+    }
+    .width('100%')
+    .padding(16)
+    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .borderRadius(12)
+    .shadow({
+      radius: this.isDarkMode ? 8 : 12,
+      color: this.isDarkMode ? '#0C000000' : '#19000000',
+      offsetX: 0,
+      offsetY: 2
+    })
+    .margin({ bottom: 16 })
+  }
+
+  /**
+   * 路径浏览器对话框
+   */
+  @Builder
+  PathBrowserDialog() {
+    Column() {
+      // 标题栏
+      Row({ space: 12 }) {
+        Text('选择上传路径')
+          .fontSize(18)
+          .fontWeight(FontWeight.Bold)
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+          .layoutWeight(1)
+
+        Button('关闭')
+          .fontSize(14)
+          .height(36)
+          .padding({ left: 16, right: 16 })
+          .backgroundColor(this.isDarkMode ? '#2E3033' : '#E5E5EA')
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+          .borderRadius(8)
+          .onClick(() => {
+            this.closePathBrowser();
+          })
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+
+      // 当前路径显示
+      Row({ space: 12 }) {
+        Column({ space: 4 }) {
+          Text('当前路径')
+            .fontSize(12)
+            .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+          
+          Text(this.currentBrowsePath)
+            .fontSize(14)
+            .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+        }
+        .alignItems(HorizontalAlign.Start)
+        .layoutWeight(1)
+        
+        if (this.currentBrowsePath !== '/') {
+          Button('返回上级')
+            .fontSize(12)
+            .height(36)
+            .padding({ left: 12, right: 12 })
+            .backgroundColor(this.themeColor)
+            .fontColor('#FFFFFF')
+            .borderRadius(8)
+            .onClick(() => {
+              this.goBackBrowseFolder();
+            })
+        }
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
+
+      // 文件夹列表
+      if (this.isLoadingFolders) {
+        Column({ space: 12 }) {
+          LoadingProgress()
+            .width(48)
+            .height(48)
+            .color(this.themeColor)
+          
+          Text('加载中...')
+            .fontSize(14)
+            .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
+        }
+        .width('100%')
+        .height(300)
+        .justifyContent(FlexAlign.Center)
+        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+      } else if (this.browseFolders.length === 0) {
+        Column() {
+          Text('📂')
+            .fontSize(48)
+            .margin({ bottom: 12 })
+          
+          Text('当前目录没有子文件夹')
+            .fontSize(14)
+            .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+        }
+        .width('100%')
+        .height(300)
+        .justifyContent(FlexAlign.Center)
+        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+      } else {
+        List({ space: 8 }) {
+          ForEach(this.browseFolders, (folder: FileInfo, index: number) => {
+            ListItem() {
+              Row({ space: 12 }) {
+                Text('📁')
+                  .fontSize(24)
+
+                Text(folder.fileName)
+                  .fontSize(14)
+                  .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+                  .layoutWeight(1)
+                  .maxLines(1)
+                  .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+                Text('→')
+                  .fontSize(18)
+                  .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+              }
+              .width('100%')
+              .padding(16)
+              .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
+              .borderRadius(8)
+              .onClick(() => {
+                this.enterBrowseFolder(folder);
+              })
+            }
+          }, (folder: FileInfo, index: number) => `folder-${index}-${folder.fileName}`)
+        }
+        .width('100%')
+        .height(300)
+        .padding(16)
+        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+        .divider({
+          strokeWidth: 1,
+          color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
+        })
+      }
+
+      // 底部按钮
+      Row({ space: 12 }) {
+        Button('取消')
+          .fontSize(14)
+          .height(48)
+          .layoutWeight(1)
+          .backgroundColor(this.isDarkMode ? '#2E3033' : '#E5E5EA')
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+          .borderRadius(8)
+          .onClick(() => {
+            this.closePathBrowser();
+          })
+
+        Button('选择此路径')
+          .fontSize(14)
+          .height(48)
+          .layoutWeight(1)
+          .backgroundColor(this.themeColor)
+          .fontColor('#FFFFFF')
+          .borderRadius(8)
+          .onClick(() => {
+            this.confirmPathSelection();
+          })
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    }
+    .width('90%')
+    .constraintSize({ maxWidth: 600 })
+    .backgroundColor(this.isDarkMode ? '#191A1C' : '#FFFFFF')
+    .borderRadius(16)
+    .shadow({
+      radius: 24,
+      color: this.isDarkMode ? '#33000000' : '#19000000',
+      offsetX: 0,
+      offsetY: 8
+    })
+  }
+
+  /**
+   * 账户选择区域组件
+   */
+  @Builder
+  AccountSelectorSection() {
+    Column({ space: 16 }) {
+      // 标题行
+      Row({ space: 8 }) {
+        Text('☁️')
+          .fontSize(20)
+        Text('选择账户')
+          .fontSize(16)
+          .fontWeight(FontWeight.Medium)
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+      }
+      .alignSelf(ItemAlign.Start)
+
+      if (this.accounts.length === 0) {
+        Column({ space: 12 }) {
+          Text('📦')
+            .fontSize(48)
+          
+          Text('暂无WebDAV账户')
+            .fontSize(16)
+            .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
+          
+          Text('请先添加账户')
+            .fontSize(14)
+            .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+        }
+        .width('100%')
+        .padding(32)
+        .justifyContent(FlexAlign.Center)
+      } else {
+        List({ space: 8 }) {
+          ForEach(this.accounts, (account: WebDavAccount, index: number) => {
+            ListItem() {
+              Row({ space: 12 }) {
+                // 账户图标
+                Text('☁️')
+                  .fontSize(24)
+
+                Column({ space: 4 }) {
+                  Text(account.name)
+                    .fontSize(16)
+                    .fontWeight(FontWeight.Medium)
+                    .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F')
+                  
+                  Text(`${account.host}:${account.port}`)
+                    .fontSize(12)
+                    .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93')
+                }
+                .alignItems(HorizontalAlign.Start)
+                .layoutWeight(1)
+
+                if (this.selectedAccount && this.selectedAccount.id === account.id) {
+                  Text('✓')
+                    .fontSize(24)
+                    .fontColor(this.themeColor)
+                }
+              }
+              .width('100%')
+              .padding(14)
+              .backgroundColor(this.selectedAccount && this.selectedAccount.id === account.id
+                ? (this.isDarkMode ? '#2E3238' : '#FFFFFF')
+                : (this.isDarkMode ? '#2E3238' : '#F5F7FA'))
+              .borderRadius(10)
+              .border({
+                width: this.selectedAccount && this.selectedAccount.id === account.id ? 2 : 1,
+                color: this.selectedAccount && this.selectedAccount.id === account.id
+                  ? this.themeColor
+                  : (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA')
+              })
+              .onClick(() => {
+                this.selectUploadAccount(account);
+              })
+            }
+          }, (account: WebDavAccount) => `account-${account.id}`)
+        }
+        .width('100%')
+        .divider({
+          strokeWidth: 1,
+          color: this.isDarkMode ? '#19FFFFFF' : '#0C000000'
+        })
+      }
+    }
+    .width('100%')
+    .padding(16)
+    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .borderRadius(12)
+    .shadow({
+      radius: this.isDarkMode ? 8 : 12,
+      color: this.isDarkMode ? '#0C000000' : '#19000000',
+      offsetX: 0,
+      offsetY: 2
+    })
+    .margin({ bottom: 16 })
+  }
+
+  /**
+   * 文件选择区域组件
+   */
+  @Builder
+  FilePickerSection() {
+    Column({ space: 16 }) {
+      // 标题行
+      Row({ space: 8 }) {
+        Text('🎵')
+          .fontSize(20)
+        Text('选择文件')
+          .fontSize(16)
+          .fontWeight(FontWeight.Medium)
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+      }
+      .alignSelf(ItemAlign.Start)
+
+      // 按钮组
+      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%')
+
+      if (this.selectedFiles.length > 0) {
+        Row({ space: 10 }) {
+          Text('✓')
+            .fontSize(18)
+            .fontColor(this.themeColor)
+          
+          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 ? '#2E3238' : '#F5F7FA')
+        .borderRadius(8)
+        .border({
+          width: 1,
+          color: this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30'
+        })
+      }
+    }
+    .width('100%')
+    .padding(16)
+    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .borderRadius(12)
+    .shadow({
+      radius: this.isDarkMode ? 8 : 12,
+      color: this.isDarkMode ? '#0C000000' : '#19000000',
+      offsetX: 0,
+      offsetY: 2
+    })
+    .margin({ bottom: 16 })
+  }
+
+  /**
+   * 已选文件列表组件
+   */
+  @Builder
+  SelectedFilesList() {
+    Column({ space: 16 }) {
+      // 标题行
+      Row({ space: 8 }) {
+        Text('📋')
+          .fontSize(20)
+        Text('已选文件')
+          .fontSize(16)
+          .fontWeight(FontWeight.Medium)
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+        
+        Blank()
+        
+        Text(`${this.selectedFiles.length}`)
+          .fontSize(13)
+          .fontWeight(FontWeight.Medium)
+          .fontColor('#FFFFFF')
+          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
+          .backgroundColor(this.themeColor)
+          .borderRadius(10)
+      }
+      .width('100%')
+      .alignSelf(ItemAlign.Start)
+
+      List({ space: 8 }) {
+        ForEach(this.selectedFileNames, (fileName: string, index: number) => {
+          ListItem() {
+            Row({ space: 12 }) {
+              // 文件/文件夹图标
+              Text(this.selectedFileTypes[index] === 'folder' ? '📁' : '🎵')
+                .fontSize(20)
+
+              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({ 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)
+            .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA')
+            .borderRadius(8)
+            .border({
+              width: 1,
+              color: this.isDarkMode ? '#19FFFFFF' : '#E5E5EA'
+            })
+          }
+        }, (fileName: string, index: number) => `${index}-${fileName}`)
+      }
+      .width('100%')
+      .constraintSize({ maxHeight: 300 })
+    }
+    .width('100%')
+    .padding(16)
+    .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    .borderRadius(12)
+    .shadow({
+      radius: this.isDarkMode ? 8 : 12,
+      color: this.isDarkMode ? '#0C000000' : '#19000000',
+      offsetX: 0,
+      offsetY: 2
+    })
+    .margin({ bottom: 16 })
+  }
+
+  build() {
+    Column() {
+      // 标题栏
+      Row({ space: 12 }) {
+        Button({ type: ButtonType.Circle }) {
+          Text('←')
+            .fontSize(24)
+            .fontColor('#FFFFFF')
+        }
+        .width(44)
+        .height(44)
+        .backgroundColor(this.themeColor)
+        .shadow({
+          radius: 8,
+          color: this.themeColor + '40',
+          offsetX: 0,
+          offsetY: 2
+        })
+        .onClick(() => {
+          router.back();
+        })
+
+        Text('上传音乐到WebDAV')
+          .fontSize(20)
+          .fontWeight(FontWeight.Bold)
+          .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+          .layoutWeight(1)
+      }
+      .width('100%')
+      .height(56)
+      .padding({ left: 16, right: 16 })
+      .margin({ top: this.topSafeHeight })
+      .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+
+      // 页面内容区域
+      Scroll() {
+        Column({ space: 0 }) {
+          // 账户选择区域
+          this.AccountSelectorSection()
+
+          // 上传配置区域
+          this.UploadConfigSection()
+
+          // 文件选择区域
+          this.FilePickerSection()
+
+          // 已选文件列表
+          if (this.selectedFiles.length > 0) {
+            this.SelectedFilesList()
+          }
+
+          // 开始上传按钮
+          if (!this.isUploading) {
+            this.StartUploadButton()
+          }
+
+          // 上传进度显示
+          this.UploadProgressView()
+
+          // 上传队列管理
+          if (this.uploadQueue.length > 0 || this.finishQueue.length > 0) {
+            this.UploadQueueView()
+          }
+        }
+        .width('100%')
+        .constraintSize({ minHeight: '100%' })
+      }
+      .layoutWeight(1)
+      .width('100%')
+      .padding(16)
+      .scrollBar(BarState.Auto)
+
+      // 底部安全区
+      Row()
+        .height(this.bottomSafeHeight)
+        .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF')
+    }
+    .width('100%')
+    .height('100%')
+    .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5')
+    .bindContentCover(this.isShowFileBrowser, this.FileBrowserDialogBuilder(), {
+      modalTransition: ModalTransition.DEFAULT,
+      onDisappear: () => {
+        Logger.info(TAG, 'FileBrowser onDisappear 回调');
+        this.isShowFileBrowser = false;
+      }
+    })
+  }
+
+  @Builder
+  PathBrowserDialogBuilder() {
+    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%')
+  }
+
+  /**
+   * 文件浏览器对话框
+   */
+  @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%')
+  }
+}

+ 39 - 2
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -13,6 +13,7 @@ import { emitter } from '@kit.BasicServicesKit';
 import { EventConstants } from '../common/constants/EventConstants';
 import { LazyDataSource } from '../common/util/LazyDataSource';
 import { PreferencesUtil, StrUtil } from '@pura/harmony-utils';
+import { DialogHelper } from '@pura/harmony-dialog';
 import { ButtonFancyModifier,
   MenuModifier,
   ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
@@ -671,6 +672,42 @@ export struct WebDavMainPage {
     }
   }
 
+  // 导航到上传页面
+  private navigateToUploadPage(): void {
+    if (!this.selectedAccount || !this.selectedAccount.id) {
+      this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' });
+      return;
+    }
+    
+    Logger.info(TAG, '导航到上传页面,账户ID: ' + this.selectedAccount.id);
+    router.pushUrl({
+      url: 'pages/UploadMusicPage',
+      params: {
+        accountId: this.selectedAccount.id
+      }
+    }).catch((error: Error) => {
+      Logger.error(TAG, '导航到上传页面失败: ' + error.message);
+      this.getUIContext().getPromptAction().showToast({ message: '打开上传页面失败' });
+    });
+  }
+
+  private showQuickActionSheet(): void {
+    DialogHelper.showActionSheetDialog({
+      title: '请选择操作',
+      sheets: [
+        { value: '一键创建歌单', fontColor: $r('app.color.text_color') },
+        { value: '上传到WebDAV', fontColor: $r('app.color.text_color') }
+      ],
+      onAction: (index: number) => {
+        if (index === 0) {
+          this.createPlaylistFromCurrentWebDav();
+        } else if (index === 1) {
+          this.navigateToUploadPage();
+        }
+      }
+    });
+  }
+
   @Builder
   SortMenuBuilder() {
     Menu() {
@@ -890,7 +927,7 @@ export struct WebDavMainPage {
           .bindMenu(this.SortMenuBuilder)
           .attributeModifier(new ShadowModifier())
           .zIndex(0)
-          //添加按钮
+          //添加/上传综合按钮
           Button({ type: ButtonType.Circle, stateEffect: true }) {
             SymbolGlyph($r('sys.symbol.plus'))
               .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
@@ -899,7 +936,7 @@ export struct WebDavMainPage {
           .animation({ duration: 300, curve: Curve.Ease })
           .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
           .onClick(() => {
-            this.createPlaylistFromCurrentWebDav();
+            this.showQuickActionSheet();
           })
           .attributeModifier(new ShadowModifier())
           .zIndex(0)

+ 117 - 0
entry/src/main/ets/viewmodel/UploadTask.ets

@@ -0,0 +1,117 @@
+import { VideoItem } from './VideoItem';
+
+/**
+ * 上传任务状态枚举
+ */
+export enum UploadTaskStatus {
+  Pending = 0,    // 等待中
+  Uploading = 1,  // 上传中
+  Success = 2,    // 成功
+  Failed = 3,     // 失败
+  Paused = 4      // 已暂停
+}
+
+/**
+ * 上传任务数据源(用于LazyForEach优化)
+ */
+export class UploadTaskDataSource implements IDataSource {
+  private tasks: VideoItem[] = [];
+  private listeners: DataChangeListener[] = [];
+
+  public totalCount(): number {
+    return this.tasks.length;
+  }
+
+  public getData(index: number): VideoItem {
+    return this.tasks[index];
+  }
+
+  public registerDataChangeListener(listener: DataChangeListener): void {
+    if (this.listeners.indexOf(listener) < 0) {
+      this.listeners.push(listener);
+    }
+  }
+
+  public unregisterDataChangeListener(listener: DataChangeListener): void {
+    const pos = this.listeners.indexOf(listener);
+    if (pos >= 0) {
+      this.listeners.splice(pos, 1);
+    }
+  }
+
+  /**
+   * 更新数据源
+   * @param tasks 新的任务列表
+   */
+  public updateTasks(tasks: VideoItem[]): void {
+    this.tasks = tasks;
+    this.notifyDataReload();
+  }
+
+  /**
+   * 添加任务
+   * @param task 任务
+   */
+  public addTask(task: VideoItem): void {
+    this.tasks.push(task);
+    this.notifyDataAdd(this.tasks.length - 1);
+  }
+
+  /**
+   * 移除任务
+   * @param index 索引
+   */
+  public removeTask(index: number): void {
+    if (index >= 0 && index < this.tasks.length) {
+      this.tasks.splice(index, 1);
+      this.notifyDataDelete(index);
+    }
+  }
+
+  /**
+   * 清空任务
+   */
+  public clearTasks(): void {
+    this.tasks = [];
+    this.notifyDataReload();
+  }
+
+  /**
+   * 通知数据重新加载
+   */
+  private notifyDataReload(): void {
+    this.listeners.forEach(listener => {
+      listener.onDataReloaded();
+    });
+  }
+
+  /**
+   * 通知数据添加
+   * @param index 索引
+   */
+  private notifyDataAdd(index: number): void {
+    this.listeners.forEach(listener => {
+      listener.onDataAdd(index);
+    });
+  }
+
+  /**
+   * 通知数据删除
+   * @param index 索引
+   */
+  private notifyDataDelete(index: number): void {
+    this.listeners.forEach(listener => {
+      listener.onDataDelete(index);
+    });
+  }
+
+  /**
+   * 通知数据变更
+   * @param index 索引
+   */
+  private notifyDataChange(index: number): void {
+    this.listeners.forEach(listener => {
+      listener.onDataChange(index);
+    });
+  }
+}

+ 4 - 0
entry/src/main/resources/base/element/string.json

@@ -635,6 +635,10 @@
       "name": "FILE_ACCESS_PERSIST_REASON",
       "value": "Save the file selected by the user to avoid repeated operations"
     },
+    {
+      "name": "READ_MEDIA_REASON",
+      "value": "需要读取本地音频文件以便上传到WebDAV服务器"
+    },
     {
       "name": "edit_audio",
       "value": "剪辑音频"

+ 2 - 1
entry/src/main/resources/base/profile/main_pages.json

@@ -8,6 +8,7 @@
     "pages/VipPage",
     "pages/Demo",
     "pages/PlaylistDetailPage",
-    "pages/SmbTestPage"
+    "pages/SmbTestPage",
+    "pages/UploadMusicPage"
   ]
 }

+ 4 - 0
entry/src/main/resources/en_US/element/string.json

@@ -162,6 +162,10 @@
     {
       "name": "reason",
       "value": "Used to initiate network data requests."
+    },
+    {
+      "name": "READ_MEDIA_REASON",
+      "value": "Need to read local audio files to upload to WebDAV server"
     }
   ]
 }