Ver Fonte

新增webdav上传

chendeben há 9 meses atrás
pai
commit
84f408a03d

+ 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
+}

+ 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;
+    }
+  }
 }
 
 // 合并两个路径的工具函数

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

@@ -714,4 +714,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 = `${enableHttps ? "https" : "http"}://${host}:${port}${remotePath}`;
+      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);
+    });
+  }
 }

+ 566 - 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';
@@ -44,6 +45,7 @@ export interface StreamAuthInfo {
 export interface TransferTask {
   song: VideoItem;
   account: WebDavAccount;
+  retryCount?: number; // 已重试次数
 }
 
 @Observed
@@ -86,13 +88,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();
 
@@ -1456,29 +1467,567 @@ 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账户
+   */
+  public addToUploadQueue(songs: VideoItem[], account: WebDavAccount): void {
+    Logger.info(TAG, '========== 添加上传任务 ==========');
+    Logger.info(TAG, `请求添加: ${songs.length} 个文件`);
+    Logger.info(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
+    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;
+    
+    Logger.info(TAG, `实际添加: ${songsToAdd.length} 个文件`);
+    for (let i = 0; i < songsToAdd.length; i++) {
+      const song = songsToAdd[i];
+      const task: TransferTask = { song, account };
+      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 removeFromUploadQueue(index: number): void {
-    if (index >= 0 && index < this.uploadQueue.length) {
-      const task = this.uploadQueue[index];
+  /**
+   * 开始处理上传队列
+   */
+  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 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 = account.uploadFilePath || '/';
+      const normalizedPath = this.normalizeFullPath(uploadPath);
+      const remotePath = `${normalizedPath === '/' ? '' : normalizedPath}/${song.fileName || song.name}`;
+      Logger.info(TAG, `目标路径: ${remotePath}`);
+
+      // 检查文件是否存在
+      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, '远程文件不存在,可以上传');
+      }
+
+      // 执行上传
+      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;
   }
 
   // ==================== 安全认证方法 ====================

+ 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() {

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

@@ -0,0 +1,1382 @@
+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 { picker } from '@kit.CoreFileKit';
+import { BusinessError } from '@kit.BasicServicesKit';
+import { fileUri } from '@kit.CoreFileKit';
+import { PreferencesUtil } from '@pura/harmony-utils';
+import { UploadTaskDataSource } from '../viewmodel/UploadTask';
+
+const TAG = 'UploadMusicPage';
+
+@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 accounts: WebDavAccount[] = [];
+  @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;
+
+  // 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);
+  };
+
+  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.uploadPath = this.selectedAccount.uploadFilePath || '/';
+      }
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `加载账户列表失败: ${err.message}`);
+    }
+  }
+
+  /**
+   * 预选指定账户
+   * @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.uploadPath = account.uploadFilePath || '/';
+          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);
+    }
+    
+    // 更新LazyDataSource
+    this.uploadQueueDataSource.updateTasks(this.uploadQueue);
+    
+    Logger.info(TAG, `上传队列更新,当前数量: ${this.uploadQueue.length}`);
+  }
+
+  /**
+   * 处理完成队列变更事件
+   */
+  private handleFinishQueueChanged(): void {
+    const tasks = this.webdavManager.finishUploadQueue;
+    this.finishQueue = [];
+    for (let i = 0; i < tasks.length; i++) {
+      this.finishQueue.push(tasks[i].song);
+    }
+    
+    // 更新LazyDataSource
+    this.finishQueueDataSource.updateTasks(this.finishQueue);
+    
+    Logger.info(TAG, `完成队列更新,当前数量: ${this.finishQueue.length}`);
+  }
+
+  /**
+   * 打开文件选择器
+   */
+  private async openFilePicker(): Promise<void> {
+    try {
+      const documentSelectOptions = new picker.DocumentSelectOptions();
+      
+      // 设置音频文件过滤器
+      documentSelectOptions.fileSuffixFilters = CommonConstants.AUDIO_EXTENSIONS;
+      
+      // 支持多选
+      documentSelectOptions.maxSelectNumber = 100;
+
+      const documentPicker = new picker.DocumentViewPicker();
+      
+      const documentSelectResult = await documentPicker.select(documentSelectOptions);
+      
+      if (documentSelectResult && documentSelectResult.length > 0) {
+        Logger.info(TAG, `选择了 ${documentSelectResult.length} 个文件`);
+        
+        // 保存选中的文件URI
+        this.selectedFiles = documentSelectResult;
+        
+        // 提取文件名用于显示
+        this.selectedFileNames = [];
+        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);
+          } catch (error) {
+            const err = error as Error;
+            Logger.error(TAG, `解析文件URI失败: ${err.message}`);
+            this.selectedFileNames.push('未知文件');
+          }
+        }
+        
+        Logger.info(TAG, `文件列表: ${JSON.stringify(this.selectedFileNames)}`);
+      }
+    } catch (error) {
+      const err = error as BusinessError;
+      Logger.error(TAG, `文件选择失败: ${err.message}`);
+    }
+  }
+
+  /**
+   * 移除已选文件
+   * @param index 文件索引
+   */
+  private removeSelectedFile(index: number): void {
+    if (index >= 0 && index < this.selectedFiles.length) {
+      this.selectedFiles.splice(index, 1);
+      this.selectedFileNames.splice(index, 1);
+      Logger.info(TAG, `移除文件,剩余 ${this.selectedFiles.length} 个`);
+    }
+  }
+
+  /**
+   * 选择上传账户
+   * @param account 选中的账户
+   */
+  private selectUploadAccount(account: WebDavAccount): void {
+    this.selectedAccount = account;
+    this.uploadPath = account.uploadFilePath || '/';
+    Logger.info(TAG, `选择账户: ${account.name}, 上传路径: ${this.uploadPath}`);
+  }
+
+  /**
+   * 打开路径浏览器
+   */
+  private openPathBrowser(): void {
+    if (!this.selectedAccount) {
+      Logger.warn(TAG, '请先选择账户');
+      return;
+    }
+
+    this.currentBrowsePath = this.uploadPath;
+    this.isShowPathBrowser = true;
+    this.loadWebDavFolders(this.currentBrowsePath);
+  }
+
+  /**
+   * 关闭路径浏览器
+   */
+  private closePathBrowser(): void {
+    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 = [];
+      for (let i = 0; i < allFiles.length; i++) {
+        const file = allFiles[i];
+        if (file.isDirectory) {
+          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, '开始上传任务');
+
+      // 构建VideoItem列表
+      const songs: VideoItem[] = [];
+      for (let i = 0; i < this.selectedFiles.length; i++) {
+        const fileUri = this.selectedFiles[i];
+        const fileName = this.selectedFileNames[i];
+
+        const videoItem = new VideoItem(
+          fileName,
+          fileUri,
+          fileUri,
+          CommonConstants.TYPE_LOCAL,
+          0,
+          '',
+          undefined,
+          undefined,
+          undefined,
+          undefined,
+          undefined,
+          fileName
+        );
+
+        songs.push(videoItem);
+      }
+
+      // 添加到上传队列
+      this.webdavManager.addToUploadQueue(songs, this.selectedAccount);
+      this.totalUploadCount = songs.length;
+      this.uploadedCount = 0;
+
+      // 开始处理上传队列
+      await this.webdavManager.startUploadQueue();
+
+      Logger.info(TAG, '上传任务已启动');
+    } catch (error) {
+      const err = error as Error;
+      Logger.error(TAG, `启动上传失败: ${err.message}`);
+    }
+  }
+
+  /**
+   * 暂停上传
+   */
+  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) {
+      return;
+    }
+
+    Column() {
+      Text('上传进度')
+        .fontSize(16)
+        .fontWeight(FontWeight.Medium)
+        .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+        .margin({ bottom: 12 })
+        .alignSelf(ItemAlign.Start)
+
+      // 当前文件名
+      Text(`当前文件: ${this.currentTask.name}`)
+        .fontSize(14)
+        .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
+        .margin({ bottom: 8 })
+        .maxLines(1)
+        .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+      // 进度条
+      Progress({ value: this.uploadProgress, total: 100, type: ProgressType.Linear })
+        .width('100%')
+        .color(this.themeColor)
+        .backgroundColor(this.isDarkMode ? '#33FFFFFF' : '#19000000')
+        .margin({ bottom: 8 })
+
+      // 进度百分比和速度信息行
+      Row() {
+        Text(`${this.uploadProgress}%`)
+          .fontSize(14)
+          .fontWeight(FontWeight.Medium)
+          .fontColor(this.themeColor)
+          .layoutWeight(1)
+
+        Text(`${this.uploadSpeed}`)
+          .fontSize(14)
+          .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+      }
+      .width('100%')
+      .margin({ bottom: 8 })
+
+      // 已上传数量
+      Text(`已上传: ${this.uploadedCount}/${this.totalUploadCount}`)
+        .fontSize(14)
+        .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
+        .margin({ bottom: 16 })
+
+      // 控制按钮
+      Row({ space: 12 }) {
+        Button(this.webdavManager.isPauseUpload ? '恢复' : '暂停')
+          .fontSize(14)
+          .height(44)
+          .layoutWeight(1)
+          .backgroundColor(this.themeColor)
+          .fontColor('#FFFFFF')
+          .borderRadius(8)
+          .onClick(() => {
+            if (this.webdavManager.isPauseUpload) {
+              this.resumeUpload();
+            } else {
+              this.pauseUpload();
+            }
+          })
+
+        Button('取消')
+          .fontSize(14)
+          .height(44)
+          .layoutWeight(1)
+          .backgroundColor(this.isDarkMode ? '#D94838' : '#E84026')
+          .fontColor('#FFFFFF')
+          .borderRadius(8)
+          .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() {
+      Text('上传队列')
+        .fontSize(16)
+        .fontWeight(FontWeight.Medium)
+        .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+        .margin({ bottom: 12 })
+        .alignSelf(ItemAlign.Start)
+
+      Tabs({ barPosition: BarPosition.Start }) {
+        TabContent() {
+          this.QueueList(this.uploadQueueDataSource, 'pending')
+        }
+        .tabBar(`待上传 (${this.uploadQueue.length})`)
+
+        TabContent() {
+          this.QueueList(this.finishQueueDataSource, 'finished')
+        }
+        .tabBar(`已完成 (${this.finishQueue.length})`)
+      }
+      .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 })
+  }
+
+  /**
+   * 队列列表组件(使用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('开始上传')
+      .width('100%')
+      .height(52)
+      .fontSize(16)
+      .fontWeight(FontWeight.Bold)
+      .fontColor('#FFFFFF')
+      .backgroundColor(this.themeColor)
+      .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.5)
+      .shadow({
+        radius: 12,
+        color: this.themeColor + '40',
+        offsetX: 0,
+        offsetY: 4
+      })
+      .onClick(() => {
+        this.startUpload();
+      })
+      .margin({ bottom: 16 })
+  }
+
+  /**
+   * 上传配置区域组件
+   */
+  @Builder
+  UploadConfigSection() {
+    Column({ space: 12 }) {
+      Text('上传配置')
+        .fontSize(16)
+        .fontWeight(FontWeight.Medium)
+        .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+        .alignSelf(ItemAlign.Start)
+
+      // 上传路径选择
+      Row({ space: 12 }) {
+        Column({ space: 4 }) {
+          Text('上传路径')
+            .fontSize(12)
+            .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+          
+          Text(this.uploadPath || '请选择路径')
+            .fontSize(14)
+            .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+            .maxLines(1)
+            .textOverflow({ overflow: TextOverflow.Ellipsis })
+        }
+        .alignItems(HorizontalAlign.Start)
+        .layoutWeight(1)
+
+        Button('浏览')
+          .fontSize(14)
+          .height(40)
+          .padding({ left: 16, right: 16 })
+          .backgroundColor(this.themeColor)
+          .fontColor('#FFFFFF')
+          .borderRadius(8)
+          .enabled(this.selectedAccount !== null)
+          .opacity(this.selectedAccount !== null ? 1.0 : 0.5)
+          .onClick(() => {
+            this.openPathBrowser();
+          })
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
+      .borderRadius(8)
+
+      // 重复文件处理方式
+      Column({ space: 8 }) {
+        Text('重复文件处理')
+          .fontSize(12)
+          .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+          .alignSelf(ItemAlign.Start)
+
+        Row({ space: 16 }) {
+          Row({ space: 6 }) {
+            Radio({ value: 'skip', group: 'duplicateAction' })
+              .checked(this.duplicateAction === 'skip')
+              .radioStyle({
+                checkedBackgroundColor: this.themeColor
+              })
+              .onChange((checked: boolean) => {
+                if (checked) {
+                  this.duplicateAction = 'skip';
+                }
+              })
+            Text('跳过')
+              .fontSize(14)
+              .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+          }
+
+          Row({ space: 6 }) {
+            Radio({ value: 'overwrite', group: 'duplicateAction' })
+              .checked(this.duplicateAction === 'overwrite')
+              .radioStyle({
+                checkedBackgroundColor: this.themeColor
+              })
+              .onChange((checked: boolean) => {
+                if (checked) {
+                  this.duplicateAction = 'overwrite';
+                }
+              })
+            Text('覆盖')
+              .fontSize(14)
+              .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+          }
+
+          Row({ space: 6 }) {
+            Radio({ value: 'rename', group: 'duplicateAction' })
+              .checked(this.duplicateAction === 'rename')
+              .radioStyle({
+                checkedBackgroundColor: this.themeColor
+              })
+              .onChange((checked: boolean) => {
+                if (checked) {
+                  this.duplicateAction = 'rename';
+                }
+              })
+            Text('重命名')
+              .fontSize(14)
+              .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+          }
+        }
+        .width('100%')
+      }
+      .width('100%')
+      .padding(16)
+      .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
+      .borderRadius(8)
+    }
+    .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%')
+    .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: 12 }) {
+      Text('选择WebDAV账户')
+        .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' : '#E5000000')
+                  
+                  Text(`${account.host}:${account.port}`)
+                    .fontSize(12)
+                    .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000')
+                }
+                .alignItems(HorizontalAlign.Start)
+                .layoutWeight(1)
+
+                if (this.selectedAccount && this.selectedAccount.id === account.id) {
+                  Text('✓')
+                    .fontSize(24)
+                    .fontColor(this.themeColor)
+                }
+              }
+              .width('100%')
+              .padding(16)
+              .backgroundColor(this.selectedAccount && this.selectedAccount.id === account.id
+                ? (this.isDarkMode ? '#2E3033' : this.themeColor + '1A')
+                : (this.isDarkMode ? '#2E3033' : '#F1F3F5'))
+              .borderRadius(8)
+              .border({
+                width: 2,
+                color: this.selectedAccount && this.selectedAccount.id === account.id
+                  ? this.themeColor
+                  : 'transparent'
+              })
+              .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: 12 }) {
+      Text('选择文件')
+        .fontSize(16)
+        .fontWeight(FontWeight.Medium)
+        .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+        .alignSelf(ItemAlign.Start)
+
+      Button('选择音频文件')
+        .width('100%')
+        .height(52)
+        .fontSize(16)
+        .fontColor('#FFFFFF')
+        .backgroundColor(this.themeColor)
+        .borderRadius(12)
+        .onClick(() => {
+          this.openFilePicker();
+        })
+
+      if (this.selectedFiles.length > 0) {
+        Row({ space: 8 }) {
+          Text('✓')
+            .fontSize(18)
+            .fontColor(this.isDarkMode ? '#5BA854' : '#64BB5C')
+          
+          Text(`已选择 ${this.selectedFiles.length} 个文件`)
+            .fontSize(14)
+            .fontColor(this.isDarkMode ? '#99FFFFFF' : '#99000000')
+        }
+        .padding({ top: 4 })
+      }
+    }
+    .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: 12 }) {
+      Text('已选文件')
+        .fontSize(16)
+        .fontWeight(FontWeight.Medium)
+        .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+        .alignSelf(ItemAlign.Start)
+
+      List({ space: 8 }) {
+        ForEach(this.selectedFileNames, (fileName: string, index: number) => {
+          ListItem() {
+            Row({ space: 12 }) {
+              // 文件图标
+              Text('🎵')
+                .fontSize(20)
+
+              Text(fileName)
+                .fontSize(14)
+                .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000')
+                .layoutWeight(1)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+
+              Button('移除')
+                .fontSize(12)
+                .height(32)
+                .padding({ left: 12, right: 12 })
+                .backgroundColor(this.isDarkMode ? '#D94838' : '#E84026')
+                .fontColor('#FFFFFF')
+                .borderRadius(6)
+                .onClick(() => {
+                  this.removeSelectedFile(index);
+                })
+            }
+            .width('100%')
+            .padding(12)
+            .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5')
+            .borderRadius(8)
+          }
+        }, (fileName: string, index: number) => `${index}-${fileName}`)
+      }
+      .width('100%')
+      .maxHeight(300)
+      .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 })
+  }
+
+  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.isShowPathBrowser, this.PathBrowserDialogBuilder(), {
+      modalTransition: ModalTransition.DEFAULT,
+      backgroundColor: 'rgba(0, 0, 0, 0.6)',
+      onDisappear: () => {
+        this.isShowPathBrowser = false;
+      }
+    })
+  }
+
+  @Builder
+  PathBrowserDialogBuilder() {
+    Column() {
+      this.PathBrowserDialog()
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Center)
+    .backgroundColor('rgba(0, 0, 0, 0.5)')
+    .onClick(() => {
+      // 点击背景关闭对话框
+      this.closePathBrowser();
+    })
+  }
+}

+ 33 - 0
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -673,6 +673,25 @@ 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: '打开上传页面失败' });
+    });
+  }
+
   @Builder
   SortMenuBuilder() {
     Menu() {
@@ -905,6 +924,20 @@ export struct WebDavMainPage {
           })
           .attributeModifier(new ShadowModifier())
           .zIndex(0)
+          
+          //上传按钮
+          Button({ type: ButtonType.Circle, stateEffect: true }) {
+            SymbolGlyph($r('sys.symbol.arrow_up'))
+              .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''))
+          }
+          .attributeModifier(new ButtonFancyModifier(40, 40))
+          .animation({ duration: 300, curve: Curve.Ease })
+          .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
+          .onClick(() => {
+            this.navigateToUploadPage();
+          })
+          .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": "剪辑音频"

+ 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"
     }
   ]
 }