后端新增 POST api/files/delete 接口,同时删除物理文件、缩略图和数据库记录。 前端在 FileCard 和 FileListItem 中增加 hover 可见的删除按钮,点击后弹出确认提示。
12 KiB
File Delete Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a permanent file delete API endpoint and frontend delete button with confirmation dialog.
Architecture: Backend adds POST api/files/delete that deletes physical file + thumbnail + DB record. Frontend adds delete button to FileCard and FileListItem, with window.confirm() guard.
Tech Stack: ASP.NET Core 10, EF Core 10, Vue 3.5, TypeScript 6
Global Constraints
- Do not edit
FileShare-EFCore/Migrations/**— schema changes require user to run migration scripts - All endpoints use POST (project convention)
- Frontend response interceptor auto-unwraps
{ success, data }envelope - Follow existing code patterns (sealed record DTOs,
ResponseHelper,defineProps/defineEmits)
File Map
| File | Action | Purpose |
|---|---|---|
FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs |
Modify | Add DeleteFileRequest DTO |
FileShare-Services/Services/FileLibrary/IFileLibraryService.cs |
Modify | Add DeleteFileAsync method |
FileShare-Services/Services/FileLibrary/FileLibraryService.cs |
Modify | Implement delete logic |
FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs |
Modify | Add DeleteFileAsync endpoint method |
FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs |
Modify | Implement endpoint adapter |
FileShare-Services/Endpoints/AppEndpoints.cs |
Modify | Register route |
FileShare-Web-VUE/src/api/index.ts |
Modify | Add deleteFile API method |
FileShare-Web-VUE/src/components/client/FileCard.vue |
Modify | Add delete button |
FileShare-Web-VUE/src/components/client/FileListItem.vue |
Modify | Add delete button |
FileShare-Web-VUE/src/components/ClientPage.vue |
Modify | Handle delete event |
Task 1: Backend — Contract, Service Interface, Service Implementation
Covers: S2 (backend delete logic)
Files:
-
Modify:
FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs:35 -
Modify:
FileShare-Services/Services/FileLibrary/IFileLibraryService.cs:123 -
Modify:
FileShare-Services/Services/FileLibrary/FileLibraryService.cs:546 -
Step 1: Add DeleteFileRequest to FileLibraryContracts.cs
After line 35 (the DeleteLibraryRootRequest record), add:
/// <summary>
/// 永久删除文件的请求。
/// </summary>
public sealed record DeleteFileRequest(
[property: JsonPropertyName("id")] int Id);
- Step 2: Add DeleteFileAsync to IFileLibraryService.cs
After line 123 (SaveFileProgressAsync), add:
/// <summary>
/// 永久删除指定文件,同时删除物理文件和缩略图。
/// </summary>
/// <param name="id">文件记录 ID。</param>
/// <param name="cancellationToken">取消令牌。</param>
Task DeleteFileAsync(int id, CancellationToken cancellationToken = default);
- Step 3: Implement DeleteFileAsync in FileLibraryService.cs
After line 546 (end of SaveFileProgressAsync), add:
/// <inheritdoc />
public async Task DeleteFileAsync(int id, CancellationToken cancellationToken = default)
{
var record = await db.ManagedFileRecords
.Include(f => f.Thumbnail)
.FirstOrDefaultAsync(f => f.Id == id, cancellationToken);
if (record is null)
{
return;
}
// 删除物理文件
try
{
if (File.Exists(record.AbsolutePath))
{
File.Delete(record.AbsolutePath);
}
}
catch (Exception ex)
{
Serilog.Log.Warning(ex, "删除物理文件失败 FilePath={FilePath}", record.AbsolutePath);
}
// 删除缩略图文件
if (record.Thumbnail is not null)
{
try
{
var thumbnailPath = thumbnailService.GetAbsolutePath(record.Thumbnail.RelativePath);
if (File.Exists(thumbnailPath))
{
File.Delete(thumbnailPath);
}
}
catch (Exception ex)
{
Serilog.Log.Warning(ex, "删除缩略图文件失败 ThumbnailId={ThumbnailId}", record.ThumbnailId);
}
db.ManagedThumbnailMaps.Remove(record.Thumbnail);
}
db.ManagedFileRecords.Remove(record);
await db.SaveChangesAsync(cancellationToken);
}
Task 2: Backend — Endpoint Service + Route Registration
Covers: S2 (endpoint and routing)
Files:
-
Modify:
FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs:107 -
Modify:
FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs:114 -
Modify:
FileShare-Services/Endpoints/AppEndpoints.cs:92 -
Step 1: Add DeleteFileAsync to IFileLibraryEndpointService.cs
After line 107 (SaveFileProgressAsync), add:
/// <summary>
/// 永久删除指定文件。
/// </summary>
/// <param name="request">包含文件 ID 的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> DeleteFileAsync(DeleteFileRequest request);
- Step 2: Implement DeleteFileAsync in FileLibraryEndpointService.cs
After line 114 (end of SaveFileProgressAsync), add:
/// <inheritdoc />
public async Task<IApiResponse> DeleteFileAsync(DeleteFileRequest request)
{
ValidateFileId(request.Id);
await fileLibrary.DeleteFileAsync(request.Id);
return ResponseHelper.Succeed("文件已永久删除。");
}
- Step 3: Register route in AppEndpoints.cs
After line 92 (the SaveFileProgress endpoint), add:
endpoints.MapPost<IFileLibraryEndpointService, DeleteFileRequest>("api/files/delete", (service, request, _) => service.DeleteFileAsync(request))
.WithOpenApi("FileLibrary", "永久删除文件(物理文件+数据库记录)。")
.WithName("DeleteFile");
Task 3: Frontend — API Method + Components
Covers: S3 (frontend delete button and API)
Files:
-
Modify:
FileShare-Web-VUE/src/api/index.ts:117 -
Modify:
FileShare-Web-VUE/src/components/client/FileCard.vue -
Modify:
FileShare-Web-VUE/src/components/client/FileListItem.vue -
Modify:
FileShare-Web-VUE/src/components/ClientPage.vue -
Step 1: Add deleteFile API method in index.ts
After line 117 (saveFileProgress), add:
deleteFile: (id: number) =>
request('files/delete', { method: 'POST', body: { id } }),
- Step 2: Add delete button to FileCard.vue
Replace the entire FileCard.vue content with:
<script setup lang="ts">
import { api, type FileRecordDto } from '../../api'
import { formatCreatedTime, formatDuration, formatSize } from '../../utils/formatters'
defineProps<{
file: FileRecordDto
selected?: boolean
showCreatedTime?: boolean
}>()
defineEmits<{
select: [file: FileRecordDto]
delete: [file: FileRecordDto]
}>()
</script>
<template>
<button
class="file-card"
:class="{ active: selected }"
type="button"
@click="$emit('select', file)"
>
<span class="delete-btn" @click.stop="$emit('delete', file)" title="永久删除">×</span>
<img
v-if="file.thumbnailUrl"
:src="api.thumbnailUrl(file.thumbnailUrl)"
class="card-thumb"
alt=""
loading="lazy"
/>
<div v-else class="card-thumb-placeholder">{{ file.mediaType }}</div>
<div class="card-body">
<strong>{{ file.fileName }}</strong>
<small>
{{ formatSize(file.sizeBytes) }}
<template v-if="file.videoDuration"> · {{ formatDuration(file.videoDuration) }}</template>
</small>
<small v-if="showCreatedTime && formatCreatedTime(file)">{{ formatCreatedTime(file) }}</small>
</div>
</button>
</template>
<style scoped>
.delete-btn {
position: absolute;
top: 4px;
right: 4px;
width: 22px;
height: 22px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 14px;
line-height: 22px;
text-align: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
}
.file-card:hover .delete-btn {
opacity: 1;
}
.delete-btn:hover {
background: #e53e3e;
}
</style>
- Step 3: Add delete button to FileListItem.vue
Replace the entire FileListItem.vue content with:
<script setup lang="ts">
import { api, type FileRecordDto } from '../../api'
import { formatCreatedTime, formatDate, formatDuration, formatSize } from '../../utils/formatters'
defineProps<{
file: FileRecordDto
selected?: boolean
showCreatedTime?: boolean
showLastPlayed?: boolean
}>()
defineEmits<{
select: [file: FileRecordDto]
delete: [file: FileRecordDto]
}>()
</script>
<template>
<button
class="mobile-file"
:class="{ active: selected }"
type="button"
@click="$emit('select', file)"
>
<img
v-if="file.thumbnailUrl"
:src="api.thumbnailUrl(file.thumbnailUrl)"
class="file-thumb"
alt=""
loading="lazy"
/>
<span v-else class="type-badge">{{ file.mediaType }}</span>
<span>
<strong>{{ file.fileName }}</strong>
<small>
{{ formatSize(file.sizeBytes) }}
<template v-if="file.videoDuration"> · {{ formatDuration(file.videoDuration) }}</template>
<template v-if="showLastPlayed && file.lastPlayedAt"> · {{ formatDate(file.lastPlayedAt) }}</template>
</small>
<small v-if="showCreatedTime && formatCreatedTime(file)">{{ formatCreatedTime(file) }}</small>
</span>
<span class="delete-btn" @click.stop="$emit('delete', file)" title="永久删除">×</span>
</button>
</template>
<style scoped>
.delete-btn {
margin-left: auto;
padding: 2px 8px;
color: #999;
font-size: 16px;
cursor: pointer;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.15s;
}
.mobile-file:hover .delete-btn {
opacity: 1;
}
.delete-btn:hover {
color: #e53e3e;
}
</style>
- Step 4: Add delete handler and wire events in ClientPage.vue
In <script setup>, after the existing saveFileProgress function (search for async function saveFileProgress), add:
async function deleteFile(file: FileRecordDto) {
if (!window.confirm(`确定要永久删除 "${file.fileName}" 吗?\n\n删除后无法恢复!`)) {
return
}
try {
await api.deleteFile(file.id)
// 刷新当前浏览数据
if (browseData.value) {
await loadBrowse()
}
// 刷新搜索结果
if (searchResults.value.length > 0) {
await loadSearch()
}
// 清除选中状态
if (selectedFile.value?.id === file.id) {
selectedFile.value = null
}
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '删除失败'
}
}
In the template, find each <FileCard and <FileListItem usage and add @delete="deleteFile":
For the displayedFiles grid (around line 628):
<FileCard
:file="file"
:selected="selectedFile?.id === file.id"
show-created-time
@select="selectFile"
@delete="deleteFile"
/>
For the displayedFiles list (around line 659):
<FileListItem
:file="file"
:selected="selectedFile?.id === file.id"
show-created-time
@select="selectFile"
@delete="deleteFile"
/>
For the searchResults grid (around line 466):
<FileCard :file="file" show-created-time @select="selectSearchFile" @delete="deleteFile" />
For the searchResults list (around line 491):
<FileListItem :file="file" show-created-time @select="selectSearchFile" @delete="deleteFile" />
For the recentFiles grid (around line 534):
<FileCard :file="file" :selected="selectedFile?.id === file.id" @select="selectFile" @delete="deleteFile" />
For the recentFiles list (around line 559):
<FileListItem :file="file" :selected="selectedFile?.id === file.id" @select="selectFile" @delete="deleteFile" />
Task 4: Verify
Covers: S4, S5
- Step 1: Build backend
dotnet build FileShare-API/FileShare-API.csproj
- Step 2: Build frontend
cd FileShare-Web-VUE && npx vue-tsc --noEmit
- Step 3: Verify no regressions
Ensure existing functionality still works by checking the build passes.