Browse Source

修复weddav上传音乐文件上传不了的问题
修复webdavmainpage的搜索音乐文件 搜出来点击播不了的问题

onecold 4 tháng trước cách đây
mục cha
commit
ed090d4ffe

+ 505 - 87
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -86,13 +86,18 @@ interface BaiduUploadProgressMessage {
   totalSize?: number;
 }
 
-interface RemoteDirectorySnapshot {
+export interface RemoteDirectorySnapshot {
   files: FileInfo[];
   folders: FileInfo[];
   songs: VideoItem[];
   rawEntries?: BaiduListEntry[];
 }
 
+interface DirectoryPreviewSnapshotPayload {
+  files?: FileInfo[];
+  songs?: VideoItem[];
+}
+
 interface RemoteDriveGlobalSearchIndex {
   fingerprint: string;
   songs: VideoItem[];
@@ -318,6 +323,12 @@ export interface TransferTask {
   customUploadPath?: string; // 自定义上传路径(可选)
 }
 
+interface UploadTaskResult {
+  success: boolean;
+  skipped?: boolean;
+  message?: string;
+}
+
 @Observed
 export class RemoteDriveManager {
   public rcpSocket: RcpSocket = RcpSocket.getInstance();
@@ -340,6 +351,10 @@ export class RemoteDriveManager {
   public webDavAccounts: WebDavAccount[] = [];
   public webDavSongs: VideoItem[] = [];
   public webDavFiles: FileInfo[] = [];  // 当前目录的所有文件(包括文件夹)
+  private readonly DIRECTORY_PREVIEW_CACHE_LIMIT: number = 20;
+  private readonly DIRECTORY_PREVIEW_STORE_PREFIX: string = 'webdav_directory_preview_';
+  private readonly DIRECTORY_PREVIEW_STORE_INDEX_KEY: string = 'webdav_directory_preview_index';
+  private readonly DIRECTORY_PREVIEW_STORE_LIMIT: number = 80;
   private static readonly BAIDU_DLINK_CACHE_TTL: number = 10 * 60 * 1000; // 10分钟
   private baiduDlinkCache: Map<string, BaiduDlinkCacheEntry> = new Map();
 
@@ -381,6 +396,10 @@ export class RemoteDriveManager {
   private lastAccountId: number | null = null;
   private readonly GLOBAL_SEARCH_PROGRESS_NOTIFY_INTERVAL: number = 4;
   private readonly GLOBAL_SEARCH_YIELD_INTERVAL: number = 6;
+  private readonly DIRECTORY_METADATA_ENRICH_LIMIT: number = 80;
+  private readonly DIRECTORY_LOAD_YIELD_INTERVAL: number = 40;
+  private directoryLoadRequestVersion: number = 0;
+  private scheduledWebDavAccountIdRepairIds: Set<string> = new Set();
 
   public currentAccount:WebDavAccount = new WebDavAccount();
 
@@ -1307,43 +1326,91 @@ export class RemoteDriveManager {
     }
     this.lastAccountId = account.id ?? null;
 
-    // 更新现有WebDAV歌曲的webdav_account_id字段
-    if (account && account.id) {
-      await this.updateWebDavSongsAccountId(account.id.toString());
-    }
+    const normalizedFullPath = this.normalizeFullPath(customPath !== undefined ? customPath : account.filepath);
+    const requestVersion = ++this.directoryLoadRequestVersion;
+    this.currentPath = normalizedFullPath;
+    this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoStart);
+    this.scheduleWebDavSongsAccountIdRepair(account.id);
 
     try {
-      this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoStart);
-
-      // 使用自定义路径或账户默认路径
-      const normalizedFullPath = this.normalizeFullPath(customPath !== undefined ? customPath : account.filepath);
-      this.currentPath = normalizedFullPath;
-
       if (account.webType === RemoteDriveType.Smb) {
-        await this.loadSmbFiles(account, normalizedFullPath);
+        await this.loadSmbFiles(account, normalizedFullPath, requestVersion);
       } else if (account.webType === RemoteDriveType.Navidrome) {
-        await this.loadNavidromeFiles(account, normalizedFullPath);
+        await this.loadNavidromeFiles(account, normalizedFullPath, requestVersion);
       } else if (account.webType === RemoteDriveType.Jellyfin) {
-        await this.loadJellyfinFiles(account, normalizedFullPath);
+        await this.loadJellyfinFiles(account, normalizedFullPath, requestVersion);
       } else if (account.webType === RemoteDriveType.Emby) {
-        await this.loadEmbyFiles(account, normalizedFullPath);
+        await this.loadEmbyFiles(account, normalizedFullPath, requestVersion);
       } else if (account.webType === RemoteDriveType.Ftp) {
-        await this.loadFtpFiles(account, normalizedFullPath);
+        await this.loadFtpFiles(account, normalizedFullPath, requestVersion);
       } else if (account.webType === RemoteDriveType.Baidu) {
-        await this.loadBaiduFiles(account, normalizedFullPath);
+        await this.loadBaiduFiles(account, normalizedFullPath, requestVersion);
       } else {
-        await this.loadWebDavFiles(account, normalizedFullPath);
+        await this.loadWebDavFiles(account, normalizedFullPath, requestVersion);
       }
 
+      if (!this.isDirectoryLoadRequestActive(requestVersion, normalizedFullPath)) {
+        Logger.info(TAG, `忽略过期目录成功事件 path=${normalizedFullPath}, request=${requestVersion}`);
+        return;
+      }
       this.seedCurrentDirectoryIntoGlobalSearchIndex(account);
-      void this.ensureGlobalSearchIndex(account);
       this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoSucceed);
     } catch (error) {
+      if (!this.isDirectoryLoadRequestActive(requestVersion, normalizedFullPath)) {
+        Logger.info(TAG, `忽略过期目录失败事件 path=${normalizedFullPath}, request=${requestVersion}`);
+        return;
+      }
       this.ErrorMessage = error as BusinessError;
       this.notifyObservers(RemoteDriveManagerStates.LoadFilesInfoFailed);
     }
   }
 
+  public async loadDirectorySnapshotSilently(account: WebDavAccount, customPath?: string): Promise<RemoteDirectorySnapshot> {
+    const normalizedFullPath = this.normalizeFullPath(customPath !== undefined ? customPath : account.filepath);
+    if (account.webType === RemoteDriveType.Smb) {
+      return await this.fetchSmbDirectorySnapshot(account, normalizedFullPath, false);
+    }
+    if (account.webType === RemoteDriveType.Ftp) {
+      return await this.fetchFtpDirectorySnapshot(account, normalizedFullPath, false);
+    }
+    if (account.webType === RemoteDriveType.Baidu) {
+      const accessToken = await this.ensureBaiduAccessToken(account);
+      return await this.fetchBaiduDirectorySnapshot(account, normalizedFullPath, false, accessToken);
+    }
+    return await this.fetchWebDavDirectorySnapshot(account, normalizedFullPath, false);
+  }
+
+  private scheduleWebDavSongsAccountIdRepair(accountId?: number | null): void {
+    if (accountId === undefined || accountId === null) {
+      return;
+    }
+    const accountIdString = accountId.toString();
+    if (accountIdString.length === 0 || this.scheduledWebDavAccountIdRepairIds.has(accountIdString)) {
+      return;
+    }
+    this.scheduledWebDavAccountIdRepairIds.add(accountIdString);
+    void this.updateWebDavSongsAccountId(accountIdString);
+  }
+
+  private isDirectoryLoadRequestActive(requestVersion: number, targetPath: string): boolean {
+    return requestVersion === this.directoryLoadRequestVersion && this.currentPath === targetPath;
+  }
+
+  private applyDirectorySnapshot(account: WebDavAccount, fullPath: string, snapshot: RemoteDirectorySnapshot,
+    requestVersion: number, shouldCache: boolean = true): boolean {
+    if (!this.isDirectoryLoadRequestActive(requestVersion, fullPath)) {
+      Logger.info(TAG,
+        `忽略过期目录结果 path=${fullPath}, request=${requestVersion}, latest=${this.directoryLoadRequestVersion}, currentPath=${this.currentPath}`);
+      return false;
+    }
+    this.webDavFiles = snapshot.files;
+    this.webDavSongs = snapshot.songs;
+    if (shouldCache) {
+      this.cacheDirectoryPreview(account, fullPath, snapshot);
+    }
+    return true;
+  }
+
   private normalizeFullPath(path?: string): string {
     if (!path || path.trim().length === 0) {
       return '/';
@@ -1359,6 +1426,230 @@ export class RemoteDriveManager {
     return normalized || '/';
   }
 
+  private getDirectoryPreviewCacheKey(account: WebDavAccount, fullPath: string): string {
+    const accountKey = account?.id !== undefined && account?.id !== null ? `${account.id}` :
+      `${account.webType}_${account.name ?? ''}`;
+    return `${accountKey}:${this.normalizeFullPath(fullPath)}`;
+  }
+
+  private getDirectoryPreviewStorageKey(account: WebDavAccount, fullPath: string): string {
+    return `${this.DIRECTORY_PREVIEW_STORE_PREFIX}${encodeURIComponent(
+      this.getDirectoryPreviewCacheKey(account, fullPath)
+    )}`;
+  }
+
+  private cloneDirectoryPreviewFile(info: FileInfo): FileInfo {
+    const clone = new FileInfo(info.rootpath, info.name, info.totalSize, info.time);
+    clone.readOnly = info.readOnly;
+    clone.fileName = info.fileName;
+    clone.href = info.href;
+    clone.contentLength = info.contentLength;
+    clone.isDirectory = info.isDirectory;
+    return clone;
+  }
+
+  private cloneDirectoryPreviewSong(item: VideoItem): VideoItem {
+    const clone = new VideoItem(
+      item.name,
+      item.id,
+      item.filePath,
+      item.type,
+      item.videoSize,
+      item.cTime,
+      item.size,
+      item.pixelMapPath,
+      item.artist,
+      item.album,
+      item.fileName,
+      item.lastPlayed
+    );
+    clone.parentPath = item.parentPath;
+    clone.isFav = item.isFav;
+    clone.duration = item.duration;
+    clone.mimeType = item.mimeType;
+    clone.sampleRate = item.sampleRate;
+    clone.trackCount = item.trackCount;
+    clone.lastPlayedStr = item.lastPlayedStr;
+    clone.playCount = item.playCount;
+    clone.lyricContent = item.lyricContent;
+    clone.md5Str = item.md5Str;
+    clone.extra_json = item.extra_json;
+    clone.pyStr = item.pyStr;
+    clone.bit_rate = item.bit_rate;
+    clone.probe_score = item.probe_score;
+    clone.year = item.year;
+    clone.nb_streams = item.nb_streams;
+    clone.nb_programs = item.nb_programs;
+    clone.genre = item.genre;
+    clone.track = item.track;
+    clone.bits_per_raw_sample = item.bits_per_raw_sample;
+    clone.channels = item.channels;
+    clone.channel_layout = item.channel_layout;
+    clone.start_time = item.start_time;
+    clone.ALBUMARTIST = item.ALBUMARTIST;
+    clone.COMPOSER = item.COMPOSER;
+    clone.LYRICIST = item.LYRICIST;
+    clone.COMMENT = item.COMMENT;
+    clone.disc = item.disc;
+    clone.isCustomCover = item.isCustomCover;
+    clone.webdav_account_id = item.webdav_account_id;
+    clone.remote_rel_path = item.remote_rel_path;
+    clone.navArtistId = item.navArtistId;
+    clone.navAlbumId = item.navAlbumId;
+    clone.baiduFsId = item.baiduFsId;
+    clone.webdav_id = item.webdav_id;
+    clone.lyricIndex = item.lyricIndex;
+    return clone;
+  }
+
+  private buildDirectoryPreviewSnapshot(files: FileInfo[], songs: VideoItem[]): RemoteDirectorySnapshot {
+    return {
+      files,
+      folders: files.filter((item: FileInfo) => item.isDirectory),
+      songs
+    };
+  }
+
+  private sortDirectoryPreviewSongs(target: VideoItem[]): VideoItem[] {
+    const sortType = PreferencesUtil.getNumberSync('webDavSortType', 0);
+    const isShowFileName = PreferencesUtil.getBooleanSync('isShowFileName', false);
+    switch (sortType) {
+      case 0:
+        Utility.doSortListAscending(target, isShowFileName);
+        break;
+      case 1:
+        Utility.doSortListDescending(target, isShowFileName);
+        break;
+      case 2:
+        target.sort((a: VideoItem, b: VideoItem) => a.cTime.localeCompare(b.cTime));
+        break;
+      case 3:
+        target.sort((a: VideoItem, b: VideoItem) => b.cTime.localeCompare(a.cTime));
+        break;
+      case 4:
+        target.sort((a: VideoItem, b: VideoItem) => a.videoSize - b.videoSize);
+        break;
+      case 5:
+        target.sort((a: VideoItem, b: VideoItem) => b.videoSize - a.videoSize);
+        break;
+      default:
+        Utility.doSortListAscending(target, isShowFileName);
+        break;
+    }
+    return target;
+  }
+
+  private sortDirectoryPreviewFiles(target: FileInfo[]): FileInfo[] {
+    const sortType = PreferencesUtil.getNumberSync('webDavSortType', 0);
+    switch (sortType) {
+      case 1:
+        target.sort((a: FileInfo, b: FileInfo) => b.fileName.localeCompare(a.fileName));
+        break;
+      case 2:
+        target.sort((a: FileInfo, b: FileInfo) => a.time - b.time);
+        break;
+      case 3:
+        target.sort((a: FileInfo, b: FileInfo) => b.time - a.time);
+        break;
+      case 0:
+      default:
+        target.sort((a: FileInfo, b: FileInfo) => a.fileName.localeCompare(b.fileName));
+        break;
+    }
+    return target;
+  }
+
+  private serializeDirectoryPreviewSnapshot(snapshot: RemoteDirectorySnapshot): string {
+    const files = this.sortDirectoryPreviewFiles(
+      snapshot.files.map((item: FileInfo) => this.cloneDirectoryPreviewFile(item))
+    ).slice(0, this.DIRECTORY_PREVIEW_CACHE_LIMIT);
+    const songs = this.sortDirectoryPreviewSongs(
+      snapshot.songs.map((item: VideoItem) => this.cloneDirectoryPreviewSong(item))
+    ).slice(0, this.DIRECTORY_PREVIEW_CACHE_LIMIT);
+    return JSON.stringify({
+      files,
+      songs,
+      updatedAt: Date.now()
+    });
+  }
+
+  private deserializeDirectoryPreviewSnapshot(serialized: string): RemoteDirectorySnapshot | undefined {
+    if (!serialized || serialized.length === 0) {
+      return undefined;
+    }
+    const parsed = JSON.parse(serialized) as DirectoryPreviewSnapshotPayload;
+    const files = Array.isArray(parsed.files) ? parsed.files
+      .map((item: FileInfo) => this.cloneDirectoryPreviewFile(item)) : [];
+    const songs = Array.isArray(parsed.songs) ? parsed.songs
+      .map((item: VideoItem) => this.cloneDirectoryPreviewSong(item)) : [];
+    if (!Array.isArray(parsed.files) && !Array.isArray(parsed.songs)) {
+      return undefined;
+    }
+    return this.buildDirectoryPreviewSnapshot(files, songs);
+  }
+
+  private loadDirectoryPreviewStorageKeys(): string[] {
+    try {
+      const raw = PreferencesUtil.getStringSync(this.DIRECTORY_PREVIEW_STORE_INDEX_KEY, '');
+      if (!raw || raw.length === 0) {
+        return [];
+      }
+      const parsed = JSON.parse(raw) as string[];
+      return Array.isArray(parsed) ? parsed.filter((item: string) => !!item && item.length > 0) : [];
+    } catch (error) {
+      Logger.warn(TAG, `读取目录预览缓存索引失败: ${(error as Error).message}`);
+      return [];
+    }
+  }
+
+  private saveDirectoryPreviewStorageKeys(keys: string[]): void {
+    PreferencesUtil.putSync(this.DIRECTORY_PREVIEW_STORE_INDEX_KEY, JSON.stringify(keys));
+  }
+
+  private touchDirectoryPreviewStorageKey(storageKey: string): void {
+    const currentKeys = this.loadDirectoryPreviewStorageKeys().filter((item: string) => item !== storageKey);
+    currentKeys.unshift(storageKey);
+    if (currentKeys.length > this.DIRECTORY_PREVIEW_STORE_LIMIT) {
+      const staleKeys = currentKeys.slice(this.DIRECTORY_PREVIEW_STORE_LIMIT);
+      for (let i = 0; i < staleKeys.length; i++) {
+        PreferencesUtil.deleteSync(staleKeys[i]);
+      }
+    }
+    this.saveDirectoryPreviewStorageKeys(currentKeys.slice(0, this.DIRECTORY_PREVIEW_STORE_LIMIT));
+  }
+
+  private cacheDirectoryPreview(account: WebDavAccount, fullPath: string, snapshot: RemoteDirectorySnapshot): void {
+    if (!account || !snapshot) {
+      return;
+    }
+    try {
+      const storageKey = this.getDirectoryPreviewStorageKey(account, fullPath);
+      PreferencesUtil.putSync(storageKey, this.serializeDirectoryPreviewSnapshot(snapshot));
+      this.touchDirectoryPreviewStorageKey(storageKey);
+    } catch (error) {
+      Logger.warn(TAG, `写入目录预览缓存失败 path=${fullPath}, error=${(error as Error).message}`);
+    }
+  }
+
+  public getDirectoryPreview(account: WebDavAccount, fullPath?: string): RemoteDirectorySnapshot | undefined {
+    if (!account) {
+      return undefined;
+    }
+    try {
+      const storageKey = this.getDirectoryPreviewStorageKey(account,
+        fullPath !== undefined ? fullPath : this.currentPath || account.filepath || '/');
+      const serialized = PreferencesUtil.getStringSync(storageKey, '');
+      return this.deserializeDirectoryPreviewSnapshot(serialized);
+    } catch (error) {
+      Logger.warn(TAG, `读取目录预览缓存失败: ${(error as Error).message}`);
+      return undefined;
+    }
+  }
+
+  public getBreadcrumbsForPreview(account: WebDavAccount, targetPath: string): BreadcrumbItem[] {
+    return this.getBreadcrumbsForPath(account, targetPath);
+  }
+
   private getSongKey(song: VideoItem): string {
     if (!song) {
       return '';
@@ -1406,6 +1697,19 @@ export class RemoteDriveManager {
     return normalized.substring(0, lastSlash);
   }
 
+
+  private getFileNameFromRemotePath(remotePath: string): string {
+    if (!remotePath || remotePath === '/') {
+      return '';
+    }
+    const normalized = this.normalizeFullPath(remotePath);
+    const lastSlash = normalized.lastIndexOf('/');
+    if (lastSlash < 0) {
+      return normalized;
+    }
+    return normalized.substring(lastSlash + 1);
+  }
+
   private async ensureRemoteDirectories(account: WebDavAccount, directoryPath: string): Promise<void> {
     if (!directoryPath || directoryPath === '/') {
       return;
@@ -1485,20 +1789,22 @@ export class RemoteDriveManager {
     return fullPath.replace(/^\/+/, '');
   }
 
-  private async loadWebDavFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+  private async loadWebDavFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
     const snapshot = await this.fetchWebDavDirectorySnapshot(account, fullPath, true);
-    this.webDavFiles = snapshot.files;
-    this.webDavSongs = snapshot.songs;
+    if (!this.applyDirectorySnapshot(account, fullPath, snapshot, requestVersion)) {
+      return;
+    }
     Logger.info(TAG, `从WebDAV获取到 ${snapshot.files.length} 个文件/文件夹`);
   }
 
-  private async loadSmbFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+  private async loadSmbFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
     if (!account.smbShare) {
       throw new Error('SMB账户缺少共享名称');
     }
     const snapshot = await this.fetchSmbDirectorySnapshot(account, fullPath, true);
-    this.webDavFiles = snapshot.files;
-    this.webDavSongs = snapshot.songs;
+    if (!this.applyDirectorySnapshot(account, fullPath, snapshot, requestVersion)) {
+      return;
+    }
     Logger.info(TAG, `从SMB获取到 ${snapshot.files.length} 个文件/文件夹`);
   }
 
@@ -1528,18 +1834,20 @@ export class RemoteDriveManager {
     };
   }
 
-  private async loadFtpFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+  private async loadFtpFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
     const snapshot = await this.fetchFtpDirectorySnapshot(account, fullPath, true);
-    this.webDavFiles = snapshot.files;
-    this.webDavSongs = snapshot.songs;
+    if (!this.applyDirectorySnapshot(account, fullPath, snapshot, requestVersion)) {
+      return;
+    }
     Logger.info(TAG, `从FTP获取到 ${snapshot.files.length} 个文件/文件夹`);
   }
 
-  private async loadBaiduFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+  private async loadBaiduFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
     const accessToken = await this.ensureBaiduAccessToken(account);
     const snapshot = await this.fetchBaiduDirectorySnapshot(account, fullPath, true, accessToken);
-    this.webDavFiles = snapshot.files;
-    this.webDavSongs = snapshot.songs;
+    if (!this.applyDirectorySnapshot(account, fullPath, snapshot, requestVersion)) {
+      return;
+    }
     Logger.info(TAG, `从百度网盘获取到 ${snapshot.files.length} 个文件/文件夹`);
     const rawEntries = snapshot.rawEntries ?? [];
     this.scheduleBaiduPrefetch(account, accessToken, rawEntries);
@@ -2171,7 +2479,7 @@ export class RemoteDriveManager {
     void prefetch();
   }
 
-  private async loadNavidromeFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+  private async loadNavidromeFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
     const normalized = this.normalizeFullPath(fullPath);
     this.registerPathLabel('/', '根目录');
     const segments = normalized.split('/').filter(part => part.length > 0);
@@ -2180,12 +2488,15 @@ export class RemoteDriveManager {
     if (normalized === '/' || segments.length === 0) {
       const artists: NavidromeArtist[] = await this.navidromeApi.getArtists(account);
       artists.sort((a, b) => a.name.localeCompare(b.name));
-      this.webDavFiles = artists.map(artist => {
+      const files: FileInfo[] = artists.map(artist => {
         const info = this.createNavDirectory(artist.name, `/artist/${artist.id}`);
         this.registerPathLabel(info.href, artist.name);
         return info;
       });
-      this.webDavSongs = [];
+      const snapshot = this.buildDirectoryPreviewSnapshot(files, []);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
       void ServerLogUtil.info(TAG, `获取艺术家列表,数量: ${artists.length}`);
       void ServerLogUtil.debug(TAG, `artists ids: ${artists.map(a => a.id).join(',')}`)
       return;
@@ -2195,12 +2506,15 @@ export class RemoteDriveManager {
       const artistId = segments[1];
       const detail: NavidromeArtistDetail = await this.navidromeApi.getArtist(account, artistId);
       this.registerPathLabel(`/artist/${artistId}`, detail.name);
-      this.webDavFiles = detail.albums.map(album => {
+      const files: FileInfo[] = detail.albums.map(album => {
         const info = this.createNavDirectory(album.name, `/album/${album.id}`);
         this.registerPathLabel(info.href, album.name);
         return info;
       });
-      this.webDavSongs = [];
+      const snapshot = this.buildDirectoryPreviewSnapshot(files, []);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
       void ServerLogUtil.info(TAG, `进入艺术家 ${detail.name},专辑数: ${detail.albums.length}`);
       void ServerLogUtil.debug(TAG, `artist albums: ${detail.albums.map(a => a.id).join(',')}`)
       return;
@@ -2210,11 +2524,18 @@ export class RemoteDriveManager {
       const albumId = segments[1];
       const detail: NavidromeAlbumDetail = await this.navidromeApi.getAlbum(account, albumId);
       this.registerPathLabel(`/album/${albumId}`, detail.name);
-      this.webDavFiles = detail.songs.map(song => this.createNavSongFileInfo(song, albumId));
+      const files: FileInfo[] = detail.songs.map(song => this.createNavSongFileInfo(song, albumId));
       const albumCoverUrl = await this.navidromeApi.buildCoverArtUrl(account,
         detail.coverArt ?? (detail.id ? `al-${detail.id}` : undefined));
-      this.webDavSongs = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail, albumCoverUrl));
-      await this.enrichSongsWithDatabase(this.webDavSongs);
+      const songs: VideoItem[] = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail, albumCoverUrl));
+      if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
+        return;
+      }
+      await this.enrichSongsWithDatabase(songs);
+      const snapshot = this.buildDirectoryPreviewSnapshot(files, songs);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
       void ServerLogUtil.info(TAG, `进入专辑 ${detail.name},歌曲数: ${detail.songs.length}`);
       void ServerLogUtil.debug(TAG, `album songs ids: ${detail.songs.map(s => s.id).join(',')}`)
       return;
@@ -2223,18 +2544,24 @@ export class RemoteDriveManager {
     throw new Error(`不支持的Navidrome路径: ${fullPath}`);
   }
 
-  private async loadJellyfinFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+  private async loadJellyfinFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
     const normalized = this.normalizeFullPath(fullPath);
     this.registerPathLabel('/', '根目录');
     const segments = normalized.split('/').filter(part => part.length > 0);
     void ServerLogUtil.info(TAG, `浏览 Jellyfin 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
 
     if (normalized === '/' || segments.length === 0) {
-      const songs = await this.jellyfinApi.getAllSongs(account);
-      this.webDavFiles = [];
-      this.webDavSongs = songs.map(song => this.buildJellyfinVideoItem(song, account, null, undefined));
-      await this.enrichSongsWithDatabase(this.webDavSongs);
-      void ServerLogUtil.info(TAG, `进入 Jellyfin 根目录,歌曲数: ${songs.length}`);
+      const sourceSongs = await this.jellyfinApi.getAllSongs(account);
+      const songs: VideoItem[] = sourceSongs.map(song => this.buildJellyfinVideoItem(song, account, null, undefined));
+      if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
+        return;
+      }
+      await this.enrichSongsWithDatabase(songs);
+      const snapshot = this.buildDirectoryPreviewSnapshot([], songs);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
+      void ServerLogUtil.info(TAG, `进入 Jellyfin 根目录,歌曲数: ${sourceSongs.length}`);
       return;
     }
 
@@ -2245,12 +2572,15 @@ export class RemoteDriveManager {
       if (artistLabel) {
         this.registerPathLabel(`/artist/${artistId}`, artistLabel);
       }
-      this.webDavFiles = albums.map(album => {
+      const files: FileInfo[] = albums.map(album => {
         const info = this.createJellyfinDirectory(album.name, `/album/${album.id}`);
         this.registerPathLabel(info.href, album.name);
         return info;
       });
-      this.webDavSongs = [];
+      const snapshot = this.buildDirectoryPreviewSnapshot(files, []);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
       void ServerLogUtil.info(TAG, `进入 Jellyfin 艺术家 ${artistLabel},专辑数: ${albums.length}`);
       return;
     }
@@ -2267,9 +2597,16 @@ export class RemoteDriveManager {
       } catch (error) {
         Logger.warn(TAG, `Jellyfin 专辑封面获取失败: ${(error as Error).message}`);
       }
-      this.webDavFiles = songs.map(song => this.createJellyfinSongFileInfo(song, albumId));
-      this.webDavSongs = songs.map(song => this.buildJellyfinVideoItem(song, account, album, coverUrl));
-      await this.enrichSongsWithDatabase(this.webDavSongs);
+      const files: FileInfo[] = songs.map(song => this.createJellyfinSongFileInfo(song, albumId));
+      const songItems: VideoItem[] = songs.map(song => this.buildJellyfinVideoItem(song, account, album, coverUrl));
+      if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
+        return;
+      }
+      await this.enrichSongsWithDatabase(songItems);
+      const snapshot = this.buildDirectoryPreviewSnapshot(files, songItems);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
       void ServerLogUtil.info(TAG, `进入 Jellyfin 专辑 ${albumName},歌曲数: ${songs.length}`);
       return;
     }
@@ -2277,18 +2614,24 @@ export class RemoteDriveManager {
     throw new Error(`不支持的Jellyfin路径: ${fullPath}`);
   }
 
-  private async loadEmbyFiles(account: WebDavAccount, fullPath: string): Promise<void> {
+  private async loadEmbyFiles(account: WebDavAccount, fullPath: string, requestVersion: number): Promise<void> {
     const normalized = this.normalizeFullPath(fullPath);
     this.registerPathLabel('/', '根目录');
     const segments = normalized.split('/').filter(part => part.length > 0);
     void ServerLogUtil.info(TAG, `浏览 Emby 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
 
     if (normalized === '/' || segments.length === 0) {
-      const songs = await this.embyApi.getAllSongs(account);
-      this.webDavFiles = [];
-      this.webDavSongs = songs.map(song => this.buildEmbyVideoItem(song, account, null, undefined));
-      await this.enrichSongsWithDatabase(this.webDavSongs);
-      void ServerLogUtil.info(TAG, `进入 Emby 根目录,歌曲数: ${songs.length}`);
+      const sourceSongs = await this.embyApi.getAllSongs(account);
+      const songs: VideoItem[] = sourceSongs.map(song => this.buildEmbyVideoItem(song, account, null, undefined));
+      if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
+        return;
+      }
+      await this.enrichSongsWithDatabase(songs);
+      const snapshot = this.buildDirectoryPreviewSnapshot([], songs);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
+      void ServerLogUtil.info(TAG, `进入 Emby 根目录,歌曲数: ${sourceSongs.length}`);
       return;
     }
 
@@ -2299,12 +2642,15 @@ export class RemoteDriveManager {
       if (artistLabel) {
         this.registerPathLabel(`/artist/${artistId}`, artistLabel);
       }
-      this.webDavFiles = albums.map(album => {
+      const files: FileInfo[] = albums.map(album => {
         const info = this.createEmbyDirectory(album.name, `/album/${album.id}`);
         this.registerPathLabel(info.href, album.name);
         return info;
       });
-      this.webDavSongs = [];
+      const snapshot = this.buildDirectoryPreviewSnapshot(files, []);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
       void ServerLogUtil.info(TAG, `进入 Emby 艺术家 ${artistLabel},专辑数: ${albums.length}`);
       return;
     }
@@ -2321,9 +2667,16 @@ export class RemoteDriveManager {
       } catch (error) {
         Logger.warn(TAG, `Emby 专辑封面获取失败: ${(error as Error).message}`);
       }
-      this.webDavFiles = songs.map(song => this.createEmbySongFileInfo(song, albumId));
-      this.webDavSongs = songs.map(song => this.buildEmbyVideoItem(song, account, album, coverUrl));
-      await this.enrichSongsWithDatabase(this.webDavSongs);
+      const files: FileInfo[] = songs.map(song => this.createEmbySongFileInfo(song, albumId));
+      const songItems: VideoItem[] = songs.map(song => this.buildEmbyVideoItem(song, account, album, coverUrl));
+      if (!this.isDirectoryLoadRequestActive(requestVersion, normalized)) {
+        return;
+      }
+      await this.enrichSongsWithDatabase(songItems);
+      const snapshot = this.buildDirectoryPreviewSnapshot(files, songItems);
+      if (!this.applyDirectorySnapshot(account, normalized, snapshot, requestVersion, false)) {
+        return;
+      }
       void ServerLogUtil.info(TAG, `进入 Emby 专辑 ${albumName},歌曲数: ${songs.length}`);
       return;
     }
@@ -2365,10 +2718,11 @@ export class RemoteDriveManager {
       } else if (this.isAudioFile(info.fileName)) {
         songs.push(this.fileInfoToVideoItem(info, account));
       }
+      if ((i + 1) % this.DIRECTORY_LOAD_YIELD_INTERVAL === 0) {
+        await this.yieldGlobalSearchBuild();
+      }
     }
-    if (enrichFromDatabase) {
-      await this.enrichSongsWithDatabase(songs);
-    }
+    await this.enrichDirectorySongsIfNeeded(songs, enrichFromDatabase);
     return {
       files: normalizedFiles,
       folders: normalizedFiles.filter(file => file.isDirectory),
@@ -2400,10 +2754,11 @@ export class RemoteDriveManager {
       } else if (this.isAudioFile(info.fileName)) {
         songs.push(this.fileInfoToVideoItem(info, account));
       }
+      if ((i + 1) % this.DIRECTORY_LOAD_YIELD_INTERVAL === 0) {
+        await this.yieldGlobalSearchBuild();
+      }
     }
-    if (enrichFromDatabase) {
-      await this.enrichSongsWithDatabase(songs);
-    }
+    await this.enrichDirectorySongsIfNeeded(songs, enrichFromDatabase);
     return {
       files,
       folders: files.filter(file => file.isDirectory),
@@ -2439,10 +2794,11 @@ export class RemoteDriveManager {
       } else if (this.isAudioFile(info.fileName)) {
         songs.push(this.buildFtpVideoItem(info, account));
       }
+      if ((i + 1) % this.DIRECTORY_LOAD_YIELD_INTERVAL === 0) {
+        await this.yieldGlobalSearchBuild();
+      }
     }
-    if (enrichFromDatabase) {
-      await this.enrichSongsWithDatabase(songs);
-    }
+    await this.enrichDirectorySongsIfNeeded(songs, enrichFromDatabase);
     return {
       files,
       folders: files.filter(file => file.isDirectory),
@@ -2467,10 +2823,11 @@ export class RemoteDriveManager {
       } else if (this.isAudioFile(info.fileName)) {
         songs.push(this.buildBaiduVideoItem(entry, account));
       }
+      if ((i + 1) % this.DIRECTORY_LOAD_YIELD_INTERVAL === 0) {
+        await this.yieldGlobalSearchBuild();
+      }
     }
-    if (enrichFromDatabase) {
-      await this.enrichSongsWithDatabase(songs);
-    }
+    await this.enrichDirectorySongsIfNeeded(songs, enrichFromDatabase);
     return {
       files,
       folders: files.filter(file => file.isDirectory),
@@ -2479,6 +2836,20 @@ export class RemoteDriveManager {
     };
   }
 
+  private async enrichDirectorySongsIfNeeded(songs: VideoItem[], enrichFromDatabase: boolean): Promise<void> {
+    if (!enrichFromDatabase || !songs || songs.length === 0) {
+      return;
+    }
+    if (songs.length <= this.DIRECTORY_METADATA_ENRICH_LIMIT) {
+      await this.enrichSongsWithDatabase(songs);
+      return;
+    }
+    const initialSongs = songs.slice(0, this.DIRECTORY_METADATA_ENRICH_LIMIT);
+    Logger.info(TAG,
+      `目录歌曲较多,首屏先补全前 ${initialSongs.length}/${songs.length} 首元数据,避免进入目录卡顿`);
+    await this.enrichSongsWithDatabase(initialSongs);
+  }
+
   private supportsGlobalSearchIndex(account: WebDavAccount): boolean {
     switch (account.webType) {
       case RemoteDriveType.WebDav:
@@ -4176,16 +4547,25 @@ export class RemoteDriveManager {
     this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
 
     try {
-      if(this.currentAccount.webType==RemoteDriveType.WebDav){
-        await this.uploadSingleFile(task);
-      }else if(this.currentAccount.webType==RemoteDriveType.Baidu){
-        await this.uploadBaiduFile(task);
+      let result: UploadTaskResult;
+      if (task.account.webType === RemoteDriveType.WebDav) {
+        result = await this.uploadSingleFile(task);
+      } else if (task.account.webType === RemoteDriveType.Baidu) {
+        result = await this.uploadBaiduFile(task);
+      } else {
+        throw new Error(`暂不支持该类型上传: ${task.account.webType}`);
       }
 
-      
-      // 上传成功,移到完成队列
-      Logger.info(TAG, `任务成功,移至完成队列: ${task.song.name}`);
       this.uploadQueue.shift();
+      if (result.skipped) {
+        Logger.warn(TAG, `任务被跳过: ${task.song.name}, reason=${result.message || 'skip'}`);
+        this.notifyObservers(RemoteDriveManagerStates.UploadSkipped);
+        this.notifyObservers(RemoteDriveManagerStates.ChangeUploadQueue);
+        await this.processNextUploadTask();
+        return;
+      }
+
+      Logger.info(TAG, `任务成功,移至完成队列: ${task.song.name}`);
       this.finishUploadQueue.push(task);
       this.invalidateGlobalSearchIndex(task.account);
       this.notifyObservers(RemoteDriveManagerStates.UploadSuccess);
@@ -4367,7 +4747,7 @@ export class RemoteDriveManager {
    * 上传单个文件
    * @param task 上传任务
    */
-  private async uploadSingleFile(task: TransferTask): Promise<void> {
+  private async uploadSingleFile(task: TransferTask): Promise<UploadTaskResult> {
     const song = task.song;
     const account = task.account;
     const startTime = Date.now();
@@ -4424,7 +4804,11 @@ export class RemoteDriveManager {
         
         if (duplicateAction === 'skip') {
           Logger.warn(TAG, '文件已存在,跳过上传:', remotePath);
-          return;
+          return {
+            success: true,
+            skipped: true,
+            message: `远程已存在同名文件: ${remotePath}`
+          };
         } else if (duplicateAction === 'overwrite') {
           Logger.warn(TAG, '文件已存在,将覆盖:', remotePath);
         } else if (duplicateAction === 'rename') {
@@ -4476,7 +4860,15 @@ export class RemoteDriveManager {
       Logger.info(TAG, `平均速度: ${avgSpeed} MB/s`);
       Logger.info(TAG, `目标路径: ${remotePath}`);
       Logger.info(TAG, '====================================');
-      
+      if (account.webType === RemoteDriveType.WebDav) {
+        const uploadedExists = await this.checkFileExists(account, remotePath);
+        if (!uploadedExists) {
+          throw new Error(`上传请求已完成,但服务端未找到文件: ${remotePath}`);
+        }
+      }
+      return {
+        success: true
+      };
     } catch (error) {
       const err = error as Error;
       const endTime = Date.now();
@@ -4507,7 +4899,7 @@ export class RemoteDriveManager {
    * 上传百度网盘文件(使用TaskPool避免主线程阻塞)
    * @param task 上传任务
    */
-  private async uploadBaiduFile(task: TransferTask): Promise<void> {
+  private async uploadBaiduFile(task: TransferTask): Promise<UploadTaskResult> {
     const song = task.song;
     const account = task.account;
     const startTime = Date.now();
@@ -4600,6 +4992,9 @@ export class RemoteDriveManager {
 
       // 更新上传进度到100%
       this.notifyObservers(RemoteDriveManagerStates.UploadProgress);
+      return {
+        success: true
+      };
 
     } catch (error) {
       const err = error as Error;
@@ -4683,8 +5078,32 @@ export class RemoteDriveManager {
    * @param remotePath 远程路径
    */
   private async checkFileExists(account: WebDavAccount, remotePath: string): Promise<boolean> {
+    Logger.info(TAG, `检查远程文件是否存在: ${remotePath}`);
+    if (account.webType === RemoteDriveType.WebDav) {
+      try {
+        const parentPath = this.getDirectoryFromRemotePath(remotePath);
+        const targetFileName = this.getFileNameFromRemotePath(remotePath);
+        const files = await this.rcpSocket.getFileList(
+          account.host,
+          account.localHost,
+          account.isUseLocalHost,
+          account.port,
+          parentPath,
+          account.account,
+          account.password,
+          account.enableHttps
+        );
+        const exists = files.some((item: FileInfo) => !item.isDirectory && item.fileName === targetFileName);
+        Logger.info(TAG, `WebDAV目录比对结果: ${targetFileName} => ${exists ? '存在' : '不存在'}`);
+        return exists;
+      } catch (error) {
+        const err = error as Error;
+        Logger.info(TAG, `WebDAV目录比对失败: ${err.message}, 假定文件不存在`);
+        return false;
+      }
+    }
+
     try {
-      Logger.info(TAG, `检查远程文件是否存在: ${remotePath}`);
       const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
       const fileSize = await this.rcpSocket.RcpSendHead(
         host,
@@ -4694,13 +5113,12 @@ export class RemoteDriveManager {
         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;
     }

+ 232 - 91
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -1,4 +1,4 @@
-import { BreadcrumbItem, RemoteDriveGlobalSearchResult, RemoteDriveManager } from '../common/util/RemoteDriveManager';
+import { BreadcrumbItem, RemoteDirectorySnapshot, RemoteDriveGlobalSearchResult, RemoteDriveManager } from '../common/util/RemoteDriveManager';
 import { WebDavAccount } from '../viewmodel/WebDavAccount';
 import { Song } from '../viewmodel/Song';
 import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
@@ -65,8 +65,15 @@ interface RemoteThumbSource {
 
 const TAG = 'heanup WebDavMainPage';
 const REMOTE_THUMB_CACHE_DIR: string = 'remote_thumbs';
-const REMOTE_THUMB_MAX_TASK_COUNT: number = 36;
+const REMOTE_THUMB_MAX_TASK_COUNT: number = 12;
+const REMOTE_THUMB_LARGE_LIST_THRESHOLD: number = 120;
+const REMOTE_THUMB_LARGE_LIST_PREFETCH_COUNT: number = 6;
+const REMOTE_THUMB_LARGE_LIST_DELAY_MS: number = 900;
 const REMOTE_THUMB_CAPTURE_SECONDS: string = '1.2';
+const WEBDAV_PROGRESSIVE_RELOAD_THRESHOLD: number = 180;
+const WEBDAV_PROGRESSIVE_RELOAD_INITIAL_COUNT: number = 20;
+const WEBDAV_PROGRESSIVE_RELOAD_BATCH_SIZE: number = 20;
+const WEBDAV_PROGRESSIVE_RELOAD_DELAY_MS: number = 32;
 
 // WebDAV歌曲数据全局内存存储
 let globalWebdavVideoItems: VideoItem[] = [];
@@ -122,15 +129,17 @@ export struct WebDavMainPage {
   @State webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance();
   @State accounts: WebDavAccount[] = [];
   @Prop @Watch('onSwitchAccount') selectedAccount: WebDavAccount;
-  @State songs: VideoItem[] = [];
+  private songs: VideoItem[] = [];
   @State  dataSource:LazyDataSource<VideoItem> = new LazyDataSource(this.songs)
   @Link mType: number;
   @Link offsetX: number;
   @Link isShowDrawer: boolean;
   @State isLoading: boolean = false;
-  @State webDavFiles: FileInfo[] = []; // 直接在页面中保存文件列表副本
+  @State isRefreshingCache: boolean = false;
+  private webDavFiles: FileInfo[] = []; // 原始目录数据保持为普通字段,避免大数组响应式代理卡顿
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
   @StorageProp('isDarkMode') isDarkMode: boolean = false;
+  @State currentDirectoryPath: string = '';
   @State breadcrumbs:BreadcrumbItem[] = []//面包屑导航
 
   @State visibleFoldersState: FileInfo[] = []; // 改为普通状态变量
@@ -162,6 +171,9 @@ export struct WebDavMainPage {
     this.scheduleDownloadCenterRefresh();
   };
   private downloadCenterRefreshTimer: number = -1;
+  private displayReloadToken: number = 0;
+  private pendingDirectoryRefreshTimer: number = -1;
+  private pendingDirectoryRefreshToken: number = 0;
 
   private async runSerializedThumbnailFfmpeg(task: () => Promise<void>): Promise<void> {
     const nextTask: Promise<void> = globalRemoteThumbFfmpegQueue.then(async (): Promise<void> => {
@@ -175,6 +187,7 @@ export struct WebDavMainPage {
 
   async onSwitchAccount(){
     console.log('heanup 切换账户:', this.selectedAccount.name);
+    this.cancelPendingDirectoryRefresh();
     this.thumbnailTaskToken += 1;
     this.thumbnailRunningKeys.clear();
     this.searchTicket++;
@@ -191,34 +204,110 @@ export struct WebDavMainPage {
     // 注意:不再需要清空全局上下文,因为LocalMusic已改用实例变量缓存
     // 当新账户加载时,新的认证信息会自动覆盖旧的
     Logger.info(TAG, 'heanup 切换账户,准备加载新账户文件');
-    
-    this.isLoading = true;
-    await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount)
-      .catch((error: Error) => {
-        Logger.error(TAG, '加载文件失败: ' + error.message);
-        this.isLoading = false;
-      });
+    this.currentDirectoryPath = this.selectedAccount.filepath || '/';
+
+    this.scheduleDirectoryRefresh(this.selectedAccount.filepath,
+      () => this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount), '加载文件失败');
     this.breadcrumbs = this.webdavManager.getBreadcrumbs();
   }
 
-  updateListData(mList:Array<VideoItem>, noSort?: boolean){
+  updateListData(mList:Array<VideoItem>, noSort?: boolean, shouldScrollToTop: boolean = true){
     if (!noSort) {
       this.sortType = PreferencesUtil.getNumberSync('webDavSortType', 0)
-      this.doSortType(this.sortType)
+      this.doSortType(this.sortType, shouldScrollToTop)
       return
     }
-    this.refreshDisplaySongs(mList)
+    this.refreshDisplaySongs(mList, shouldScrollToTop)
   }
 
-  private refreshDisplaySongs(mList: Array<VideoItem>): void {
-    this.dataSource.pushArrayData(mList)
-    if(mList.length > 0){
+  private refreshDisplaySongs(mList: Array<VideoItem>, shouldScrollToTop: boolean = true): void {
+    const reloadToken: number = ++this.displayReloadToken;
+    const displayItems: Array<VideoItem> = mList ? [...mList] : [];
+    if (displayItems.length <= WEBDAV_PROGRESSIVE_RELOAD_THRESHOLD) {
+      this.dataSource.pushArrayData(displayItems)
+      if (shouldScrollToTop && displayItems.length > 0) {
+        setTimeout(() => {
+          if (reloadToken !== this.displayReloadToken) {
+            return
+          }
+          this.listScroller.scrollToIndex(0)
+        }, 120)
+      }
+      return
+    }
+
+    const initialItems: Array<VideoItem> = displayItems.slice(0, WEBDAV_PROGRESSIVE_RELOAD_INITIAL_COUNT)
+    this.dataSource.pushArrayData(initialItems)
+    if (shouldScrollToTop && initialItems.length > 0) {
       setTimeout(() => {
+        if (reloadToken !== this.displayReloadToken) {
+          return
+        }
         this.listScroller.scrollToIndex(0)
-      },200)
+      }, 80)
+    }
 
+    let cursor: number = initialItems.length
+    const appendNextBatch = (): void => {
+      if (reloadToken !== this.displayReloadToken) {
+        return
+      }
+      if (cursor >= displayItems.length) {
+        return
+      }
+      const nextBatch: Array<VideoItem> = displayItems.slice(cursor, cursor + WEBDAV_PROGRESSIVE_RELOAD_BATCH_SIZE)
+      this.dataSource.appendArrayData(nextBatch)
+      cursor += nextBatch.length
+      if (cursor < displayItems.length) {
+        setTimeout(() => {
+          appendNextBatch()
+        }, WEBDAV_PROGRESSIVE_RELOAD_DELAY_MS)
+      }
     }
+    setTimeout(() => {
+      appendNextBatch()
+    }, WEBDAV_PROGRESSIVE_RELOAD_DELAY_MS)
+  }
 
+  private cancelPendingDirectoryRefresh(): void {
+    if (this.pendingDirectoryRefreshTimer >= 0) {
+      clearTimeout(this.pendingDirectoryRefreshTimer)
+      this.pendingDirectoryRefreshTimer = -1
+    }
+    this.pendingDirectoryRefreshToken += 1
+  }
+
+  private scheduleDirectoryRefresh(previewPath: string | undefined, loadAction: () => Promise<void>,
+    errorPrefix: string): void {
+    this.cancelPendingDirectoryRefresh()
+    const refreshToken: number = this.pendingDirectoryRefreshToken
+    const hasPreview: boolean = previewPath !== undefined ? this.showDirectoryPreviewCache(previewPath) : false
+    this.isLoading = !hasPreview
+    this.isRefreshingCache = false
+
+    const executeLoad = (): void => {
+      if (refreshToken !== this.pendingDirectoryRefreshToken) {
+        return
+      }
+      void loadAction().catch((error: Error) => {
+        if (refreshToken !== this.pendingDirectoryRefreshToken) {
+          return
+        }
+        Logger.error(TAG, `${errorPrefix}: ${error.message}`)
+        this.isLoading = false
+        this.isRefreshingCache = false
+      })
+    }
+
+    if (!hasPreview) {
+      executeLoad()
+      return
+    }
+
+    this.pendingDirectoryRefreshTimer = setTimeout(() => {
+      this.pendingDirectoryRefreshTimer = -1
+      executeLoad()
+    }, 220) as number
   }
 
   private buildThumbnailIdentity(item: VideoItem): string {
@@ -463,7 +552,6 @@ export struct WebDavMainPage {
     if (dataIndex >= 0) {
       this.dataSource.notifyDataChange(dataIndex);
     }
-    this.listRefreshKey += 1;
   }
 
   private async prepareRemoteThumbSource(item: VideoItem): Promise<RemoteThumbSource> {
@@ -590,11 +678,15 @@ export struct WebDavMainPage {
     if (mediaItems.length <= 0) {
       return;
     }
-    const limitCount: number = Math.min(mediaItems.length, REMOTE_THUMB_MAX_TASK_COUNT);
+    const isLargeList: boolean = mediaItems.length >= REMOTE_THUMB_LARGE_LIST_THRESHOLD;
+    const limitCount: number = isLargeList ?
+      Math.min(mediaItems.length, REMOTE_THUMB_LARGE_LIST_PREFETCH_COUNT) :
+      Math.min(mediaItems.length, REMOTE_THUMB_MAX_TASK_COUNT);
     const targetItems: VideoItem[] = mediaItems.slice(0, limitCount);
+    const delayMs: number = isLargeList ? REMOTE_THUMB_LARGE_LIST_DELAY_MS : 120;
     setTimeout((): void => {
       void this.runRemoteThumbPrefetchQueue(targetItems, token);
-    }, 120);
+    }, delayMs);
   }
 
   private async runRemoteThumbPrefetchQueue(items: VideoItem[], token: number): Promise<void> {
@@ -1189,7 +1281,7 @@ export struct WebDavMainPage {
     this.isLoading = true;
     try {
       await this.webdavManager.renameRemoteSong(account, song, nextName);
-      await this.webdavManager.loadFilesInfoFromAccount(account, this.webdavManager.currentPath);
+      await this.webdavManager.loadFilesInfoFromAccount(account, this.currentDirectoryPath || account.filepath || '/');
       this.getUIContext().getPromptAction().showToast({ message: '重命名成功' });
     } catch (error) {
       const err = error as Error;
@@ -1216,7 +1308,8 @@ export struct WebDavMainPage {
     this.isLoading = true;
     try {
       await this.webdavManager.renameRemoteFolder(this.selectedAccount, folder, nextName);
-      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount,
+        this.currentDirectoryPath || this.selectedAccount.filepath || '/');
       this.getUIContext().getPromptAction().showToast({ message: '重命名成功' });
     } catch (error) {
       const err = error as Error;
@@ -1230,8 +1323,9 @@ export struct WebDavMainPage {
     if (!song) {
       return;
     }
+    const displayQueue: VideoItem[] = this.getCurrentPlaybackQueue();
     const queue: VideoItem[] = globalWebdavVideoItems && globalWebdavVideoItems.length > 0 ?
-      globalWebdavVideoItems : this.songs;
+      globalWebdavVideoItems : displayQueue;
     if (!queue || queue.length === 0) {
       this.getUIContext().getPromptAction().showToast({ message: '当前列表为空' });
       return;
@@ -1239,7 +1333,7 @@ export struct WebDavMainPage {
 
     const currentPath = this.currentSong?.filePath;
     if (StrUtil.isEmpty(currentPath)) {
-      const targetIndex = this.songs.findIndex((item: VideoItem) => item.filePath === song.filePath);
+      const targetIndex = displayQueue.findIndex((item: VideoItem) => item.filePath === song.filePath);
       if (targetIndex >= 0) {
         this.playSong(song, targetIndex, false);
         this.getUIContext().getPromptAction().showToast({ message: '已开始播放' });
@@ -2047,6 +2141,7 @@ export struct WebDavMainPage {
 
   aboutToDisappear(): void {
     // 取消订阅
+    this.cancelPendingDirectoryRefresh();
     this.thumbnailTaskToken += 1;
     this.thumbnailRunningKeys.clear();
     this.webdavManager.unsubscribe(this.eventHandler);
@@ -2058,12 +2153,23 @@ export struct WebDavMainPage {
   // 处理WebDAV事件
   private handleWebdavEvent(event: string): void {
     switch (event) {
+      case RemoteDriveManagerStates.LoadFilesInfoStart:
+        if (this.dataSource.totalCount() > 0 || this.visibleFoldersState.length > 0) {
+          this.isLoading = false;
+          this.isRefreshingCache = true;
+        } else {
+          this.isLoading = true;
+          this.isRefreshingCache = false;
+        }
+        break;
       case RemoteDriveManagerStates.LoadFilesInfoSucceed:
+        const shouldScrollToTop: boolean = !this.isRefreshingCache;
         this.songs = this.webdavManager.webDavSongs;
-        this.updateListData(this.songs)
         // 直接引用webdavManager的数组,避免@Observed序列化问题
         this.webDavFiles = this.webdavManager.webDavFiles;
+        this.currentDirectoryPath = this.webdavManager.currentPath || '/';
         this.isLoading = false;
+        this.isRefreshingCache = false;
 
         // 更新可见文件夹列表
         this.updateVisibleFolders();
@@ -2073,7 +2179,7 @@ export struct WebDavMainPage {
         if (this.isSearchMode && this.searchText.length > 0) {
           void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
         } else {
-          this.restoreCurrentDirectorySearchView();
+          this.restoreCurrentDirectorySearchView(shouldScrollToTop);
         }
 
         // promptAction.showToast({
@@ -2082,6 +2188,7 @@ export struct WebDavMainPage {
         break;
       case RemoteDriveManagerStates.LoadFilesInfoFailed:
         this.isLoading = false;
+        this.isRefreshingCache = false;
         // this.getUIContext().getPromptAction().showToast({ message: '加载失败' });
         break;
       case RemoteDriveManagerStates.InsertAccountSucceed:
@@ -2235,6 +2342,47 @@ export struct WebDavMainPage {
     return clone;
   }
 
+  private cloneFolderInfo(item: FileInfo): FileInfo {
+    const clone = new FileInfo(item.rootpath, item.name, item.totalSize, item.time);
+    clone.readOnly = item.readOnly;
+    clone.fileName = item.fileName;
+    clone.href = item.href;
+    clone.contentLength = item.contentLength;
+    clone.isDirectory = item.isDirectory;
+    return clone;
+  }
+
+  private applyDirectoryPreviewSnapshot(snapshot: RemoteDirectorySnapshot, targetPath: string): void {
+    const previewSongs: VideoItem[] = snapshot.songs.map((item: VideoItem) => this.cloneSong(item));
+    const previewFiles: FileInfo[] = snapshot.files.map((item: FileInfo) => this.cloneFolderInfo(item));
+    this.songs = previewSongs;
+    this.webDavFiles = previewFiles;
+    this.currentDirectoryPath = targetPath;
+    this.webdavManager.webDavSongs = previewSongs.slice();
+    this.webdavManager.webDavFiles = previewFiles.slice();
+    this.webdavManager.currentPath = targetPath;
+    this.updateVisibleFolders();
+    this.breadcrumbs = this.webdavManager.getBreadcrumbsForPreview(this.selectedAccount, targetPath);
+    if (this.isSearchMode && this.searchText.length > 0) {
+      void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
+      return;
+    }
+    this.restoreCurrentDirectorySearchView();
+  }
+
+  private showDirectoryPreviewCache(targetPath?: string): boolean {
+    if (!this.selectedAccount) {
+      return false;
+    }
+    const resolvedPath = targetPath && targetPath.length > 0 ? targetPath : (this.selectedAccount.filepath || '/');
+    const preview = this.webdavManager.getDirectoryPreview(this.selectedAccount, resolvedPath);
+    if (!preview) {
+      return false;
+    }
+    this.applyDirectoryPreviewSnapshot(preview, resolvedPath);
+    return true;
+  }
+
   // 加载账户列表
   private loadAccounts(): void {
     this.accounts = this.webdavManager.getAllWebDavAccounts();
@@ -2247,29 +2395,19 @@ export struct WebDavMainPage {
       this.getUIContext().getPromptAction().showToast({ message: '请先选择账户' });
       return;
     }
-
-    this.isLoading = true;
     console.info('heanup '+this.selectedAccount.name+'type '+this.selectedAccount.webType)
-    this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount)
-      .catch((error: Error) => {
-        Logger.error(TAG, '加载文件失败: ' + error.message);
-        this.isLoading = false;
-      });
+    this.scheduleDirectoryRefresh(this.selectedAccount.filepath,
+      () => this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount), '加载文件失败')
   }
 
   // 进入文件夹
   private enterFolder(folder: FileInfo): void {
-    this.isLoading = true;
-    this.webdavManager.enterFolder(folder)
-      .catch((error: Error) => {
-        Logger.error(TAG, '进入文件夹失败: ' + error.message);
-        this.isLoading = false;
-      });
+    this.scheduleDirectoryRefresh(folder.href, () => this.webdavManager.enterFolder(folder), '进入文件夹失败')
   }
 
   // 检查是否为当前目录的直接子项
   private isDirectChildOfCurrentPath(folder: FileInfo): boolean {
-    const currentPath = this.webdavManager.currentPath || '';
+    const currentPath = this.currentDirectoryPath || '';
 
     // 如果在根目录(currentPath为空或'/'),显示所有第一级文件夹
     if (currentPath === '' || currentPath === '/') {
@@ -2293,13 +2431,8 @@ export struct WebDavMainPage {
   // 返回上级目录
   private goBack(): void {
     if(this.webdavManager.canGoBack()){
-      this.isLoading = true;
-      this.webdavManager.goBack()
-        .catch((error: Error) => {
-          Logger.error(TAG, '返回失败: ' + error.message);
-          this.isLoading = false;
-        });
-      this.breadcrumbs = this.webdavManager.getBreadcrumbs();
+      const previousPath: string | undefined = this.webdavManager.pathHistory[this.webdavManager.pathHistory.length - 1];
+      this.scheduleDirectoryRefresh(previousPath, () => this.webdavManager.goBack(), '返回失败')
     }else{
       this.getUIContext().animateTo({ duration: 555 }, () => {
         // 动画闭包内控制Image组件的出现和消失
@@ -2318,18 +2451,25 @@ export struct WebDavMainPage {
     this.updateListData(this.songs)
   }
 
-  private resolveSongPlayIndex(song: VideoItem, fallbackIndex: number): number {
-    if (this.songs.length <= 0) {
+  private getCurrentPlaybackQueue(): VideoItem[] {
+    if (this.isSearchMode && this.searchText.length > 0 && this.filteredList.length > 0) {
+      return this.filteredList;
+    }
+    return this.songs;
+  }
+
+  private resolveSongPlayIndex(song: VideoItem, fallbackIndex: number, queue: VideoItem[]): number {
+    if (queue.length <= 0) {
       return -1;
     }
     if (song && StrUtil.isNotEmpty(song.filePath)) {
-      for (let i = 0; i < this.songs.length; i++) {
-        if (this.songs[i].filePath === song.filePath) {
+      for (let i = 0; i < queue.length; i++) {
+        if (queue[i].filePath === song.filePath) {
           return i;
         }
       }
     }
-    if (fallbackIndex >= 0 && fallbackIndex < this.songs.length) {
+    if (fallbackIndex >= 0 && fallbackIndex < queue.length) {
       return fallbackIndex;
     }
     return 0;
@@ -2338,11 +2478,12 @@ export struct WebDavMainPage {
   // 播放WebDAV歌曲
   private playSong(song: VideoItem, index: number,isJump:boolean=false): void {
     try {
+      const queue: VideoItem[] = this.getCurrentPlaybackQueue();
       Logger.info(TAG, `heanup === WebDAV playSong 方法被调用 ===`);
       Logger.info(TAG, 'heanup 播放指定歌曲: ' + song.name + ', 索引: ' + index);
-      Logger.info(TAG, 'heanup 歌曲列表长度: ' + this.songs.length);
-      const playIndex = this.resolveSongPlayIndex(song, index);
-      if (playIndex < 0 || playIndex >= this.songs.length) {
+      Logger.info(TAG, 'heanup 播放队列长度: ' + queue.length);
+      const playIndex = this.resolveSongPlayIndex(song, index, queue);
+      if (playIndex < 0 || playIndex >= queue.length) {
         this.getUIContext().getPromptAction().showToast({ message: '播放失败,未找到歌曲' });
         Logger.warn(TAG, `heanup 无法定位播放索引: fallback=${index}, songPath=${song.filePath}`);
         return;
@@ -2356,11 +2497,11 @@ export struct WebDavMainPage {
 
       // 确保所有歌曲都设置了正确的webdav_account_id
       if (this.selectedAccount && this.selectedAccount.id) {
-        const accountIds = this.songs.map(s => s.webdav_account_id).filter(id => id);
-        Logger.info(TAG, `heanup 当前歌曲列表中有 ${accountIds.length}/${this.songs.length} 首歌曲设置了webdav_account_id`);
+        const accountIds = queue.map(s => s.webdav_account_id).filter(id => id);
+        Logger.info(TAG, `heanup 当前歌曲列表中有 ${accountIds.length}/${queue.length} 首歌曲设置了webdav_account_id`);
 
         // 如果发现歌曲缺少webdav_account_id,立即设置
-        this.songs.forEach((item, idx) => {
+        queue.forEach((item) => {
           if (!item.webdav_account_id) {
             item.webdav_account_id = this.selectedAccount!.id.toString();
             Logger.info(TAG, `heanup 为歌曲 "${item.name}" 设置webdav_account_id: ${item.webdav_account_id}`);
@@ -2369,11 +2510,11 @@ export struct WebDavMainPage {
       }
 
       // 直接使用当前的VideoItem数组
-      const videoItems: VideoItem[] = this.songs;
+      const videoItems: VideoItem[] = queue.slice();
       const songFilePaths: string[] = [];
 
-      for (let i = 0; i < this.songs.length; i++) {
-        const item = this.songs[i];
+      for (let i = 0; i < videoItems.length; i++) {
+        const item = videoItems[i];
         songFilePaths.push(item.filePath); // 使用filePath作为文件路径
       }
 
@@ -2383,7 +2524,7 @@ export struct WebDavMainPage {
       const playlistData: PlaylistEventData = {
         playlistId: 'webdav-playlist', // 使用特殊的ID标识网盘播放列表
         playlistName: `${getRemoteDriveDisplayLabel(this.selectedAccount?.webType)} - ${this.selectedAccount?.name || '未知账户'}`,
-        songCount: this.songs.length,
+        songCount: videoItems.length,
         startIndex: playIndex,
         isJump: isJump,//设置true会弹出播放页
         songFilePaths: songFilePaths
@@ -2437,7 +2578,7 @@ export struct WebDavMainPage {
       }
 
       // 构建歌单名称:账户名 + 当前路径(简化)
-      const rawPath = this.webdavManager.currentPath || '/';
+      const rawPath = this.currentDirectoryPath || '/';
       const shortPath = rawPath === '/' ? '根目录' : rawPath.replace(/\/$/, '').split('/').slice(-1)[0];
       const playlistName = `${getRemoteDrivePlaylistPrefix(this.selectedAccount.webType)}-${this.selectedAccount.name}-${shortPath}`;
 
@@ -2532,13 +2673,8 @@ export struct WebDavMainPage {
     if (!crumb) {
       return;
     }
-    try {
-      this.isLoading = true;
-      await this.webdavManager.enterFolderFromPath(crumb.path);
-    } catch (error) {
-      Logger.error(TAG, '导航到面包屑路径失败: ' + (error as Error).message);
-      this.isLoading = false;
-    }
+    this.scheduleDirectoryRefresh(crumb.path, () => this.webdavManager.enterFolderFromPath(crumb.path),
+      '导航到面包屑路径失败')
   }
 
 
@@ -2710,21 +2846,21 @@ export struct WebDavMainPage {
     return target;
   }
 
-  doSortType(index: number) {
+  doSortType(index: number, shouldScrollToTop: boolean = true) {
     this.sortType = index;
-    this.sortSongsForType(this.songs, index)
-    this.sortFoldersForType(this.visibleFoldersState, index)
-    if (this.filteredList.length > 0) {
-      this.filteredList = this.sortSongsForType([...this.filteredList], index)
-    }
-    if (this.filteredFolderList.length > 0) {
-      this.filteredFolderList = this.sortFoldersForType([...this.filteredFolderList], index)
-    }
     if (this.isSearchMode && this.searchText.length > 0) {
-      this.refreshDisplaySongs(this.filteredList)
+      if (this.filteredList.length > 0) {
+        this.filteredList = this.sortSongsForType([...this.filteredList], index)
+      }
+      if (this.filteredFolderList.length > 0) {
+        this.filteredFolderList = this.sortFoldersForType([...this.filteredFolderList], index)
+      }
+      this.refreshDisplaySongs(this.filteredList, shouldScrollToTop)
       return
     }
-    this.updateListData(this.songs,true)
+    this.songs = this.sortSongsForType([...this.songs], index)
+    this.visibleFoldersState = this.sortFoldersForType([...this.visibleFoldersState], index)
+    this.updateListData(this.songs,true, shouldScrollToTop)
   }
 
   private getSortItemBackground(sortType: number): ResourceColor {
@@ -2974,11 +3110,13 @@ export struct WebDavMainPage {
     this.onSearchInput(keyword);
   }
 
-  private restoreCurrentDirectorySearchView(): void {
-    this.filteredList = this.sortSongsForType([...this.songs], this.sortType)
-    this.filteredFolderList = this.sortFoldersForType([...this.visibleFoldersState], this.sortType)
+  private restoreCurrentDirectorySearchView(shouldScrollToTop: boolean = true): void {
+    this.filteredList = []
+    this.filteredFolderList = []
+    this.songs = this.sortSongsForType([...this.songs], this.sortType)
+    this.visibleFoldersState = this.sortFoldersForType([...this.visibleFoldersState], this.sortType)
     this.isSearchLoading = false
-    this.refreshDisplaySongs(this.filteredList)
+    this.refreshDisplaySongs(this.songs, shouldScrollToTop)
     this.syncSelectionAfterRefresh()
   }
 
@@ -3289,7 +3427,7 @@ export struct WebDavMainPage {
   }
 
   private getSearchLoadingTopOffset(): number {
-    return this.topSafeHeight + (this.webdavManager.currentPath !== '' ? 100 : 56)
+    return this.topSafeHeight + (this.currentDirectoryPath !== '' ? 100 : 56)
   }
 
   private shouldShowSearchIndexingTip(): boolean {
@@ -3310,7 +3448,7 @@ export struct WebDavMainPage {
     // 面包屑导航
     Column({ space: 8 }) {
       // 面包屑导航
-      if (this.webdavManager.currentPath !== '') {
+      if (this.currentDirectoryPath !== '') {
         Row({ space: 8 }) {
           Button({ type: ButtonType.Circle }) {
             Image(this.selectedAccount.coverPath?
@@ -3429,7 +3567,8 @@ export struct WebDavMainPage {
 
 
       // 文件列表(文件夹 + 歌曲)
-      if (this.webDavFiles.length > 0 || this.songs.length > 0) {
+      if ((this.isSearchMode ? this.filteredFolderList.length : this.visibleFoldersState.length) > 0 ||
+        this.dataSource.totalCount() > 0) {
         List({ scroller: this.listScroller ,space: 0 }) {
           // 显示文件夹 - 只显示当前目录下的直接子文件夹
           ForEach(this.isSearchMode ? this.filteredFolderList : this.visibleFoldersState, (folder: FileInfo) => {
@@ -3988,7 +4127,8 @@ export struct WebDavMainPage {
       this.isLoading = true;
 
       // 重新加载当前账户的文件信息
-      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount,
+        this.currentDirectoryPath || this.selectedAccount.filepath || '/');
 
 
       Logger.info(TAG, 'heanup 文件列表刷新完成');
@@ -4045,7 +4185,8 @@ export struct WebDavMainPage {
       }
 
       // 创建成功后刷新当前目录
-      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
+      await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount,
+        this.currentDirectoryPath || this.selectedAccount.filepath || '/');
       this.getUIContext().getPromptAction().showToast({
         message: `文件夹 "${folderName}" 创建成功`
       });