feat: 支持根据筛选条件批量编辑视频的下载状态 (#558)
This commit is contained in:
@@ -5,7 +5,7 @@ import type {
|
||||
VideosResponse,
|
||||
VideoResponse,
|
||||
ResetVideoResponse,
|
||||
ResetAllVideosResponse,
|
||||
ResetFilteredVideosResponse,
|
||||
UpdateVideoStatusRequest,
|
||||
UpdateVideoStatusResponse,
|
||||
ApiError,
|
||||
@@ -21,8 +21,12 @@ import type {
|
||||
DashBoardResponse,
|
||||
SysInfo,
|
||||
TaskStatus,
|
||||
ResetRequest,
|
||||
UpdateVideoSourceResponse
|
||||
ResetVideoStatusRequest,
|
||||
UpdateVideoSourceResponse,
|
||||
Notifier,
|
||||
UpdateFilteredVideoStatusRequest,
|
||||
UpdateFilteredVideoStatusResponse,
|
||||
ResetFilteredVideoStatusRequest
|
||||
} from './types';
|
||||
import { wsManager } from './ws';
|
||||
|
||||
@@ -152,12 +156,17 @@ class ApiClient {
|
||||
return this.get<VideoResponse>(`/videos/${id}`);
|
||||
}
|
||||
|
||||
async resetVideo(id: number, request: ResetRequest): Promise<ApiResponse<ResetVideoResponse>> {
|
||||
return this.post<ResetVideoResponse>(`/videos/${id}/reset`, request);
|
||||
async resetVideoStatus(
|
||||
id: number,
|
||||
request: ResetVideoStatusRequest
|
||||
): Promise<ApiResponse<ResetVideoResponse>> {
|
||||
return this.post<ResetVideoResponse>(`/videos/${id}/reset-status`, request);
|
||||
}
|
||||
|
||||
async resetAllVideos(request: ResetRequest): Promise<ApiResponse<ResetAllVideosResponse>> {
|
||||
return this.post<ResetAllVideosResponse>('/videos/reset-all', request);
|
||||
async resetFilteredVideoStatus(
|
||||
request: ResetFilteredVideoStatusRequest
|
||||
): Promise<ApiResponse<ResetFilteredVideosResponse>> {
|
||||
return this.post<ResetFilteredVideosResponse>('/videos/reset-status', request);
|
||||
}
|
||||
|
||||
async updateVideoStatus(
|
||||
@@ -167,6 +176,12 @@ class ApiClient {
|
||||
return this.post<UpdateVideoStatusResponse>(`/videos/${id}/update-status`, request);
|
||||
}
|
||||
|
||||
async updateFilteredVideoStatus(
|
||||
request: UpdateFilteredVideoStatusRequest
|
||||
): Promise<ApiResponse<UpdateFilteredVideoStatusResponse>> {
|
||||
return this.post<UpdateFilteredVideoStatusResponse>('/videos/update-status', request);
|
||||
}
|
||||
|
||||
async getCreatedFavorites(): Promise<ApiResponse<FavoritesResponse>> {
|
||||
return this.get<FavoritesResponse>('/me/favorites');
|
||||
}
|
||||
@@ -268,10 +283,14 @@ const api = {
|
||||
getVideoSources: () => apiClient.getVideoSources(),
|
||||
getVideos: (params?: VideosRequest) => apiClient.getVideos(params),
|
||||
getVideo: (id: number) => apiClient.getVideo(id),
|
||||
resetVideo: (id: number, request: ResetRequest) => apiClient.resetVideo(id, request),
|
||||
resetAllVideos: (request: ResetRequest) => apiClient.resetAllVideos(request),
|
||||
resetVideoStatus: (id: number, request: ResetVideoStatusRequest) =>
|
||||
apiClient.resetVideoStatus(id, request),
|
||||
resetFilteredVideoStatus: (request: ResetFilteredVideoStatusRequest) =>
|
||||
apiClient.resetFilteredVideoStatus(request),
|
||||
updateVideoStatus: (id: number, request: UpdateVideoStatusRequest) =>
|
||||
apiClient.updateVideoStatus(id, request),
|
||||
updateFilteredVideoStatus: (request: UpdateFilteredVideoStatusRequest) =>
|
||||
apiClient.updateFilteredVideoStatus(request),
|
||||
getCreatedFavorites: () => apiClient.getCreatedFavorites(),
|
||||
getFollowedCollections: (pageNum?: number, pageSize?: number) =>
|
||||
apiClient.getFollowedCollections(pageNum, pageSize),
|
||||
|
||||
322
web/src/lib/components/filtered-status-editor.svelte
Normal file
322
web/src/lib/components/filtered-status-editor.svelte
Normal file
@@ -0,0 +1,322 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle
|
||||
} from '$lib/components/ui/sheet/index.js';
|
||||
import type { StatusUpdate, UpdateFilteredVideoStatusRequest } from '$lib/types';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let open = false;
|
||||
export let hasFilters = false;
|
||||
export let loading = false;
|
||||
export let filterDescriptionParts: string[] = [];
|
||||
export let onsubmit: (request: UpdateFilteredVideoStatusRequest) => void;
|
||||
|
||||
// 视频任务名称(与后端 VideoStatus 对应)
|
||||
const videoTaskNames = ['视频封面', '视频信息', 'UP 主头像', 'UP 主信息', '分页下载'];
|
||||
|
||||
// 分页任务名称(与后端 PageStatus 对应)
|
||||
const pageTaskNames = ['视频封面', '视频内容', '视频信息', '视频弹幕', '视频字幕'];
|
||||
|
||||
// 状态选项:null 表示未选择,0 表示未开始,7 表示已完成
|
||||
type StatusValue = null | 0 | 7;
|
||||
|
||||
// 视频任务状态,默认都是 null(未选择)
|
||||
let videoStatuses: StatusValue[] = Array(5).fill(null);
|
||||
|
||||
// 分页任务状态,默认都是 null(未选择)
|
||||
let pageStatuses: StatusValue[] = Array(5).fill(null);
|
||||
|
||||
function setVideoStatus(taskIndex: number, value: StatusValue) {
|
||||
videoStatuses[taskIndex] = value;
|
||||
videoStatuses = [...videoStatuses];
|
||||
}
|
||||
|
||||
function setPageStatus(taskIndex: number, value: StatusValue) {
|
||||
pageStatuses[taskIndex] = value;
|
||||
pageStatuses = [...pageStatuses];
|
||||
}
|
||||
|
||||
function resetVideoStatus(taskIndex: number) {
|
||||
videoStatuses[taskIndex] = null;
|
||||
videoStatuses = [...videoStatuses];
|
||||
}
|
||||
|
||||
function resetPageStatus(taskIndex: number) {
|
||||
pageStatuses[taskIndex] = null;
|
||||
pageStatuses = [...pageStatuses];
|
||||
}
|
||||
|
||||
function resetAllStatuses() {
|
||||
videoStatuses = Array(5).fill(null);
|
||||
pageStatuses = Array(5).fill(null);
|
||||
}
|
||||
|
||||
function hasAnyChanges(): boolean {
|
||||
return (
|
||||
videoStatuses.some((status) => status !== null) ||
|
||||
pageStatuses.some((status) => status !== null)
|
||||
);
|
||||
}
|
||||
|
||||
function buildRequest(): UpdateFilteredVideoStatusRequest {
|
||||
const request: UpdateFilteredVideoStatusRequest = {};
|
||||
|
||||
// 添加视频更新
|
||||
const videoUpdates: StatusUpdate[] = [];
|
||||
videoStatuses.forEach((status, index) => {
|
||||
if (status !== null) {
|
||||
videoUpdates.push({
|
||||
status_index: index,
|
||||
status_value: status
|
||||
});
|
||||
}
|
||||
});
|
||||
if (videoUpdates.length > 0) {
|
||||
request.video_updates = videoUpdates;
|
||||
}
|
||||
|
||||
// 添加分页更新
|
||||
const pageUpdates: StatusUpdate[] = [];
|
||||
pageStatuses.forEach((status, index) => {
|
||||
if (status !== null) {
|
||||
pageUpdates.push({
|
||||
status_index: index,
|
||||
status_value: status
|
||||
});
|
||||
}
|
||||
});
|
||||
if (pageUpdates.length > 0) {
|
||||
request.page_updates = pageUpdates;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!hasAnyChanges()) {
|
||||
toast.info('请至少选择一个状态进行修改');
|
||||
return;
|
||||
}
|
||||
const request = buildRequest();
|
||||
onsubmit(request);
|
||||
}
|
||||
|
||||
// 当 Sheet 关闭时重置状态
|
||||
$: if (!open) {
|
||||
resetAllStatuses();
|
||||
}
|
||||
|
||||
function getStatusInfo(status: StatusValue) {
|
||||
if (status === 0) {
|
||||
return { label: '未开始', class: 'text-yellow-600', dotClass: 'bg-yellow-600' };
|
||||
}
|
||||
if (status === 7) {
|
||||
return { label: '已完成', class: 'text-emerald-600', dotClass: 'bg-emerald-600' };
|
||||
}
|
||||
return { label: '无修改', class: 'text-muted-foreground', dotClass: 'bg-muted-foreground' };
|
||||
}
|
||||
</script>
|
||||
|
||||
<Sheet bind:open>
|
||||
<SheetContent side="right" class="flex w-full flex-col sm:max-w-3xl">
|
||||
<SheetHeader class="px-6 pb-2">
|
||||
<SheetTitle class="text-lg">{hasFilters ? '编辑筛选视频' : '编辑全部视频'}</SheetTitle>
|
||||
<SheetDescription class="text-muted-foreground space-y-2 text-sm"
|
||||
>批量编辑视频和分页的下载状态。可将任意子任务状态修改为“未开始”或“已完成”。<br />
|
||||
{#if hasFilters}
|
||||
正在编辑<strong>符合以下筛选条件</strong>的视频的下载状态:
|
||||
<div class="bg-muted my-2 rounded-md p-2 text-left">
|
||||
{#each filterDescriptionParts as part, index (index)}
|
||||
<div><strong>{part}</strong></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
正在编辑<strong>全部视频</strong>的下载状态。 <br />
|
||||
{/if}
|
||||
<div class="leading-relaxed text-orange-600">
|
||||
⚠️ 仅当分页下载状态不是"已完成"时,程序才会尝试执行分页下载。
|
||||
</div>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-6">
|
||||
<div class="space-y-6 py-2">
|
||||
<!-- 视频状态编辑 -->
|
||||
<div>
|
||||
<h3 class="mb-4 text-base font-medium">视频状态</h3>
|
||||
<div class="bg-card rounded-lg border p-4">
|
||||
<div class="space-y-3">
|
||||
{#each videoTaskNames as taskName, index (index)}
|
||||
{@const statusInfo = getStatusInfo(videoStatuses[index])}
|
||||
{@const isModified = videoStatuses[index] !== null}
|
||||
<div
|
||||
class="bg-background hover:bg-muted/30 flex items-center justify-between rounded-md border p-3 transition-colors {isModified
|
||||
? 'border-blue-200 ring-2 ring-blue-500/20'
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">{taskName}</span>
|
||||
{#if isModified}
|
||||
<span class="hidden text-xs font-medium text-blue-600 sm:inline"
|
||||
>已修改</span
|
||||
>
|
||||
<div
|
||||
class="h-2 w-2 rounded-full bg-blue-500 sm:hidden"
|
||||
title="已修改"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-0.5 flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full {statusInfo.dotClass}"></div>
|
||||
<span class="text-xs {statusInfo.class}">{statusInfo.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1.5">
|
||||
{#if isModified}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => resetVideoStatus(index)}
|
||||
disabled={loading}
|
||||
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs text-gray-600 hover:bg-gray-100"
|
||||
title="恢复到原始状态"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
variant={videoStatuses[index] === 0 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onclick={() => setVideoStatus(index, 0)}
|
||||
disabled={loading}
|
||||
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs {videoStatuses[index] ===
|
||||
0
|
||||
? 'border-yellow-600 bg-yellow-600 font-medium text-white hover:bg-yellow-700'
|
||||
: 'hover:border-yellow-400 hover:bg-yellow-50 hover:text-yellow-700'}"
|
||||
>
|
||||
未开始
|
||||
</Button>
|
||||
<Button
|
||||
variant={videoStatuses[index] === 7 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onclick={() => setVideoStatus(index, 7)}
|
||||
disabled={loading}
|
||||
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs {videoStatuses[index] ===
|
||||
7
|
||||
? 'border-emerald-600 bg-emerald-600 font-medium text-white hover:bg-emerald-700'
|
||||
: 'hover:border-emerald-400 hover:bg-emerald-50 hover:text-emerald-700'}"
|
||||
>
|
||||
已完成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页状态编辑 -->
|
||||
<div>
|
||||
<h3 class="mb-4 text-base font-medium">分页状态</h3>
|
||||
<div class="bg-card rounded-lg border p-4">
|
||||
<div class="space-y-3">
|
||||
{#each pageTaskNames as taskName, index (index)}
|
||||
{@const statusInfo = getStatusInfo(pageStatuses[index])}
|
||||
{@const isModified = pageStatuses[index] !== null}
|
||||
<div
|
||||
class="bg-background hover:bg-muted/30 flex items-center justify-between rounded-md border p-3 transition-colors {isModified
|
||||
? 'border-blue-200 ring-2 ring-blue-500/20'
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">{taskName}</span>
|
||||
{#if isModified}
|
||||
<span class="hidden text-xs font-medium text-blue-600 sm:inline"
|
||||
>已修改</span
|
||||
>
|
||||
<div
|
||||
class="h-2 w-2 rounded-full bg-blue-500 sm:hidden"
|
||||
title="已修改"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-0.5 flex items-center gap-1.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full {statusInfo.dotClass}"></div>
|
||||
<span class="text-xs {statusInfo.class}">{statusInfo.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1.5">
|
||||
{#if isModified}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => resetPageStatus(index)}
|
||||
disabled={loading}
|
||||
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs text-gray-600 hover:bg-gray-100"
|
||||
title="恢复到原始状态"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
variant={pageStatuses[index] === 0 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onclick={() => setPageStatus(index, 0)}
|
||||
disabled={loading}
|
||||
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs {pageStatuses[index] === 0
|
||||
? 'border-yellow-600 bg-yellow-600 font-medium text-white hover:bg-yellow-700'
|
||||
: 'hover:border-yellow-400 hover:bg-yellow-50 hover:text-yellow-700'}"
|
||||
>
|
||||
未开始
|
||||
</Button>
|
||||
<Button
|
||||
variant={pageStatuses[index] === 7 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onclick={() => setPageStatus(index, 7)}
|
||||
disabled={loading}
|
||||
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs {pageStatuses[index] === 7
|
||||
? 'border-emerald-600 bg-emerald-600 font-medium text-white hover:bg-emerald-700'
|
||||
: 'hover:border-emerald-400 hover:bg-emerald-50 hover:text-emerald-700'}"
|
||||
>
|
||||
已完成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetFooter class="bg-background flex gap-2 border-t px-6 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={resetAllStatuses}
|
||||
disabled={!hasAnyChanges() || loading}
|
||||
class="flex-1 cursor-pointer"
|
||||
>
|
||||
重置所有状态
|
||||
</Button>
|
||||
<Button
|
||||
onclick={handleSubmit}
|
||||
disabled={loading || !hasAnyChanges()}
|
||||
class="flex-1 cursor-pointer"
|
||||
>
|
||||
{loading ? '提交中...' : '提交更改'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
@@ -31,6 +31,41 @@ export const ToQuery = (state: AppState): string => {
|
||||
return queryString ? `videos?${queryString}` : 'videos';
|
||||
};
|
||||
|
||||
// 将 AppState 转换为请求体中的筛选参数
|
||||
export const ToFilterParams = (
|
||||
state: AppState
|
||||
): {
|
||||
query?: string;
|
||||
collection?: number;
|
||||
favorite?: number;
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
} => {
|
||||
const params: {
|
||||
query?: string;
|
||||
collection?: number;
|
||||
favorite?: number;
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
} = {};
|
||||
|
||||
if (state.query.trim()) {
|
||||
params.query = state.query;
|
||||
}
|
||||
|
||||
if (state.videoSource && state.videoSource.type && state.videoSource.id) {
|
||||
const { type, id } = state.videoSource;
|
||||
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
// 检查是否有活动的筛选条件
|
||||
export const hasActiveFilters = (state: AppState): boolean => {
|
||||
return !!(state.query.trim() || state.videoSource);
|
||||
};
|
||||
|
||||
export const setQuery = (query: string) => {
|
||||
appStateStore.update((state) => ({
|
||||
...state,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// API 响应包装器
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
status_code: number;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 请求参数类型
|
||||
export interface VideosRequest {
|
||||
collection?: number;
|
||||
favorite?: number;
|
||||
@@ -16,13 +13,11 @@ export interface VideosRequest {
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
// 视频来源类型
|
||||
export interface VideoSource {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 视频来源响应类型
|
||||
export interface VideoSourcesResponse {
|
||||
collection: VideoSource[];
|
||||
favorite: VideoSource[];
|
||||
@@ -30,7 +25,6 @@ export interface VideoSourcesResponse {
|
||||
watch_later: VideoSource[];
|
||||
}
|
||||
|
||||
// 视频信息类型
|
||||
export interface VideoInfo {
|
||||
id: number;
|
||||
bvid: string;
|
||||
@@ -40,13 +34,11 @@ export interface VideoInfo {
|
||||
download_status: [number, number, number, number, number];
|
||||
}
|
||||
|
||||
// 视频列表响应类型
|
||||
export interface VideosResponse {
|
||||
videos: VideoInfo[];
|
||||
total_count: number;
|
||||
}
|
||||
|
||||
// 分页信息类型
|
||||
export interface PageInfo {
|
||||
id: number;
|
||||
pid: number;
|
||||
@@ -54,59 +46,75 @@ export interface PageInfo {
|
||||
download_status: [number, number, number, number, number];
|
||||
}
|
||||
|
||||
// 单个视频响应类型
|
||||
export interface VideoResponse {
|
||||
video: VideoInfo;
|
||||
pages: PageInfo[];
|
||||
}
|
||||
|
||||
// 重置视频响应类型
|
||||
export interface ResetVideoResponse {
|
||||
resetted: boolean;
|
||||
video: VideoInfo;
|
||||
pages: PageInfo[];
|
||||
}
|
||||
|
||||
// 重置所有视频响应类型
|
||||
export interface ResetAllVideosResponse {
|
||||
export interface ResetFilteredVideosResponse {
|
||||
resetted: boolean;
|
||||
resetted_videos_count: number;
|
||||
resetted_pages_count: number;
|
||||
}
|
||||
|
||||
// API 错误类型
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
// 状态更新类型
|
||||
export interface StatusUpdate {
|
||||
status_index: number;
|
||||
status_value: number;
|
||||
}
|
||||
|
||||
// 页面状态更新类型
|
||||
export interface PageStatusUpdate {
|
||||
page_id: number;
|
||||
updates: StatusUpdate[];
|
||||
}
|
||||
|
||||
// 重置视频状态请求类型
|
||||
export interface UpdateVideoStatusRequest {
|
||||
video_updates?: StatusUpdate[];
|
||||
page_updates?: PageStatusUpdate[];
|
||||
}
|
||||
|
||||
// 重置视频状态响应类型
|
||||
export interface UpdateVideoStatusResponse {
|
||||
success: boolean;
|
||||
video: VideoInfo;
|
||||
pages: PageInfo[];
|
||||
}
|
||||
|
||||
// 重置请求类型
|
||||
export interface ResetRequest {
|
||||
export interface UpdateFilteredVideoStatusResponse {
|
||||
success: boolean;
|
||||
updated_videos_count: number;
|
||||
updated_pages_count: number;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface StatusUpdate {
|
||||
status_index: number;
|
||||
status_value: number;
|
||||
}
|
||||
|
||||
export interface PageStatusUpdate {
|
||||
page_id: number;
|
||||
updates: StatusUpdate[];
|
||||
}
|
||||
|
||||
export interface UpdateVideoStatusRequest {
|
||||
video_updates?: StatusUpdate[];
|
||||
page_updates?: PageStatusUpdate[];
|
||||
}
|
||||
|
||||
export interface UpdateFilteredVideoStatusRequest {
|
||||
collection?: number;
|
||||
favorite?: number;
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
query?: string;
|
||||
video_updates?: StatusUpdate[];
|
||||
page_updates?: StatusUpdate[];
|
||||
}
|
||||
|
||||
export interface ResetVideoStatusRequest {
|
||||
force: boolean;
|
||||
}
|
||||
|
||||
export interface ResetFilteredVideoStatusRequest {
|
||||
collection?: number;
|
||||
favorite?: number;
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
query?: string;
|
||||
force: boolean;
|
||||
}
|
||||
|
||||
@@ -170,7 +178,6 @@ export interface InsertSubmissionRequest {
|
||||
path: string;
|
||||
}
|
||||
|
||||
// Rule 相关类型
|
||||
export interface Condition<T> {
|
||||
operator: string;
|
||||
value: T | T[];
|
||||
@@ -184,7 +191,6 @@ export interface RuleTarget<T> {
|
||||
export type AndGroup = RuleTarget<string | number | Date>[];
|
||||
export type Rule = AndGroup[];
|
||||
|
||||
// 视频源详细信息类型
|
||||
export interface VideoSourceDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -195,7 +201,6 @@ export interface VideoSourceDetail {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// 视频源详细信息响应类型
|
||||
export interface VideoSourcesDetailsResponse {
|
||||
collections: VideoSourceDetail[];
|
||||
favorites: VideoSourceDetail[];
|
||||
@@ -203,7 +208,6 @@ export interface VideoSourcesDetailsResponse {
|
||||
watch_later: VideoSourceDetail[];
|
||||
}
|
||||
|
||||
// 更新视频源请求类型
|
||||
export interface UpdateVideoSourceRequest {
|
||||
path: string;
|
||||
enabled: boolean;
|
||||
@@ -211,7 +215,6 @@ export interface UpdateVideoSourceRequest {
|
||||
useDynamicApi?: boolean | null;
|
||||
}
|
||||
|
||||
// 配置相关类型
|
||||
export interface Credential {
|
||||
sessdata: string;
|
||||
bili_jct: string;
|
||||
@@ -273,7 +276,6 @@ export interface ConcurrentLimit {
|
||||
download: ConcurrentDownloadLimit;
|
||||
}
|
||||
|
||||
// Notifier 相关类型
|
||||
export interface TelegramNotifier {
|
||||
type: 'telegram';
|
||||
bot_token: string;
|
||||
@@ -312,13 +314,11 @@ export interface Config {
|
||||
version: number;
|
||||
}
|
||||
|
||||
// 日期计数对类型
|
||||
export interface DayCountPair {
|
||||
day: string;
|
||||
cnt: number;
|
||||
}
|
||||
|
||||
// 仪表盘响应类型
|
||||
export interface DashBoardResponse {
|
||||
enabled_favorites: number;
|
||||
enabled_collections: number;
|
||||
@@ -327,7 +327,6 @@ export interface DashBoardResponse {
|
||||
videos_by_day: DayCountPair[];
|
||||
}
|
||||
|
||||
// 系统信息响应类型
|
||||
export interface SysInfo {
|
||||
total_memory: number;
|
||||
used_memory: number;
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
bind:resetting
|
||||
onReset={async (forceReset: boolean) => {
|
||||
try {
|
||||
const result = await api.resetVideo(videoData!.video.id, { force: forceReset });
|
||||
const result = await api.resetVideoStatus(videoData!.video.id, { force: forceReset });
|
||||
const data = result.data;
|
||||
if (data.resetted) {
|
||||
videoData = {
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
import Pagination from '$lib/components/pagination.svelte';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
|
||||
import EditIcon from '@lucide/svelte/icons/edit';
|
||||
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
|
||||
import api from '$lib/api';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import type { VideosResponse, VideoSourcesResponse, ApiError, VideoSource } from '$lib/types';
|
||||
import type {
|
||||
VideosResponse,
|
||||
VideoSourcesResponse,
|
||||
ApiError,
|
||||
VideoSource,
|
||||
UpdateFilteredVideoStatusRequest
|
||||
} from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -19,11 +26,14 @@
|
||||
setAll,
|
||||
setCurrentPage,
|
||||
setQuery,
|
||||
ToQuery
|
||||
ToQuery,
|
||||
ToFilterParams,
|
||||
hasActiveFilters
|
||||
} from '$lib/stores/filter';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import DropdownFilter, { type Filter } from '$lib/components/dropdown-filter.svelte';
|
||||
import SearchBar from '$lib/components/search-bar.svelte';
|
||||
import FilteredStatusEditor from '$lib/components/filtered-status-editor.svelte';
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
@@ -37,6 +47,9 @@
|
||||
|
||||
let forceReset = false;
|
||||
|
||||
let updateAllDialogOpen = false;
|
||||
let updatingAll = false;
|
||||
|
||||
let videoSources: VideoSourcesResponse | null = null;
|
||||
let filters: Record<string, Filter> | null = null;
|
||||
|
||||
@@ -97,7 +110,7 @@
|
||||
|
||||
async function handleResetVideo(id: number, forceReset: boolean) {
|
||||
try {
|
||||
const result = await api.resetVideo(id, { force: forceReset });
|
||||
const result = await api.resetVideoStatus(id, { force: forceReset });
|
||||
const data = result.data;
|
||||
if (data.resetted) {
|
||||
toast.success('重置成功', {
|
||||
@@ -121,7 +134,12 @@
|
||||
async function handleResetAllVideos() {
|
||||
resettingAll = true;
|
||||
try {
|
||||
const result = await api.resetAllVideos({ force: forceReset });
|
||||
// 获取筛选参数
|
||||
const filterParams = ToFilterParams($appStateStore);
|
||||
const result = await api.resetFilteredVideoStatus({
|
||||
...filterParams,
|
||||
force: forceReset
|
||||
});
|
||||
const data = result.data;
|
||||
if (data.resetted) {
|
||||
toast.success('重置成功', {
|
||||
@@ -143,6 +161,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateAllVideos(request: UpdateFilteredVideoStatusRequest) {
|
||||
updatingAll = true;
|
||||
try {
|
||||
// 获取筛选参数并合并
|
||||
const filterParams = ToFilterParams($appStateStore);
|
||||
const fullRequest = {
|
||||
...filterParams,
|
||||
...request
|
||||
};
|
||||
const result = await api.updateFilteredVideoStatus(fullRequest);
|
||||
const data = result.data;
|
||||
if (data.success) {
|
||||
toast.success('更新成功', {
|
||||
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
|
||||
});
|
||||
const { query, currentPage, videoSource } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource);
|
||||
} else {
|
||||
toast.info('没有视频被更新');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新失败:', error);
|
||||
toast.error('更新失败', {
|
||||
description: (error as ApiError).message
|
||||
});
|
||||
} finally {
|
||||
updatingAll = false;
|
||||
updateAllDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取筛选条件的显示数组
|
||||
function getFilterDescriptionParts(): string[] {
|
||||
const state = $appStateStore;
|
||||
const parts: string[] = [];
|
||||
if (state.query.trim()) {
|
||||
parts.push(`搜索词:"${state.query}"`);
|
||||
}
|
||||
if (state.videoSource && videoSources) {
|
||||
const sourceType = state.videoSource.type;
|
||||
const sourceId = parseInt(state.videoSource.id);
|
||||
const sourceConfig = Object.values(VIDEO_SOURCES).find((s) => s.type === sourceType);
|
||||
if (sourceConfig) {
|
||||
const sourceList = videoSources[sourceType as keyof VideoSourcesResponse] as VideoSource[];
|
||||
const source = sourceList.find((s) => s.id === sourceId);
|
||||
if (source) {
|
||||
parts.push(`${sourceConfig.title}:${source.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
$: if ($page.url.search !== lastSearch) {
|
||||
lastSearch = $page.url.search;
|
||||
handleSearchParamsChange($page.url.searchParams);
|
||||
@@ -177,6 +248,8 @@
|
||||
});
|
||||
|
||||
$: totalPages = videosData ? Math.ceil(videosData.total_count / pageSize) : 0;
|
||||
$: hasFilters = hasActiveFilters($appStateStore);
|
||||
$: filterDescriptionParts = videoSources && $appStateStore && getFilterDescriptionParts();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -221,6 +294,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="hover:bg-accent hover:text-accent-foreground h-8 cursor-pointer text-xs font-medium"
|
||||
onclick={() => (updateAllDialogOpen = true)}
|
||||
disabled={updatingAll || loading}
|
||||
>
|
||||
<EditIcon class="mr-1.5 h-3 w-3" />
|
||||
{hasFilters ? '编辑筛选' : '编辑全部'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -229,7 +312,7 @@
|
||||
disabled={resettingAll || loading}
|
||||
>
|
||||
<RotateCcwIcon class="mr-1.5 h-3 w-3 {resettingAll ? 'animate-spin' : ''}" />
|
||||
重置全部
|
||||
{hasFilters ? '重置筛选' : '重置全部'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,16 +354,25 @@
|
||||
<AlertDialog.Root bind:open={resetAllDialogOpen}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>重置全部视频</AlertDialog.Title>
|
||||
<AlertDialog.Title>{hasFilters ? '重置筛选视频' : '重置全部视频'}</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
确定要重置<strong>全部视频</strong>的下载状态吗?<br />
|
||||
{#if hasFilters}
|
||||
确定要重置<strong>符合以下筛选条件</strong>的视频的下载状态吗?<br />
|
||||
<div class="bg-muted my-2 rounded-md p-2 text-left">
|
||||
{#each filterDescriptionParts as part, index (index)}
|
||||
<div><strong>{part}</strong></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
确定要重置<strong>全部视频</strong>的下载状态吗?<br />
|
||||
{/if}
|
||||
此操作会将所有的失败状态重置为未开始,<span class="text-destructive font-medium"
|
||||
>无法撤销</span
|
||||
>。
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="py-2">
|
||||
<div class="rounded-lg border border-orange-200 bg-orange-50 p-3">
|
||||
<div class="mb-2 flex items-center space-x-2">
|
||||
<Checkbox id="force-reset-all" bind:checked={forceReset} />
|
||||
@@ -317,3 +409,11 @@
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<FilteredStatusEditor
|
||||
bind:open={updateAllDialogOpen}
|
||||
{hasFilters}
|
||||
loading={updatingAll}
|
||||
filterDescriptionParts={filterDescriptionParts || []}
|
||||
onsubmit={handleUpdateAllVideos}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user