import { RemoteDriveManager } from '../common/util/RemoteDriveManager'; import { WebDavAccount } from '../viewmodel/WebDavAccount'; import { VideoItem } from '../viewmodel/VideoItem'; import { FileInfo } from '../viewmodel/FileInfo'; import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates'; import Logger from '../common/util/Logger'; import { router } from '@kit.ArkUI'; import { CommonConstants } from '../common/constants/CommonConstants'; import { Constants } from '../Constants'; import { picker, fileUri } from '@kit.CoreFileKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { PreferencesUtil } from '@pura/harmony-utils'; import { UploadTaskDataSource } from '../viewmodel/UploadTask'; import { promptAction } from '@kit.ArkUI'; import fs from '@ohos.file.fs'; import ReqPermissionUtil from '../common/util/ReqPermissionUtil'; import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil'; import { SegmentButton } from '@kit.ArkUI'; import { SegmentButtonOptions, SegmentButtonItemTuple } from '@kit.ArkUI'; const TAG = 'heanup UploadMusicPage'; export interface LocalFileItem { name: string; path: string; isDirectory: boolean; isAudioFile: boolean; } interface LocalAudioFile { fullPath: string; relativePath: string; } @Entry @Component export struct UploadMusicPage { @StorageProp('topSafeHeight') topSafeHeight: number = 0; @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0; @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR; @StorageProp('isDarkMode') isDarkMode: boolean = false; // 页面状态变量 @State selectedFiles: string[] = []; @State selectedFileNames: string[] = []; @State selectedFileTypes: string[] = []; // 'file' 或 'folder' @State accounts: WebDavAccount[] = []; // 文件浏览器状态 @State isShowFileBrowser: boolean = false; @State fileBrowserPath: string = ''; @State browserFiles: LocalFileItem[] = []; @State selectedBrowserItems: Set = new Set(); @State isLoadingFiles: boolean = false; @State selectedAccount: WebDavAccount | null = null; @State uploadPath: string = '/'; @State availablePaths: string[] = []; @State duplicateAction: string = 'skip'; // SegmentButton 重复文件处理选项 @State duplicateOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({ buttons: [{ text: '跳过' }, { text: '覆盖' }, { text: '重命名' }] as SegmentButtonItemTuple, backgroundColor: $r('app.color.index_background'), selectedBackgroundColor: $r('app.color.start_window_background'), selectedFontColor: $r('app.color.text_color'), buttonPadding: { top: 10, bottom: 10 }, multiply: false }); @State @Watch('onDuplicateActionChange') selectedDuplicateIndex: number[] = [0]; @State uploadProgress: number = 0; @State uploadSpeed: string = '0 KB/s'; @State uploadedCount: number = 0; @State totalUploadCount: number = 0; @State isUploading: boolean = false; @State uploadQueue: VideoItem[] = []; @State finishQueue: VideoItem[] = []; @State currentTask: VideoItem | null = null; @State isShowPathBrowser: boolean = false; @State currentBrowsePath: string = '/'; @State browseFolders: FileInfo[] = []; @State isLoadingFolders: boolean = false; @State pendingCount: number = 0; @State finishedCount: number = 0; // RemoteDriveManager实例 private webdavManager: RemoteDriveManager = RemoteDriveManager.getInstance(); // 性能优化:使用LazyDataSource优化队列列表渲染 // LazyForEach配合IDataSource可以实现按需加载,避免一次性渲染大量列表项 // 对于包含数百个上传任务的队列,可以显著提升UI响应性能 private uploadQueueDataSource: UploadTaskDataSource = new UploadTaskDataSource(); private finishQueueDataSource: UploadTaskDataSource = new UploadTaskDataSource(); // 事件处理器引用 private eventHandler: (event: string) => void = (event: string) => { this.handleUploadEvent(event); }; private cachedDownloadRoot: string = ''; private localBrowseRoot: string = ''; // 重复文件处理方式变化监听 onDuplicateActionChange() { if (this.selectedDuplicateIndex.length > 0) { const actionMap = ['skip', 'overwrite', 'rename']; this.duplicateAction = actionMap[this.selectedDuplicateIndex[0]]; Logger.info(TAG, `重复文件处理方式变更为: ${this.duplicateAction}`); } } // 获取当前重复文件处理方式的描述 private getActionDescription(): string { switch (this.duplicateAction) { case 'skip': return '保留远程文件,不上传'; case 'overwrite': return '替换远程文件'; case 'rename': return '自动重命名后上传'; default: return '保留远程文件,不上传'; } } aboutToAppear(): void { Logger.info(TAG, '页面即将显示'); // 加载用户设置 this.loadUserSettings(); // 加载WebDAV账户列表 this.loadAccounts(); // 从路由参数获取预选账户ID const params = router.getParams() as Record; 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'); // 根据加载的设置初始化SegmentButton选择索引 const actionMap = ['skip', 'overwrite', 'rename']; const index = actionMap.indexOf(this.duplicateAction); this.selectedDuplicateIndex = [index >= 0 ? index : 0]; Logger.info(TAG, `加载用户设置 - 重复文件处理: ${this.duplicateAction}`); } catch (error) { const err = error as Error; Logger.error(TAG, `加载用户设置失败: ${err.message}`); } } aboutToDisappear(): void { Logger.info(TAG, '页面即将销毁'); // 取消订阅事件 this.unsubscribeUploadEvents(); } /** * 加载WebDAV账户列表 */ private loadAccounts(): void { try { this.accounts = this.webdavManager.getAllWebDavAccounts(); Logger.info(TAG, `加载了 ${this.accounts.length} 个WebDAV账户`); // 如果没有选中账户且有可用账户,默认选择第一个 if (!this.selectedAccount && this.accounts.length > 0) { this.selectedAccount = this.accounts[0]; this.applyDefaultUploadPath(this.selectedAccount); } } catch (error) { const err = error as Error; Logger.error(TAG, `加载账户列表失败: ${err.message}`); } } private getPreferredDownloadRoot(): string { if (this.cachedDownloadRoot) { return this.cachedDownloadRoot; } try { let saved = PreferencesUtil.getStringSync('download_path', ''); if (saved && saved.length > 0) { this.cachedDownloadRoot = this.normalizeLocalDirectory(saved); if (this.cachedDownloadRoot) { return this.cachedDownloadRoot; } } } catch (error) { const err = error as Error; Logger.warn(TAG, `读取download_path失败: ${err.message}`); } return ''; } private normalizeLocalDirectory(path: string): string { if (!path || path.length === 0) { return ''; } let normalized = path.replace(/\\/g, '/'); normalized = normalized.replace(/\/+$/, ''); return normalized; } private ensureDirectoryAccessible(path: string): boolean { if (!path || path.length === 0) { return false; } try { const stat = fs.statSync(path); return stat.isDirectory(); } catch (_error) { try { fs.mkdirSync(path); return true; } catch (createErr) { const err = createErr as Error; Logger.warn(TAG, `创建目录失败(${path}): ${err.message}`); return false; } } } private async requestDownloadRootFromSystem(force: boolean = false): Promise { try { const documentViewPicker = new picker.DocumentViewPicker(); const documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD }); if (documentSaveResult && documentSaveResult.length > 0) { const resolvedPath = new fileUri.FileUri(documentSaveResult[0]).path; const normalized = this.normalizeLocalDirectory(resolvedPath); if (normalized) { this.cachedDownloadRoot = normalized; const storedValue = normalized.endsWith('/') ? normalized : `${normalized}/`; PreferencesUtil.putSync('download_path', storedValue); await ReqPermissionUtil.persistPermission(documentSaveResult[0]); return normalized; } } } catch (error) { const err = error as Error; Logger.error(TAG, `请求系统下载目录失败: ${err.message}`); } return ''; } private async resolveInitialBrowserPath(): Promise { let preferred = this.getPreferredDownloadRoot(); if (preferred && this.ensureDirectoryAccessible(preferred)) { return preferred; } const requested = await this.requestDownloadRootFromSystem(); if (requested && this.ensureDirectoryAccessible(requested)) { return requested; } promptAction.showToast({ message: '请先在本地音乐页授权下载目录后再使用此功能', duration: 2000 }); throw new Error('未授权任何本地目录'); } private normalizeBrowsePath(path: string): string { if (!path || path.trim().length === 0) { return '/'; } let normalized = path.trim().replace(/\\/g, '/'); if (!normalized.startsWith('/')) { normalized = `/${normalized}`; } normalized = normalized.replace(/\/+/g, '/'); if (normalized.length > 1 && normalized.endsWith('/')) { normalized = normalized.slice(0, -1); } return normalized || '/'; } /** * 预选指定账户 * @param accountId 账户ID */ private preselectAccount(accountId: number): void { try { for (let i = 0; i < this.accounts.length; i++) { const account = this.accounts[i]; if (account.id === accountId) { this.selectedAccount = account; this.applyDefaultUploadPath(account); Logger.info(TAG, `预选账户: ${account.name}`); break; } } } catch (error) { const err = error as Error; Logger.error(TAG, `预选账户失败: ${err.message}`); } } /** * 订阅RemoteDriveManager的上传事件 */ private subscribeUploadEvents(): void { this.webdavManager.subscribe(this.eventHandler); Logger.info(TAG, '已订阅上传事件'); } /** * 取消订阅RemoteDriveManager的上传事件 */ private unsubscribeUploadEvents(): void { this.webdavManager.unsubscribe(this.eventHandler); Logger.info(TAG, '已取消订阅上传事件'); } /** * 处理上传事件 * @param event 事件类型 */ private handleUploadEvent(event: string): void { Logger.info(TAG, `收到上传事件: ${event}`); switch (event) { case RemoteDriveManagerStates.UploadStart: this.handleUploadStart(); break; case RemoteDriveManagerStates.UploadProgress: this.handleUploadProgress(); break; case RemoteDriveManagerStates.UploadSuccess: this.handleUploadSuccess(); break; case RemoteDriveManagerStates.UploadFailed: this.handleUploadFailed(); break; case RemoteDriveManagerStates.UploadPaused: this.handleUploadPaused(); break; case RemoteDriveManagerStates.UploadResumed: this.handleUploadResumed(); break; case RemoteDriveManagerStates.SetCurrentUploadTask: this.handleCurrentTaskChanged(); break; case RemoteDriveManagerStates.ChangeUploadQueue: this.handleUploadQueueChanged(); break; case RemoteDriveManagerStates.ChangeFinishUploadQueue: this.handleFinishQueueChanged(); break; } } /** * 处理上传开始事件 */ private handleUploadStart(): void { this.isUploading = true; Logger.info(TAG, '上传开始'); } /** * 处理上传进度更新事件 */ private handleUploadProgress(): void { const uploaded = this.webdavManager.uploadReceivedSize; const total = this.webdavManager.uploadTotalSize; if (total > 0) { this.uploadProgress = Math.floor((uploaded / total) * 100); // 计算上传速度(简化版,实际应该基于时间差计算) const speedMBps = uploaded / (1024 * 1024); this.uploadSpeed = `${speedMBps.toFixed(2)} MB/s`; } Logger.info(TAG, `上传进度: ${this.uploadProgress}%`); } /** * 处理上传成功事件 */ private handleUploadSuccess(): void { this.uploadedCount++; Logger.info(TAG, `上传成功,已完成: ${this.uploadedCount}/${this.totalUploadCount}`); } /** * 处理上传失败事件 */ private handleUploadFailed(): void { Logger.error(TAG, '上传失败'); } /** * 处理上传暂停事件 */ private handleUploadPaused(): void { Logger.info(TAG, '上传已暂停'); } /** * 处理上传恢复事件 */ private handleUploadResumed(): void { Logger.info(TAG, '上传已恢复'); } /** * 处理当前任务变更事件 */ private handleCurrentTaskChanged(): void { const task = this.webdavManager.currentUploadTask; if (task) { this.currentTask = task.song; Logger.info(TAG, `当前上传任务: ${task.song.name}`); } else { this.currentTask = null; } } /** * 处理上传队列变更事件 */ private handleUploadQueueChanged(): void { const tasks = this.webdavManager.uploadQueue; this.uploadQueue = []; for (let i = 0; i < tasks.length; i++) { this.uploadQueue.push(tasks[i].song); } this.pendingCount = this.uploadQueue.length; if (this.pendingCount === 0) { this.isUploading = false; this.currentTask = null; this.uploadProgress = 0; this.uploadSpeed = '0 KB/s'; } this.uploadQueueDataSource.updateTasks(this.uploadQueue); Logger.info(TAG, `上传队列更新,当前数量: ${this.pendingCount}`); } /** * 处理完成队列变更事件 */ private handleFinishQueueChanged(): void { const tasks = this.webdavManager.finishUploadQueue; this.finishQueue = []; for (let i = 0; i < tasks.length; i++) { this.finishQueue.push(tasks[i].song); } this.finishedCount = this.finishQueue.length; this.finishQueueDataSource.updateTasks(this.finishQueue); Logger.info(TAG, `完成队列更新,当前数量: ${this.finishedCount}`); } /** * 打开文件选择器 */ private async openFilePicker(): Promise { try { const documentSelectOptions = new picker.DocumentSelectOptions(); // 设置音频文件过滤器 documentSelectOptions.fileSuffixFilters = Constants.AUDIO_EXTENSIONS; // 支持多选 documentSelectOptions.maxSelectNumber = 100; const documentPicker = new picker.DocumentViewPicker(); const documentSelectResult = await documentPicker.select(documentSelectOptions); if (documentSelectResult && documentSelectResult.length > 0) { for (let i = 0; i < documentSelectResult.length; i++) { try { await ReqPermissionUtil.persistPermission(documentSelectResult[i]); } catch (error) { const err = error as Error; Logger.error(TAG, `持久化授权失败: ${err.message}`); } } Logger.info(TAG, `选择了 ${documentSelectResult.length} 个文件`); // 保存选中的文件URI this.selectedFiles = documentSelectResult; // 提取文件名用于显示 this.selectedFileNames = []; this.selectedFileTypes = []; for (let i = 0; i < documentSelectResult.length; i++) { const uri = documentSelectResult[i]; try { const fileUriObj = new fileUri.FileUri(uri); const fileName = fileUriObj.name; this.selectedFileNames.push(fileName); this.selectedFileTypes.push('file'); } catch (error) { const err = error as Error; Logger.error(TAG, `解析文件URI失败: ${err.message}`); this.selectedFileNames.push('未知文件'); this.selectedFileTypes.push('file'); } } Logger.info(TAG, `文件列表: ${JSON.stringify(this.selectedFileNames)}`); } } catch (error) { const err = error as BusinessError; Logger.error(TAG, `文件选择失败: ${err.message}`); } } /** * 打开文件浏览器 */ private async openFileBrowser(): Promise { try { const initialPath = await this.resolveInitialBrowserPath(); if (!initialPath) { promptAction.showToast({ message: '无法定位到下载目录,请授予文件访问权限', duration: 2000 }); return; } this.fileBrowserPath = initialPath; this.localBrowseRoot = initialPath; this.selectedBrowserItems.clear(); this.isShowFileBrowser = true; await this.loadLocalFiles(initialPath); Logger.info(TAG, `文件浏览器已定位到: ${initialPath}`); } catch (error) { const err = error as Error; Logger.error(TAG, `打开文件浏览器失败: ${err.message}`); promptAction.showToast({ message: err.message || '打开文件浏览器失败', duration: 2000 }); } } /** * 加载本地文件列表 */ private async loadLocalFiles(path: string): Promise { try { this.isLoadingFiles = true; this.browserFiles = []; let targetPath = path; if (this.localBrowseRoot && !targetPath.startsWith(this.localBrowseRoot)) { targetPath = this.localBrowseRoot; } Logger.info(TAG, `加载目录: ${targetPath}`); const files = fs.listFileSync(targetPath); for (let i = 0; i < files.length; i++) { const fileName = files[i]; const fullPath = `${targetPath}/${fileName}`; try { const stat = fs.statSync(fullPath); const isDirectory = stat.isDirectory(); if (fileName.startsWith('.')) { continue; } const lowerFileName = fileName.toLowerCase(); const isAudioFile = !isDirectory && Constants.AUDIO_EXTENSIONS.some(ext => lowerFileName.endsWith(ext)); // 只显示文件夹和音频文件 if (isDirectory || isAudioFile) { this.browserFiles.push({ name: fileName, path: fullPath, isDirectory: isDirectory, isAudioFile: isAudioFile }); } } catch (error) { Logger.warn(TAG, `无法访问: ${fullPath}`); } } // 排序:文件夹在前,文件在后 this.browserFiles.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); Logger.info(TAG, `加载了 ${this.browserFiles.length} 个项目`); } catch (error) { const err = error as Error; Logger.error(TAG, `加载文件列表失败: ${err.message}`); promptAction.showToast({ message: `加载失败: ${err.message}`, duration: 2000 }); } finally { this.isLoadingFiles = false; } } /** * 进入子目录 */ private async enterDirectory(item: LocalFileItem): Promise { if (!item.isDirectory) { return; } if (this.localBrowseRoot && !item.path.startsWith(this.localBrowseRoot)) { promptAction.showToast({ message: '没有此目录的访问权限', duration: 2000 }); return; } this.fileBrowserPath = item.path; await this.loadLocalFiles(item.path); } /** * 返回上级目录 */ private async goBackDirectory(): Promise { if (!this.localBrowseRoot) { return; } if (this.fileBrowserPath === this.localBrowseRoot) { return; } const lastSlash = this.fileBrowserPath.lastIndexOf('/'); if (lastSlash > 0) { const parent = this.fileBrowserPath.substring(0, lastSlash); if (!parent.startsWith(this.localBrowseRoot)) { this.fileBrowserPath = this.localBrowseRoot; await this.loadLocalFiles(this.localBrowseRoot); return; } this.fileBrowserPath = parent; await this.loadLocalFiles(this.fileBrowserPath); } } /** * 切换项目选中状态 */ private toggleItemSelection(item: LocalFileItem): void { if (this.selectedBrowserItems.has(item.path)) { this.selectedBrowserItems.delete(item.path); } else { this.selectedBrowserItems.add(item.path); } // 触发UI更新 this.selectedBrowserItems = new Set(this.selectedBrowserItems); } /** * 确认选择文件 */ private async confirmFileSelection(): Promise { if (this.selectedBrowserItems.size === 0) { promptAction.showToast({ message: '请至少选择一个项目', duration: 2000 }); return; } // 将选中的项目添加到已选列表 const selectedItems: LocalFileItem[] = []; const selectedSet = new Set(); this.selectedBrowserItems.forEach((path) => { const found = this.browserFiles.find((f) => f.path === path); if (found && !selectedSet.has(found.path)) { selectedSet.add(found.path); selectedItems.push(found); } }); for (let i = 0; i < selectedItems.length; i++) { const item = selectedItems[i]; const uri = `file://${item.path}`; try { await ReqPermissionUtil.persistPermission(uri); } catch (error) { const err = error as Error; Logger.error(TAG, `持久化授权失败: ${err.message}`); } this.selectedFiles.push(uri); this.selectedFileNames.push(item.name); this.selectedFileTypes.push(item.isDirectory ? 'folder' : 'file'); } promptAction.showToast({ message: `已添加 ${this.selectedBrowserItems.size} 个项目`, duration: 2000 }); this.closeFileBrowser(); } /** * 关闭文件浏览器 */ private closeFileBrowser(): void { this.isShowFileBrowser = false; this.selectedBrowserItems.clear(); this.browserFiles = []; this.localBrowseRoot = ''; } /** * 递归扫描文件夹,获取所有音频文件 */ private async scanFolderForAudioFiles(folderPath: string, relativePrefix: string = ''): Promise { const audioFiles: LocalAudioFile[] = []; try { const files = fs.listFileSync(folderPath); for (let i = 0; i < files.length; i++) { const fileName = files[i]; const fullPath = `${folderPath}/${fileName}`; try { const stat = fs.statSync(fullPath); if (stat.isDirectory()) { const nextPrefix = relativePrefix ? `${relativePrefix}/${fileName}` : fileName; const subFiles = await this.scanFolderForAudioFiles(fullPath, nextPrefix); audioFiles.push(...subFiles); } else { const lowerFileName = fileName.toLowerCase(); const isAudio = Constants.AUDIO_EXTENSIONS.some(ext => lowerFileName.endsWith(ext)); if (isAudio) { const relativePath = relativePrefix ? `${relativePrefix}/${fileName}` : fileName; audioFiles.push({ fullPath, relativePath }); } } } catch (error) { const err = error as Error; Logger.warn(TAG, `无法访问: ${fullPath}, 错误: ${err.message}`); } } } catch (error) { const err = error as Error; Logger.error(TAG, `扫描文件夹失败: ${err.message}`); } return audioFiles; } private getFileNameFromPath(fullPath: string): string { if (!fullPath) { return ''; } const normalized = fullPath.replace(/\\/g, '/'); const lastSlash = normalized.lastIndexOf('/'); return lastSlash >= 0 ? normalized.substring(lastSlash + 1) : normalized; } /** * 移除已选文件 * @param index 文件索引 */ private removeSelectedFile(index: number): void { if (index >= 0 && index < this.selectedFiles.length) { this.selectedFiles.splice(index, 1); this.selectedFileNames.splice(index, 1); this.selectedFileTypes.splice(index, 1); Logger.info(TAG, `移除项目,剩余 ${this.selectedFiles.length} 个`); } } /** * 选择上传账户 * @param account 选中的账户 */ private selectUploadAccount(account: WebDavAccount): void { this.selectedAccount = account; this.applyDefaultUploadPath(account); Logger.info(TAG, `选择账户: ${account.name}, 上传路径: ${this.uploadPath}`); } private applyDefaultUploadPath(account: WebDavAccount | null): void { if (!account) { return; } const managerAccount = this.webdavManager.currentAccount; const managerPath = this.webdavManager.currentPath; if ( managerPath && managerPath.length > 0 && managerAccount && managerAccount.id === account.id ) { this.uploadPath = managerPath; } else { this.uploadPath = account.uploadFilePath || '/'; } } /** * 打开路径浏览器 */ private openPathBrowser(): void { if (!this.selectedAccount) { Logger.warn(TAG, '请先选择账户'); return; } Logger.info(TAG, '========== 打开路径浏览器 =========='); Logger.info(TAG, `当前上传路径: ${this.uploadPath}`); Logger.info(TAG, `选中账户: ${this.selectedAccount.name}`); const managerAccount = this.webdavManager.currentAccount; const defaultPath = managerAccount && managerAccount.id === this.selectedAccount.id && this.webdavManager.currentPath && this.webdavManager.currentPath.length > 0 ? this.webdavManager.currentPath : this.uploadPath; this.currentBrowsePath = defaultPath; this.isShowPathBrowser = true; Logger.info(TAG, `isShowPathBrowser 设置为: ${this.isShowPathBrowser}`); Logger.info(TAG, '开始加载文件夹列表...'); this.loadWebDavFolders(this.currentBrowsePath); } /** * 关闭路径浏览器 */ private closePathBrowser(): void { Logger.info(TAG, '========== 关闭路径浏览器 =========='); this.isShowPathBrowser = false; this.browseFolders = []; } /** * 加载WebDAV文件夹列表 * @param path 路径 */ private async loadWebDavFolders(path: string): Promise { if (!this.selectedAccount) { return; } try { this.isLoadingFolders = true; Logger.info(TAG, `加载文件夹列表: ${path}`); await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, path); // 过滤出文件夹 const allFiles = this.webdavManager.webDavFiles; this.browseFolders = []; const normalizedCurrent = this.normalizeBrowsePath(this.currentBrowsePath); for (let i = 0; i < allFiles.length; i++) { const file = allFiles[i]; if (file.isDirectory) { const folderPath = this.normalizeBrowsePath(file.href); if (folderPath !== normalizedCurrent) { this.browseFolders.push(file); } } } Logger.info(TAG, `加载了 ${this.browseFolders.length} 个文件夹`); } catch (error) { const err = error as Error; Logger.error(TAG, `加载文件夹失败: ${err.message}`); } finally { this.isLoadingFolders = false; } } /** * 进入子文件夹 * @param folder 文件夹信息 */ private async enterBrowseFolder(folder: FileInfo): Promise { this.currentBrowsePath = folder.href; await this.loadWebDavFolders(folder.href); } /** * 返回上级文件夹 */ private async goBackBrowseFolder(): Promise { 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 { // 验证 if (!this.selectedAccount) { Logger.warn(TAG, '请先选择账户'); return; } if (this.selectedFiles.length === 0) { Logger.warn(TAG, '请先选择文件'); return; } if (!this.uploadPath) { Logger.warn(TAG, '请先选择上传路径'); return; } try { Logger.info(TAG, '开始上传任务'); // 显示处理提示 promptAction.showToast({ message: '正在处理文件...', duration: 2000 }); // 构建VideoItem列表 const songs: VideoItem[] = []; let totalFiles = 0; let totalFolders = 0; for (let i = 0; i < this.selectedFiles.length; i++) { const selectedUri = this.selectedFiles[i]; const displayName = this.selectedFileNames[i]; const itemType = this.selectedFileTypes[i]; try { const fileUriObj = new fileUri.FileUri(selectedUri); const resolvedPath = fileUriObj.path || ''; if (!resolvedPath) { Logger.error(TAG, `无法获取本地路径,跳过: ${selectedUri}`); continue; } if (itemType === 'folder') { totalFolders++; const folderName = displayName || this.getFileNameFromPath(resolvedPath); Logger.info(TAG, `扫描文件夹: ${folderName}`); const audioFiles = await this.scanFolderForAudioFiles(resolvedPath, folderName); Logger.info(TAG, `文件夹 ${folderName} 包含 ${audioFiles.length} 个音频文件`); for (let j = 0; j < audioFiles.length; j++) { const audioFile = audioFiles[j]; const audioFileName = this.getFileNameFromPath(audioFile.fullPath); const videoItem = new VideoItem( audioFileName, `file://${audioFile.fullPath}`, audioFile.fullPath, CommonConstants.TYPE_LOCAL, 0, '', undefined, undefined, undefined, undefined, undefined, undefined ); videoItem.remote_rel_path = audioFile.relativePath; songs.push(videoItem); } } else { // 处理单个文件 totalFiles++; const videoItem = new VideoItem( displayName, selectedUri, resolvedPath, CommonConstants.TYPE_LOCAL, 0, '', undefined, undefined, undefined, undefined, undefined, undefined ); videoItem.remote_rel_path = displayName; songs.push(videoItem); } } catch (error) { const err = error as Error; Logger.error(TAG, `处理项目失败: ${err.message}`); continue; } } if (songs.length === 0) { promptAction.showToast({ message: '未找到可上传的音频文件', duration: 2000 }); Logger.warn(TAG, '未找到可上传的有效文件,取消任务'); return; } Logger.info(TAG, `处理完成: ${totalFiles} 个文件, ${totalFolders} 个文件夹, 共 ${songs.length} 个音频文件`); promptAction.showToast({ message: `准备上传 ${songs.length} 个文件`, duration: 2000 }); // 添加到上传队列,传递用户选择的上传路径 Logger.info(TAG, `使用上传路径: ${this.uploadPath}`); const uniqueSongs = this.deduplicateSongs(songs); this.webdavManager.addToUploadQueue(uniqueSongs, this.selectedAccount, this.uploadPath); this.totalUploadCount = uniqueSongs.length; this.uploadedCount = 0; // 开始处理上传队列 await this.webdavManager.startUploadQueue(); Logger.info(TAG, '上传任务已启动'); this.selectedFiles = []; this.selectedFileNames = []; this.selectedFileTypes = []; this.pendingCount = this.webdavManager.uploadQueue.length; this.selectedFiles = []; this.selectedFileNames = []; this.selectedFileTypes = []; } catch (error) { const err = error as Error; Logger.error(TAG, `启动上传失败: ${err.message}`); promptAction.showToast({ message: `上传失败: ${err.message}`, duration: 2000 }); } } private deduplicateSongs(songs: VideoItem[]): VideoItem[] { const unique: VideoItem[] = []; const seen = new Set(); for (let i = 0; i < songs.length; i++) { const song = songs[i]; const key = song.filePath || song.name; if (!key) { continue; } if (seen.has(key)) { Logger.warn(TAG, `跳过重复文件: ${song.name}`); continue; } seen.add(key); unique.push(song); } return unique; } /** * 暂停上传 */ private pauseUpload(): void { this.webdavManager.pauseUploadQueue(); Logger.info(TAG, '暂停上传'); } /** * 恢复上传 */ private async resumeUpload(): Promise { await this.webdavManager.resumeUploadQueue(); Logger.info(TAG, '恢复上传'); } /** * 取消上传 */ private cancelUpload(): void { this.webdavManager.pauseUploadQueue(); this.webdavManager.clearUploadQueue(); this.isUploading = false; this.currentTask = null; Logger.info(TAG, '取消上传'); } /** * 上传进度显示组件 */ @Builder UploadProgressView() { if (this.isUploading && this.currentTask) { Column({ space: 16 }) { // 标题行 Row({ space: 8 }) { Text('⏫') .fontSize(20) Text('上传中') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000') } .alignSelf(ItemAlign.Start) // 当前文件信息卡片 Column({ space: 10 }) { Row({ space: 8 }) { Text('🎵') .fontSize(18) Text(this.currentTask.name) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') .layoutWeight(1) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') // 进度条 Progress({ value: this.uploadProgress, total: 100, type: ProgressType.Linear }) .width('100%') .color(this.themeColor) .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA') .style({ strokeWidth: 6 }) // 进度信息行 Row() { Text(`${this.uploadProgress}%`) .fontSize(15) .fontWeight(FontWeight.Bold) .fontColor(this.themeColor) Blank() Text(`${this.uploadSpeed}`) .fontSize(13) .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93') } .width('100%') // 已上传数量 Row({ space: 6 }) { Text('✓') .fontSize(14) .fontColor(this.themeColor) Text(`已完成 ${this.uploadedCount}/${this.totalUploadCount}`) .fontSize(13) .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93') } } .width('100%') .padding(14) .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA') .borderRadius(10) // 控制按钮 Row({ space: 10 }) { Button({ type: ButtonType.Normal }) { Row({ space: 6 }) { Text(this.webdavManager.isPauseUpload ? '▶️' : '⏸️') .fontSize(16) Text(this.webdavManager.isPauseUpload ? '恢复' : '暂停') .fontSize(14) .fontWeight(FontWeight.Medium) } } .height(44) .layoutWeight(1) .backgroundColor(this.themeColor) .fontColor('#FFFFFF') .borderRadius(10) .onClick(() => { if (this.webdavManager.isPauseUpload) { this.resumeUpload(); } else { this.pauseUpload(); } }) Button({ type: ButtonType.Normal }) { Row({ space: 6 }) { Text('❌') .fontSize(16) Text('取消') .fontSize(14) .fontWeight(FontWeight.Medium) } } .height(44) .layoutWeight(1) .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30') .fontColor('#FFFFFF') .borderRadius(10) .onClick(() => { this.cancelUpload(); }) } .width('100%') } .width('100%') .padding(16) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') .borderRadius(12) .shadow({ radius: this.isDarkMode ? 8 : 12, color: this.isDarkMode ? '#0C000000' : '#19000000', offsetX: 0, offsetY: 2 }) .margin({ bottom: 16 }) } } /** * 上传队列管理组件(使用LazyDataSource优化性能) */ @Builder UploadQueueView() { Column({ space: 16 }) { // 标题行 Row({ space: 8 }) { Text('📊') .fontSize(20) Text('任务队列') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000') } .alignSelf(ItemAlign.Start) Tabs({ barPosition: BarPosition.Start }) { TabContent() { this.QueueList(this.uploadQueueDataSource, 'pending') } .tabBar(this.TabBarBuilder('⏳', '待上传', this.pendingCount)) TabContent() { this.QueueList(this.finishQueueDataSource, 'finished') } .tabBar(this.TabBarBuilder('✅', '已完成', this.finishedCount)) } .width('100%') .height(300) .barMode(BarMode.Fixed) .barBackgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') .animationDuration(300) } .width('100%') .padding(16) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') .borderRadius(12) .shadow({ radius: this.isDarkMode ? 8 : 12, color: this.isDarkMode ? '#0C000000' : '#19000000', offsetX: 0, offsetY: 2 }) .margin({ bottom: 16 }) } /** * 自定义Tab标签构建器 */ @Builder TabBarBuilder(icon: string, title: string, count: number) { Row({ space: 6 }) { Text(icon) .fontSize(16) Text(title) .fontSize(14) .fontWeight(FontWeight.Medium) Text(`${count}`) .fontSize(12) .fontWeight(FontWeight.Medium) .fontColor('#FFFFFF') .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .backgroundColor(this.themeColor) .borderRadius(8) } .padding({ left: 4, right: 4 }) } /** * 队列列表组件(使用LazyForEach优化性能) */ @Builder QueueList(dataSource: UploadTaskDataSource, type: string) { if (dataSource.totalCount() === 0) { Column() { Text(type === 'pending' ? '暂无待上传任务' : '暂无已完成任务') .fontSize(14) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#66000000') } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } else { List({ space: 8 }) { LazyForEach(dataSource, (item: VideoItem, index: number) => { ListItem() { Row({ space: 12 }) { // 文件图标 Text('🎵') .fontSize(20) Text(item.name) .fontSize(14) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000') .layoutWeight(1) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) if (type === 'pending') { Button('移除') .fontSize(12) .height(32) .padding({ left: 12, right: 12 }) .backgroundColor(this.isDarkMode ? '#D94838' : '#E84026') .fontColor('#FFFFFF') .borderRadius(6) .onClick(() => { // 从队列移除 const tasks = this.webdavManager.uploadQueue; if (index < tasks.length) { this.webdavManager.removeFromUploadQueue(tasks[index]); } }) } else { Text('✓') .fontSize(20) .fontColor(this.isDarkMode ? '#5BA854' : '#64BB5C') } } .width('100%') .padding(12) .backgroundColor(this.isDarkMode ? '#2E3033' : '#F1F3F5') .borderRadius(8) } }, (item: VideoItem, index: number) => `${type}-${index}-${item.name}`) } .width('100%') .height('100%') .cachedCount(5) // 缓存5个列表项以提升滚动性能 .divider({ strokeWidth: 1, color: this.isDarkMode ? '#19FFFFFF' : '#0C000000' }) } } /** * 开始上传按钮组件 */ @Builder StartUploadButton() { Button({ type: ButtonType.Normal }) { Row({ space: 10 }) { Text('🚀') .fontSize(20) Text('开始上传') .fontSize(16) .fontWeight(FontWeight.Bold) } } .width('100%') .height(54) .backgroundColor(this.themeColor) .fontColor('#FFFFFF') .borderRadius(12) .enabled(!this.isUploading && this.selectedFiles.length > 0 && this.selectedAccount !== null) .opacity((!this.isUploading && this.selectedFiles.length > 0 && this.selectedAccount !== null) ? 1.0 : 0.4) .shadow({ radius: 12, color: this.themeColor + '50', offsetX: 0, offsetY: 4 }) .onClick(() => { this.startUpload(); }) .margin({ bottom: 16 }) } /** * 远程配置区域组件 */ @Builder UploadConfigSection() { Column({ space: 16 }) { // 标题行 Row({ space: 8 }) { Text('📁') .fontSize(20) Text('远程配置') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000') } .alignSelf(ItemAlign.Start) // 上传路径选择卡片 Column({ space: 12 }) { Row({ space: 8 }) { Text('远程目录') .fontSize(13) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#99FFFFFF' : '#66000000') Row({ space: 8 }) { Text('📂') .fontSize(18) Text(this.uploadPath || '请选择远程目录') .fontSize(15) .fontColor(this.uploadPath ? (this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') : (this.isDarkMode ? '#66FFFFFF' : '#99000000')) .fontWeight(this.uploadPath ? FontWeight.Medium : FontWeight.Regular) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } Blank() Button('浏览') .fontSize(13) .height(32) .padding({ left: 12, right: 12 }) .backgroundColor(this.themeColor) .fontColor('#FFFFFF') .borderRadius(6) .enabled(this.selectedAccount !== null) .opacity(this.selectedAccount !== null ? 1.0 : 0.4) .bindSheet($$this.isShowPathBrowser, this.PathBrowserDialog(), { height: '90%', dragBar: true, showClose: true, preferType: SheetType.BOTTOM, blurStyle: BlurStyle.Thin, title: { title: '选择上传路径' } }) .onClick(() => { this.openPathBrowser(); }) } .width('100%') } .width('100%') .padding(14) .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA') .borderRadius(10) .border({ width: 1, color: this.uploadPath ? (this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30') : (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA') }) // 重复文件处理卡片 Column({ space: 12 }) { Text('重复文件处理') .fontSize(13) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#99FFFFFF' : '#66000000') .alignSelf(ItemAlign.Start) // SegmentButton 选项 SegmentButton({ options: this.duplicateOptions, selectedIndexes: $selectedDuplicateIndex }) .width('100%') .margin({ top: 8 }) } .width('100%') .padding(14) .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA') .borderRadius(10) } .width('100%') .padding(16) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') .borderRadius(12) .shadow({ radius: this.isDarkMode ? 8 : 12, color: this.isDarkMode ? '#0C000000' : '#19000000', offsetX: 0, offsetY: 2 }) .margin({ bottom: 16 }) } /** * 路径浏览器对话框 */ @Builder PathBrowserDialog() { Column() { // 当前路径显示 Row({ space: 12 }) { 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%') .layoutWeight(1) .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({ left:16,right:16 }) .margin({bottom:25}) } } /** * 账户选择区域组件 */ @Builder AccountSelectorSection() { Column({ space: 16 }) { // 标题行 Row({ space: 8 }) { Text('☁️') .fontSize(20) Text('选择账户') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000') } .alignSelf(ItemAlign.Start) if (this.accounts.length === 0) { Column({ space: 12 }) { Text('📦') .fontSize(48) Text('暂无账户') .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: 3 }) { Text(account.name) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') Text(`${account.host}:${account.port}`) .fontSize(11) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93') } .alignItems(HorizontalAlign.Start) .layoutWeight(1) if (this.selectedAccount && this.selectedAccount.id === account.id) { Text('✓') .fontSize(20) .fontColor(this.themeColor) } } .width('100%') .padding(8) .backgroundColor(this.selectedAccount && this.selectedAccount.id === account.id ? (this.isDarkMode ? '#2E3238' : '#FFFFFF') : (this.isDarkMode ? '#2E3238' : '#F5F7FA')) .borderRadius(10) .border({ width: this.selectedAccount && this.selectedAccount.id === account.id ? 2 : 1, color: this.selectedAccount && this.selectedAccount.id === account.id ? this.themeColor : (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA') }) .onClick(() => { this.selectUploadAccount(account); }) } }, (account: WebDavAccount) => `account-${account.id}`) } .width('100%') } } .width('100%') .padding(10) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') .borderRadius(12) .shadow({ radius: this.isDarkMode ? 8 : 12, color: this.isDarkMode ? '#0C000000' : '#19000000', offsetX: 0, offsetY: 2 }) .margin({ bottom: 16 }) } /** * 文件选择区域组件 */ @Builder FilePickerSection() { Column({ space: 16 }) { // 标题行 Row({ space: 8 }) { Text('🎵') .fontSize(20) Text('选择文件') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000') } .alignSelf(ItemAlign.Start) // 按钮组 Row({ space: 10 }) { Button({ type: ButtonType.Normal }) { Row({ space: 6 }) { Text('📁') .fontSize(16) Text('从文件管理器') .fontSize(14) .fontWeight(FontWeight.Medium) } } .layoutWeight(1) .height(48) .backgroundColor(this.themeColor) .fontColor('#FFFFFF') .borderRadius(10) .shadow({ radius: 8, color: this.themeColor + '40', offsetX: 0, offsetY: 3 }) .onClick(() => { this.openFilePicker(); }) Button({ type: ButtonType.Normal }) { Row({ space: 6 }) { Text('🎵') .fontSize(16) Text('从本地目录') .fontSize(14) .fontWeight(FontWeight.Medium) } } .layoutWeight(1) .height(48) .backgroundColor(this.themeColor) .fontColor('#FFFFFF') .borderRadius(10) .shadow({ radius: 8, color: this.themeColor + '40', offsetX: 0, offsetY: 3 }) .onClick(() => { this.openFileBrowser(); }) } .width('100%') if (this.selectedFiles.length > 0) { Row({ space: 10 }) { Text('✓') .fontSize(18) .fontColor(this.themeColor) Column({ space: 2 }) { Text(`已选择 ${this.selectedFiles.length} 项`) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') Row({ space: 8 }) { if (this.selectedFileTypes.filter(t => t === 'file').length > 0) { Text(`${this.selectedFileTypes.filter(t => t === 'file').length} 个文件`) .fontSize(12) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93') } if (this.selectedFileTypes.filter(t => t === 'folder').length > 0) { Text(`${this.selectedFileTypes.filter(t => t === 'folder').length} 个文件夹`) .fontSize(12) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93') } } } .alignItems(HorizontalAlign.Start) } .width('100%') .padding(12) .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA') .borderRadius(8) .border({ width: 1, color: this.isDarkMode ? this.themeColor + '40' : this.themeColor + '30' }) } } .width('100%') .padding(16) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') .borderRadius(12) .shadow({ radius: this.isDarkMode ? 8 : 12, color: this.isDarkMode ? '#0C000000' : '#19000000', offsetX: 0, offsetY: 2 }) .margin({ bottom: 16 }) } /** * 已选文件列表组件 */ @Builder SelectedFilesList() { Column({ space: 16 }) { // 标题行 Row({ space: 8 }) { Text('📋') .fontSize(20) Text('已选文件') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#E5000000') Blank() Text(`${this.selectedFiles.length}`) .fontSize(13) .fontWeight(FontWeight.Medium) .fontColor('#FFFFFF') .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .backgroundColor(this.themeColor) .borderRadius(10) } .width('100%') .alignSelf(ItemAlign.Start) List({ space: 8 }) { ForEach(this.selectedFileNames, (fileName: string, index: number) => { ListItem() { Row({ space: 12 }) { // 文件/文件夹图标 Text(this.selectedFileTypes[index] === 'folder' ? '📁' : '🎵') .fontSize(20) Column({ space: 2 }) { Text(fileName) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) if (this.selectedFileTypes[index] === 'folder') { Text('文件夹') .fontSize(12) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93') } } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Button({ type: ButtonType.Normal }) { Text('移除') .fontSize(12) .fontWeight(FontWeight.Medium) } .height(32) .padding({ left: 12, right: 12 }) .backgroundColor(this.isDarkMode ? '#D94838' : '#FF3B30') .fontColor('#FFFFFF') .borderRadius(6) .onClick(() => { this.removeSelectedFile(index); }) } .width('100%') .padding(12) .backgroundColor(this.isDarkMode ? '#2E3238' : '#F5F7FA') .borderRadius(8) .border({ width: 1, color: this.isDarkMode ? '#19FFFFFF' : '#E5E5EA' }) } }, (fileName: string, index: number) => `${index}-${fileName}`) } .width('100%') .constraintSize({ maxHeight: 300 }) } .width('100%') .padding(16) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') .borderRadius(12) .shadow({ radius: this.isDarkMode ? 8 : 12, color: this.isDarkMode ? '#0C000000' : '#19000000', offsetX: 0, offsetY: 2 }) .margin({ bottom: 16 }) } build() { Column() { // 标题栏 this.topTitleBar() // 页面内容区域 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') .bindSheet($$this.isShowFileBrowser, this.FileBrowserDialog(), { height: '95%', dragBar: true, showClose: true, preferType: SheetType.BOTTOM, blurStyle: BlurStyle.Thin, title: { title: '浏览选择文件' } }) } @Builder topTitleBar() { Column() { Row({ space: 15 }) { //左侧滑动按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { SymbolGlyph($r('sys.symbol.chevron_left')) .attributeModifier(new SymbolGlyphFancyModifier(25, '', '')) } .attributeModifier(new ButtonFancyModifier(40, 40)) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) .animation({ duration: 300, curve: Curve.Ease }) .onClick(() => { this.getUIContext().getRouter().back(); }) .attributeModifier(new ShadowModifier()) .zIndex(0) Text('上传音乐') .margin({ left: 3, right: 10 }) .fontColor($r('app.color.text_color')) .fontSize(19) .maxLines(1) .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动 .layoutWeight(1) .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 }) } } .padding({ top: this.topSafeHeight + 10, left: 10, right: 10, bottom: 12 }) .width('100%') } /** * 文件浏览器对话框 */ @Builder FileBrowserDialog() { Column() { // 当前路径和返回按钮 Row({ space: 12 }) { Column({ space: 4 }) { Text('当前目录') .fontSize(12) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93') Text(this.fileBrowserPath) .fontSize(13) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) if (this.fileBrowserPath.lastIndexOf('/') > 0) { Button('返回上级') .fontSize(12) .height(36) .padding({ left: 12, right: 12 }) .backgroundColor(this.themeColor) .fontColor('#FFFFFF') .borderRadius(8) .onClick(() => { this.goBackDirectory(); }) } } .width('100%') .padding(16) .backgroundColor(this.isDarkMode ? '#191A1C' : '#F1F3F5') // 文件列表 if (this.isLoadingFiles) { Column({ space: 12 }) { LoadingProgress() .width(48) .height(48) .color(this.themeColor) Text('加载中...') .fontSize(14) .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93') } .width('100%') .height(400) .justifyContent(FlexAlign.Center) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') } else if (this.browserFiles.length === 0) { Column({ space: 12 }) { Text('📂') .fontSize(48) Text('当前目录为空') .fontSize(14) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93') } .width('100%') .height(400) .justifyContent(FlexAlign.Center) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') } else { List({ space: 8 }) { ForEach(this.browserFiles, (item: LocalFileItem, index: number) => { ListItem() { Row({ space: 12 }) { // 复选框 Checkbox() .select(this.selectedBrowserItems.has(item.path)) .selectedColor(this.themeColor) .onChange((checked: boolean) => { this.toggleItemSelection(item); }) // 图标 Text(item.isDirectory ? '📁' : '🎵') .fontSize(24) // 文件名 Column({ space: 2 }) { Text(item.name) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) if (item.isDirectory) { Text('文件夹') .fontSize(12) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93') } } .alignItems(HorizontalAlign.Start) .layoutWeight(1) // 进入按钮(仅文件夹) if (item.isDirectory) { Text('→') .fontSize(20) .fontColor(this.isDarkMode ? '#66FFFFFF' : '#8E8E93') .onClick(() => { this.enterDirectory(item); }) } } .width('100%') .padding(12) .backgroundColor(this.selectedBrowserItems.has(item.path) ? (this.isDarkMode ? '#2E3238' : '#FFFFFF') : (this.isDarkMode ? '#2E3238' : '#F5F7FA')) .borderRadius(8) .border({ width: this.selectedBrowserItems.has(item.path) ? 2 : 1, color: this.selectedBrowserItems.has(item.path) ? this.themeColor : (this.isDarkMode ? '#19FFFFFF' : '#E5E5EA') }) .onClick(() => { if (!item.isDirectory) { this.toggleItemSelection(item); } }) } }, (item: LocalFileItem, index: number) => `${index}-${item.path}`) } .width('100%') .layoutWeight(1) .padding(16) .backgroundColor(this.isDarkMode ? '#23272E' : '#FFFFFF') } // 底部按钮 Row({ space: 12 }) { Text(`已选择 ${this.selectedBrowserItems.size} 项`) .fontSize(14) .fontColor(this.isDarkMode ? '#99FFFFFF' : '#8E8E93') .layoutWeight(1) Button('取消') .fontSize(14) .height(48) .padding({ left: 30, right: 30 }) .backgroundColor(this.isDarkMode ? '#2E3238' : '#E5E5EA') .fontColor(this.isDarkMode ? '#E5FFFFFF' : '#1F1F1F') .borderRadius(8) .onClick(() => { this.closeFileBrowser(); }) Button('确定') .fontSize(14) .height(48) .padding({ left: 30, right: 30 }) .backgroundColor(this.themeColor) .fontColor('#FFFFFF') .borderRadius(8) .enabled(this.selectedBrowserItems.size > 0) .opacity(this.selectedBrowserItems.size > 0 ? 1.0 : 0.4) .onClick(() => { this.confirmFileSelection(); }) } .width('100%') .padding({ left:16,right:16 }) .margin({bottom:12}) } } }