feat: 前端组件拆分与文件库目录浏览功能

- 将 App.vue 拆分为 AdminPage、ClientPage、QrCodeModal 三个独立组件
- 新增 BrowseDirectory 接口,基于 RelativePath 实现层级目录浏览
- 前端新增面包屑导航、文件夹网格、文件列表等目录浏览 UI
- 新增对应 CSS 样式(breadcrumb、folder-grid、file-list 等)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 11:59:45 +08:00
co-authored by Claude Opus 4.7
parent d84bbb3a18
commit 8270cf198b
12 changed files with 729 additions and 452 deletions
@@ -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<string> Subdirectories,
List<FileRecordDto> Files);
public sealed record TextPreviewDto(
int Id,
string FileName,
@@ -64,6 +64,15 @@ namespace Avalonia_Services.Services.FileLibrary
: ResponseHelper.Ok(preview);
}
public async Task<IApiResponse> 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)
@@ -261,6 +261,54 @@ namespace Avalonia_Services.Services.FileLibrary
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<BrowseDirectoryResponse> 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<string>(StringComparer.OrdinalIgnoreCase);
var currentFiles = new List<FileRecordDto>();
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<TextPreviewDto?> GetTextPreviewAsync(int id, CancellationToken cancellationToken = default)
{
var file = await db.ManagedFileRecords
@@ -24,5 +24,7 @@ namespace Avalonia_Services.Services.FileLibrary
Task<IApiResponse> GetFileAsync(FileQueryRequest request);
Task<IApiResponse> GetTextPreviewAsync(FileQueryRequest request);
Task<IApiResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request);
}
}
@@ -25,5 +25,7 @@ namespace Avalonia_Services.Services.FileLibrary
Task<FileRecordDto?> GetFileAsync(int id, CancellationToken cancellationToken = default);
Task<TextPreviewDto?> GetTextPreviewAsync(int id, CancellationToken cancellationToken = default);
Task<BrowseDirectoryResponse> BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default);
}
}