diff --git a/Avalonia-Services/Endpoints/AppEndpoints.cs b/Avalonia-Services/Endpoints/AppEndpoints.cs index 1f4d812..8bc5c2d 100644 --- a/Avalonia-Services/Endpoints/AppEndpoints.cs +++ b/Avalonia-Services/Endpoints/AppEndpoints.cs @@ -65,6 +65,10 @@ namespace Avalonia_Services.Endpoints .WithOpenApi("FileLibrary", "分页查询已扫描文件。") .WithName("SearchFiles"); + endpoints.MapGet("api/files/browse", (service, request, _) => service.BrowseDirectoryAsync(request)) + .WithOpenApi("FileLibrary", "浏览文件库目录结构。") + .WithName("BrowseDirectory"); + endpoints.MapGet("api/files/detail", (service, request, _) => service.GetFileAsync(request)) .WithOpenApi("FileLibrary", "查询文件详情。") .WithName("GetFileDetail"); diff --git a/Avalonia-Services/Services/FileLibrary/FileLibraryContracts.cs b/Avalonia-Services/Services/FileLibrary/FileLibraryContracts.cs index 041ede8..a4eb42a 100644 --- a/Avalonia-Services/Services/FileLibrary/FileLibraryContracts.cs +++ b/Avalonia-Services/Services/FileLibrary/FileLibraryContracts.cs @@ -69,6 +69,15 @@ namespace Avalonia_Services.Services.FileLibrary string? TextUrl, bool BrowserPlayable); + public sealed record BrowseDirectoryRequest( + [property: JsonPropertyName("rootId")] int RootId = 0, + [property: JsonPropertyName("path")] string? Path = null); + + public sealed record BrowseDirectoryResponse( + string CurrentPath, + List Subdirectories, + List Files); + public sealed record TextPreviewDto( int Id, string FileName, diff --git a/Avalonia-Services/Services/FileLibrary/FileLibraryEndpointService.cs b/Avalonia-Services/Services/FileLibrary/FileLibraryEndpointService.cs index d5d9bf2..8a5f342 100644 --- a/Avalonia-Services/Services/FileLibrary/FileLibraryEndpointService.cs +++ b/Avalonia-Services/Services/FileLibrary/FileLibraryEndpointService.cs @@ -64,6 +64,15 @@ namespace Avalonia_Services.Services.FileLibrary : ResponseHelper.Ok(preview); } + public async Task BrowseDirectoryAsync(BrowseDirectoryRequest request) + { + if (request.RootId <= 0) + return ResponseHelper.Failure(400, "rootId 参数无效。"); + + var result = await fileLibrary.BrowseDirectoryAsync(request); + return ResponseHelper.Ok(result); + } + private static void ValidateFileId(int id) { if (id > 0) diff --git a/Avalonia-Services/Services/FileLibrary/FileLibraryService.cs b/Avalonia-Services/Services/FileLibrary/FileLibraryService.cs index c89d7f7..182194b 100644 --- a/Avalonia-Services/Services/FileLibrary/FileLibraryService.cs +++ b/Avalonia-Services/Services/FileLibrary/FileLibraryService.cs @@ -261,6 +261,54 @@ namespace Avalonia_Services.Services.FileLibrary .FirstOrDefaultAsync(cancellationToken); } + public async Task BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default) + { + var rootId = request.RootId; + // URL 友好的正斜杠,用于响应和内存处理 + var prefix = (request.Path ?? "").Trim().Replace('\\', '/').Trim('/'); + // Windows 反斜杠,用于数据库查询 + var dbPrefix = prefix.Replace('/', '\\'); + + var query = db.ManagedFileRecords + .AsNoTracking() + .Where(f => f.LibraryRootId == rootId && f.Exists + && f.LibraryRoot != null && f.LibraryRoot.IsAvailable); + + if (!string.IsNullOrEmpty(dbPrefix)) + { + var dbPrefixWithSlash = dbPrefix + "\\"; + query = query.Where(f => f.RelativePath.StartsWith(dbPrefixWithSlash)); + } + + var allFiles = await query.ToListAsync(cancellationToken); + + var subdirs = new HashSet(StringComparer.OrdinalIgnoreCase); + var currentFiles = new List(); + + foreach (var file in allFiles) + { + var relativePath = file.RelativePath.Replace('\\', '/'); + var remaining = string.IsNullOrEmpty(prefix) + ? relativePath + : relativePath[(prefix.Length + 1)..]; + + var slashIndex = remaining.IndexOf('/'); + if (slashIndex < 0) + { + currentFiles.Add(ToFileDto(file)); + } + else + { + subdirs.Add(remaining[..slashIndex]); + } + } + + return new BrowseDirectoryResponse( + prefix, + subdirs.OrderBy(d => d, StringComparer.OrdinalIgnoreCase).ToList(), + currentFiles); + } + public async Task GetTextPreviewAsync(int id, CancellationToken cancellationToken = default) { var file = await db.ManagedFileRecords diff --git a/Avalonia-Services/Services/FileLibrary/IFileLibraryEndpointService.cs b/Avalonia-Services/Services/FileLibrary/IFileLibraryEndpointService.cs index ee9a4de..e58aa36 100644 --- a/Avalonia-Services/Services/FileLibrary/IFileLibraryEndpointService.cs +++ b/Avalonia-Services/Services/FileLibrary/IFileLibraryEndpointService.cs @@ -24,5 +24,7 @@ namespace Avalonia_Services.Services.FileLibrary Task GetFileAsync(FileQueryRequest request); Task GetTextPreviewAsync(FileQueryRequest request); + + Task BrowseDirectoryAsync(BrowseDirectoryRequest request); } } diff --git a/Avalonia-Services/Services/FileLibrary/IFileLibraryService.cs b/Avalonia-Services/Services/FileLibrary/IFileLibraryService.cs index e3c5147..e9a736e 100644 --- a/Avalonia-Services/Services/FileLibrary/IFileLibraryService.cs +++ b/Avalonia-Services/Services/FileLibrary/IFileLibraryService.cs @@ -25,5 +25,7 @@ namespace Avalonia_Services.Services.FileLibrary Task GetFileAsync(int id, CancellationToken cancellationToken = default); Task GetTextPreviewAsync(int id, CancellationToken cancellationToken = default); + + Task BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default); } } diff --git a/Avalonia-Web-VUE/src/App.vue b/Avalonia-Web-VUE/src/App.vue index 71f6b54..bc30632 100644 --- a/Avalonia-Web-VUE/src/App.vue +++ b/Avalonia-Web-VUE/src/App.vue @@ -1,459 +1,12 @@ diff --git a/Avalonia-Web-VUE/src/api/index.ts b/Avalonia-Web-VUE/src/api/index.ts index bf14dd1..3b11001 100644 --- a/Avalonia-Web-VUE/src/api/index.ts +++ b/Avalonia-Web-VUE/src/api/index.ts @@ -45,6 +45,12 @@ export interface FileRecordDto { browserPlayable: boolean } +export interface BrowseDirectoryResponse { + currentPath: string + subdirectories: string[] + files: FileRecordDto[] +} + export interface TextPreviewDto { id: number fileName: string @@ -87,6 +93,8 @@ export const api = { request('library/roots/scan', { method: 'POST', body: { id } }), searchFiles: (params: { page: number; pageSize: number; mediaType?: MediaType; keyword?: string; rootId?: number }) => request>(`files${qs(params)}`), + browseDirectory: (rootId: number, path: string) => + request(`files/browse${qs({ rootId, path })}`), getTextPreview: (id: number) => request(`files/text${qs({ id })}`), mediaUrl: (path: string) => apiUrl(path), diff --git a/Avalonia-Web-VUE/src/assets/main.css b/Avalonia-Web-VUE/src/assets/main.css index bf2a03c..7cd55a5 100644 --- a/Avalonia-Web-VUE/src/assets/main.css +++ b/Avalonia-Web-VUE/src/assets/main.css @@ -648,6 +648,82 @@ a { min-width: 120px; } +.breadcrumb-nav { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 12px; + padding: 0; +} + +.breadcrumb-item { + min-height: 30px; + border: none; + padding: 4px 8px; + color: var(--muted); + background: transparent; + font-size: 14px; + font-weight: 600; +} + +.breadcrumb-item:hover:not(:disabled) { + color: var(--accent-strong); + border-color: transparent; +} + +.breadcrumb-item.active { + color: var(--text); + font-weight: 800; +} + +.breadcrumb-sep { + color: var(--muted); + font-size: 14px; +} + +.browse-content { + display: grid; + gap: 14px; +} + +.browse-section h3 { + margin: 0 0 8px; + font-size: 15px; + font-weight: 800; + color: var(--muted); +} + +.folder-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 8px; +} + +.folder-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-height: 46px; + padding: 8px 12px; + text-align: left; + font-weight: 600; +} + +.folder-icon { + font-size: 20px; +} + +.file-list { + display: grid; + gap: 8px; + border: 1px solid var(--line); + border-radius: 14px; + padding: 8px; + background: var(--panel); +} + @media (max-width: 1100px) { .admin-layout, .admin-browser { diff --git a/Avalonia-Web-VUE/src/components/AdminPage.vue b/Avalonia-Web-VUE/src/components/AdminPage.vue new file mode 100644 index 0000000..b77b9cf --- /dev/null +++ b/Avalonia-Web-VUE/src/components/AdminPage.vue @@ -0,0 +1,239 @@ + + + diff --git a/Avalonia-Web-VUE/src/components/ClientPage.vue b/Avalonia-Web-VUE/src/components/ClientPage.vue new file mode 100644 index 0000000..f688924 --- /dev/null +++ b/Avalonia-Web-VUE/src/components/ClientPage.vue @@ -0,0 +1,288 @@ + + + diff --git a/Avalonia-Web-VUE/src/components/QrCodeModal.vue b/Avalonia-Web-VUE/src/components/QrCodeModal.vue new file mode 100644 index 0000000..079a93d --- /dev/null +++ b/Avalonia-Web-VUE/src/components/QrCodeModal.vue @@ -0,0 +1,39 @@ + + +