mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-05 15:39:07 +08:00
feat: 实现仅失败、仅成功、仅等待的筛选 (#610)
This commit is contained in:
@@ -1,9 +1,22 @@
|
|||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use sea_orm::{ConnectionTrait, DatabaseTransaction};
|
use sea_orm::{Condition, ConnectionTrait, DatabaseTransaction};
|
||||||
|
|
||||||
|
use crate::api::request::StatusFilter;
|
||||||
use crate::api::response::{PageInfo, SimplePageInfo, SimpleVideoInfo, VideoInfo};
|
use crate::api::response::{PageInfo, SimplePageInfo, SimpleVideoInfo, VideoInfo};
|
||||||
|
use crate::utils::status::VideoStatus;
|
||||||
|
|
||||||
|
impl StatusFilter {
|
||||||
|
pub fn to_video_query(&self) -> Condition {
|
||||||
|
let query_builder = VideoStatus::query_builder();
|
||||||
|
match self {
|
||||||
|
Self::Failed => query_builder.failed(),
|
||||||
|
Self::Succeeded => query_builder.succeeded(),
|
||||||
|
Self::Waiting => query_builder.waiting(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait VideoRecord {
|
pub trait VideoRecord {
|
||||||
fn as_id_status_tuple(&self) -> (i32, u32);
|
fn as_id_status_tuple(&self) -> (i32, u32);
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ use validator::Validate;
|
|||||||
|
|
||||||
use crate::bilibili::CollectionType;
|
use crate::bilibili::CollectionType;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum StatusFilter {
|
||||||
|
Failed,
|
||||||
|
Succeeded,
|
||||||
|
Waiting,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct VideosRequest {
|
pub struct VideosRequest {
|
||||||
pub collection: Option<i32>,
|
pub collection: Option<i32>,
|
||||||
@@ -11,8 +19,7 @@ pub struct VideosRequest {
|
|||||||
pub submission: Option<i32>,
|
pub submission: Option<i32>,
|
||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
#[serde(default)]
|
pub status_filter: Option<StatusFilter>,
|
||||||
pub failed_only: bool,
|
|
||||||
pub page: Option<u64>,
|
pub page: Option<u64>,
|
||||||
pub page_size: Option<u64>,
|
pub page_size: Option<u64>,
|
||||||
}
|
}
|
||||||
@@ -30,8 +37,7 @@ pub struct ResetFilteredVideoStatusRequest {
|
|||||||
pub submission: Option<i32>,
|
pub submission: Option<i32>,
|
||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
#[serde(default)]
|
pub status_filter: Option<StatusFilter>,
|
||||||
pub failed_only: bool,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
}
|
}
|
||||||
@@ -68,8 +74,7 @@ pub struct UpdateFilteredVideoStatusRequest {
|
|||||||
pub submission: Option<i32>,
|
pub submission: Option<i32>,
|
||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
#[serde(default)]
|
pub status_filter: Option<StatusFilter>,
|
||||||
pub failed_only: bool,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[validate(nested)]
|
#[validate(nested)]
|
||||||
pub video_updates: Vec<StatusUpdate>,
|
pub video_updates: Vec<StatusUpdate>,
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ pub async fn get_videos(
|
|||||||
.or(video::Column::Bvid.contains(query_word)),
|
.or(video::Column::Bvid.contains(query_word)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if params.failed_only {
|
if let Some(status_filter) = params.status_filter {
|
||||||
query = query.filter(VideoStatus::query_builder().any_failed())
|
query = query.filter(status_filter.to_video_query());
|
||||||
}
|
}
|
||||||
let total_count = query.clone().count(&db).await?;
|
let total_count = query.clone().count(&db).await?;
|
||||||
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
||||||
@@ -221,8 +221,8 @@ pub async fn reset_filtered_video_status(
|
|||||||
.or(video::Column::Bvid.contains(query_word)),
|
.or(video::Column::Bvid.contains(query_word)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if request.failed_only {
|
if let Some(status_filter) = request.status_filter {
|
||||||
query = query.filter(VideoStatus::query_builder().any_failed());
|
query = query.filter(status_filter.to_video_query());
|
||||||
}
|
}
|
||||||
let all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
let all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
||||||
let all_pages = page::Entity::find()
|
let all_pages = page::Entity::find()
|
||||||
@@ -357,8 +357,8 @@ pub async fn update_filtered_video_status(
|
|||||||
.or(video::Column::Bvid.contains(query_word)),
|
.or(video::Column::Bvid.contains(query_word)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if request.failed_only {
|
if let Some(status_filter) = request.status_filter {
|
||||||
query = query.filter(VideoStatus::query_builder().any_failed())
|
query = query.filter(status_filter.to_video_query());
|
||||||
}
|
}
|
||||||
let mut all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
let mut all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
||||||
let mut all_pages = page::Entity::find()
|
let mut all_pages = page::Entity::find()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use bili_sync_entity::{page, video};
|
use bili_sync_entity::{page, video};
|
||||||
use bili_sync_migration::ExprTrait;
|
use bili_sync_migration::{ExprTrait, IntoCondition};
|
||||||
use sea_orm::sea_query::Expr;
|
use sea_orm::sea_query::Expr;
|
||||||
use sea_orm::{ColumnTrait, Condition};
|
use sea_orm::{ColumnTrait, Condition};
|
||||||
|
|
||||||
@@ -213,7 +213,17 @@ impl<const N: usize, C: ColumnTrait> StatusQueryBuilder<N, C> {
|
|||||||
Self { column }
|
Self { column }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn any_failed(&self) -> Condition {
|
/// 完成状态:所有子任务的状态都是成功
|
||||||
|
pub fn succeeded(&self) -> Condition {
|
||||||
|
let mut condition = Condition::all();
|
||||||
|
for offset in 0..N as i32 {
|
||||||
|
condition = condition.add(Expr::col(self.column).right_shift(offset * 3).bit_and(7).eq(7))
|
||||||
|
}
|
||||||
|
condition
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 失败状态:存在任何失败的子任务
|
||||||
|
pub fn failed(&self) -> Condition {
|
||||||
let mut condition = Condition::any();
|
let mut condition = Condition::any();
|
||||||
for offset in 0..N as i32 {
|
for offset in 0..N as i32 {
|
||||||
condition = condition.add(
|
condition = condition.add(
|
||||||
@@ -225,6 +235,15 @@ impl<const N: usize, C: ColumnTrait> StatusQueryBuilder<N, C> {
|
|||||||
}
|
}
|
||||||
condition
|
condition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 等待状态:所有子任务的状态都不是失败,且其中存在未开始
|
||||||
|
pub fn waiting(&self) -> Condition {
|
||||||
|
let mut condition = Condition::any();
|
||||||
|
for offset in 0..N as i32 {
|
||||||
|
condition = condition.add(Expr::col(self.column).right_shift(offset * 3).bit_and(7).eq(0))
|
||||||
|
}
|
||||||
|
condition.and(self.failed().not()).into_condition()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content class="w-[200px]" align="end">
|
<DropdownMenu.Content class="w-50" align="end">
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
{#if filters}
|
{#if filters}
|
||||||
{#each Object.entries(filters) as [key, filter] (key)}
|
{#each Object.entries(filters) as [key, filter] (key)}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CheckCircleIcon from '@lucide/svelte/icons/check-circle';
|
||||||
|
import XCircleIcon from '@lucide/svelte/icons/x-circle';
|
||||||
|
import ClockIcon from '@lucide/svelte/icons/clock';
|
||||||
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||||
|
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { type StatusFilterValue } from '$lib/stores/filter';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: StatusFilterValue | null;
|
||||||
|
onSelect?: (value: StatusFilterValue) => void;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { value = $bindable(null), onSelect, onRemove }: Props = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let triggerRef = $state<HTMLButtonElement>(null!);
|
||||||
|
|
||||||
|
function closeAndFocusTrigger() {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{
|
||||||
|
value: 'failed' as const,
|
||||||
|
label: '仅失败',
|
||||||
|
icon: XCircleIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'succeeded' as const,
|
||||||
|
label: '仅成功',
|
||||||
|
icon: CheckCircleIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'waiting' as const,
|
||||||
|
label: '仅等待',
|
||||||
|
icon: ClockIcon
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleSelect(selectedValue: StatusFilterValue) {
|
||||||
|
value = selectedValue;
|
||||||
|
onSelect?.(selectedValue);
|
||||||
|
closeAndFocusTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentOption = $derived(statusOptions.find((opt) => opt.value === value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="inline-flex items-center gap-1">
|
||||||
|
<span class="bg-secondary text-secondary-foreground rounded-lg px-2 py-1 text-xs font-medium">
|
||||||
|
{currentOption ? currentOption.label : '未应用'}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<DropdownMenu.Root bind:open>
|
||||||
|
<DropdownMenu.Trigger bind:ref={triggerRef}>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="ghost" size="sm" {...props} class="h-6 w-6 p-0">
|
||||||
|
<ChevronDownIcon class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content class="w-50" align="end">
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Label class="text-xs">视频状态</DropdownMenu.Label>
|
||||||
|
{#each statusOptions as option (option.value)}
|
||||||
|
<DropdownMenu.Item class="text-xs" onclick={() => handleSelect(option.value)}>
|
||||||
|
<option.icon class="mr-2 size-3" />
|
||||||
|
<span class:font-semibold={value === option.value}>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
{#if value === option.value}
|
||||||
|
<CheckCircleIcon class="ml-auto size-3" />
|
||||||
|
{/if}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/each}
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onclick={() => {
|
||||||
|
closeAndFocusTrigger();
|
||||||
|
onRemove?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon class="mr-2 size-3" />
|
||||||
|
<span class="text-xs font-medium">移除筛选</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type StatusFilterValue = 'failed' | 'succeeded' | 'waiting' | null;
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
query: string;
|
query: string;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
@@ -7,18 +9,18 @@ export interface AppState {
|
|||||||
type: string;
|
type: string;
|
||||||
id: string;
|
id: string;
|
||||||
} | null;
|
} | null;
|
||||||
failedOnly: boolean;
|
statusFilter: StatusFilterValue | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appStateStore = writable<AppState>({
|
export const appStateStore = writable<AppState>({
|
||||||
query: '',
|
query: '',
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
videoSource: null,
|
videoSource: null,
|
||||||
failedOnly: false
|
statusFilter: null
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ToQuery = (state: AppState): string => {
|
export const ToQuery = (state: AppState): string => {
|
||||||
const { query, videoSource, currentPage, failedOnly } = state;
|
const { query, videoSource, currentPage, statusFilter } = state;
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
params.set('page', String(currentPage));
|
params.set('page', String(currentPage));
|
||||||
@@ -29,8 +31,8 @@ export const ToQuery = (state: AppState): string => {
|
|||||||
if (videoSource && videoSource.type && videoSource.id) {
|
if (videoSource && videoSource.type && videoSource.id) {
|
||||||
params.set(videoSource.type, videoSource.id);
|
params.set(videoSource.type, videoSource.id);
|
||||||
}
|
}
|
||||||
if (failedOnly) {
|
if (statusFilter) {
|
||||||
params.set('failed_only', 'true');
|
params.set('status_filter', statusFilter);
|
||||||
}
|
}
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
return queryString ? `videos?${queryString}` : 'videos';
|
return queryString ? `videos?${queryString}` : 'videos';
|
||||||
@@ -45,7 +47,7 @@ export const ToFilterParams = (
|
|||||||
favorite?: number;
|
favorite?: number;
|
||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
failed_only?: boolean;
|
status_filter?: Exclude<StatusFilterValue, null>;
|
||||||
} => {
|
} => {
|
||||||
const params: {
|
const params: {
|
||||||
query?: string;
|
query?: string;
|
||||||
@@ -53,7 +55,7 @@ export const ToFilterParams = (
|
|||||||
favorite?: number;
|
favorite?: number;
|
||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
failed_only?: boolean;
|
status_filter?: Exclude<StatusFilterValue, null>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (state.query.trim()) {
|
if (state.query.trim()) {
|
||||||
@@ -64,15 +66,15 @@ export const ToFilterParams = (
|
|||||||
const { type, id } = state.videoSource;
|
const { type, id } = state.videoSource;
|
||||||
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
|
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
|
||||||
}
|
}
|
||||||
if (state.failedOnly) {
|
if (state.statusFilter) {
|
||||||
params.failed_only = true;
|
params.status_filter = state.statusFilter;
|
||||||
}
|
}
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查是否有活动的筛选条件
|
// 检查是否有活动的筛选条件
|
||||||
export const hasActiveFilters = (state: AppState): boolean => {
|
export const hasActiveFilters = (state: AppState): boolean => {
|
||||||
return !!(state.query.trim() || state.videoSource || state.failedOnly);
|
return !!(state.query.trim() || state.videoSource || state.statusFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setQuery = (query: string) => {
|
export const setQuery = (query: string) => {
|
||||||
@@ -82,20 +84,6 @@ export const setQuery = (query: string) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setVideoSourceFilter = (filter: { type: string; id: string }) => {
|
|
||||||
appStateStore.update((state) => ({
|
|
||||||
...state,
|
|
||||||
videoSource: filter
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearVideoSourceFilter = () => {
|
|
||||||
appStateStore.update((state) => ({
|
|
||||||
...state,
|
|
||||||
videoSource: null
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setCurrentPage = (page: number) => {
|
export const setCurrentPage = (page: number) => {
|
||||||
appStateStore.update((state) => ({
|
appStateStore.update((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -103,10 +91,10 @@ export const setCurrentPage = (page: number) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setFailedOnly = (failedOnly: boolean) => {
|
export const setStatusFilter = (statusFilter: StatusFilterValue | null) => {
|
||||||
appStateStore.update((state) => ({
|
appStateStore.update((state) => ({
|
||||||
...state,
|
...state,
|
||||||
failedOnly
|
statusFilter
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,21 +109,12 @@ export const setAll = (
|
|||||||
query: string,
|
query: string,
|
||||||
currentPage: number,
|
currentPage: number,
|
||||||
videoSource: { type: string; id: string } | null,
|
videoSource: { type: string; id: string } | null,
|
||||||
failedOnly: boolean
|
statusFilter: StatusFilterValue | null
|
||||||
) => {
|
) => {
|
||||||
appStateStore.set({
|
appStateStore.set({
|
||||||
query,
|
query,
|
||||||
currentPage,
|
currentPage,
|
||||||
videoSource,
|
videoSource,
|
||||||
failedOnly
|
statusFilter
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearAll = () => {
|
|
||||||
appStateStore.set({
|
|
||||||
query: '',
|
|
||||||
currentPage: 0,
|
|
||||||
videoSource: null,
|
|
||||||
failedOnly: false
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,15 +26,17 @@
|
|||||||
setAll,
|
setAll,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
setQuery,
|
setQuery,
|
||||||
setFailedOnly,
|
setStatusFilter,
|
||||||
ToQuery,
|
ToQuery,
|
||||||
ToFilterParams,
|
ToFilterParams,
|
||||||
hasActiveFilters
|
hasActiveFilters,
|
||||||
|
type StatusFilterValue
|
||||||
} from '$lib/stores/filter';
|
} from '$lib/stores/filter';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import DropdownFilter, { type Filter } from '$lib/components/dropdown-filter.svelte';
|
import DropdownFilter, { type Filter } from '$lib/components/dropdown-filter.svelte';
|
||||||
import SearchBar from '$lib/components/search-bar.svelte';
|
import SearchBar from '$lib/components/search-bar.svelte';
|
||||||
import FilteredStatusEditor from '$lib/components/filtered-status-editor.svelte';
|
import FilteredStatusEditor from '$lib/components/filtered-status-editor.svelte';
|
||||||
|
import StatusFilter from '$lib/components/status-filter.svelte';
|
||||||
|
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
|
|
||||||
@@ -62,13 +64,18 @@
|
|||||||
videoSource = { type: source.type, id: value };
|
videoSource = { type: source.type, id: value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 支持从 URL 里还原失败筛选
|
// 支持从 URL 里还原状态筛选
|
||||||
const failedParam = searchParams.get('failed_only');
|
const statusFilterParam = searchParams.get('status_filter');
|
||||||
const failedOnly = failedParam === 'true' || failedParam === '1';
|
const statusFilter: StatusFilterValue | null =
|
||||||
|
statusFilterParam === 'failed' ||
|
||||||
|
statusFilterParam === 'succeeded' ||
|
||||||
|
statusFilterParam === 'waiting'
|
||||||
|
? statusFilterParam
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
query: searchParams.get('query') || '',
|
query: searchParams.get('query') || '',
|
||||||
videoSource,
|
videoSource,
|
||||||
failedOnly,
|
statusFilter,
|
||||||
pageNum: parseInt(searchParams.get('page') || '0')
|
pageNum: parseInt(searchParams.get('page') || '0')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -77,7 +84,7 @@
|
|||||||
query: string,
|
query: string,
|
||||||
pageNum: number = 0,
|
pageNum: number = 0,
|
||||||
filter?: { type: string; id: string } | null,
|
filter?: { type: string; id: string } | null,
|
||||||
failedOnly: boolean = false
|
statusFilter: StatusFilterValue | null = null
|
||||||
) {
|
) {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
@@ -91,7 +98,9 @@
|
|||||||
if (filter) {
|
if (filter) {
|
||||||
params[filter.type] = parseInt(filter.id);
|
params[filter.type] = parseInt(filter.id);
|
||||||
}
|
}
|
||||||
params.failed_only = failedOnly;
|
if (statusFilter) {
|
||||||
|
params.status_filter = statusFilter;
|
||||||
|
}
|
||||||
const result = await api.getVideos(params);
|
const result = await api.getVideos(params);
|
||||||
videosData = result.data;
|
videosData = result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -110,9 +119,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSearchParamsChange(searchParams: URLSearchParams) {
|
async function handleSearchParamsChange(searchParams: URLSearchParams) {
|
||||||
const { query, videoSource, pageNum, failedOnly } = getApiParams(searchParams);
|
const { query, videoSource, pageNum, statusFilter } = getApiParams(searchParams);
|
||||||
setAll(query, pageNum, videoSource, failedOnly);
|
setAll(query, pageNum, videoSource, statusFilter);
|
||||||
loadVideos(query, pageNum, videoSource, failedOnly);
|
loadVideos(query, pageNum, videoSource, statusFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleResetVideo(id: number, forceReset: boolean) {
|
async function handleResetVideo(id: number, forceReset: boolean) {
|
||||||
@@ -123,8 +132,8 @@
|
|||||||
toast.success('重置成功', {
|
toast.success('重置成功', {
|
||||||
description: `视频「${data.video.name}」已重置`
|
description: `视频「${data.video.name}」已重置`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
const { query, currentPage, videoSource, statusFilter } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource, failedOnly);
|
await loadVideos(query, currentPage, videoSource, statusFilter);
|
||||||
} else {
|
} else {
|
||||||
toast.info('重置无效', {
|
toast.info('重置无效', {
|
||||||
description: `视频「${data.video.name}」没有失败的状态,无需重置`
|
description: `视频「${data.video.name}」没有失败的状态,无需重置`
|
||||||
@@ -151,8 +160,8 @@
|
|||||||
description: `视频「${data.video.name}」已清空重置`
|
description: `视频「${data.video.name}」已清空重置`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
const { query, currentPage, videoSource, statusFilter } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource, failedOnly);
|
await loadVideos(query, currentPage, videoSource, statusFilter);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清空重置失败:', error);
|
console.error('清空重置失败:', error);
|
||||||
toast.error('清空重置失败', {
|
toast.error('清空重置失败', {
|
||||||
@@ -175,8 +184,8 @@
|
|||||||
toast.success('重置成功', {
|
toast.success('重置成功', {
|
||||||
description: `已重置 ${data.resetted_videos_count} 个视频和 ${data.resetted_pages_count} 个分页`
|
description: `已重置 ${data.resetted_videos_count} 个视频和 ${data.resetted_pages_count} 个分页`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
const { query, currentPage, videoSource, statusFilter } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource, failedOnly);
|
await loadVideos(query, currentPage, videoSource, statusFilter);
|
||||||
} else {
|
} else {
|
||||||
toast.info('没有需要重置的视频');
|
toast.info('没有需要重置的视频');
|
||||||
}
|
}
|
||||||
@@ -206,8 +215,8 @@
|
|||||||
toast.success('更新成功', {
|
toast.success('更新成功', {
|
||||||
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
|
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
const { query, currentPage, videoSource, statusFilter } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource, failedOnly);
|
await loadVideos(query, currentPage, videoSource, statusFilter);
|
||||||
} else {
|
} else {
|
||||||
toast.info('没有视频被更新');
|
toast.info('没有视频被更新');
|
||||||
}
|
}
|
||||||
@@ -241,7 +250,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parts.push(`仅失败视频:${state.failedOnly}`);
|
if (state.statusFilter) {
|
||||||
|
const statusLabels = {
|
||||||
|
failed: '仅失败',
|
||||||
|
succeeded: '仅成功',
|
||||||
|
waiting: '仅等待'
|
||||||
|
};
|
||||||
|
parts.push(`状态:${statusLabels[state.statusFilter]}`);
|
||||||
|
}
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,34 +313,40 @@
|
|||||||
goto(`/${ToQuery($appStateStore)}`);
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
}}
|
}}
|
||||||
></SearchBar>
|
></SearchBar>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-muted-foreground text-sm">筛选:</span>
|
<!-- 状态筛选 -->
|
||||||
<div
|
<div class="flex items-center gap-1">
|
||||||
class="bg-secondary text-secondary-foreground inline-flex items-center gap-1 rounded-lg px-2 py-1 text-xs font-medium"
|
<span class="text-muted-foreground text-xs">状态:</span>
|
||||||
>
|
<StatusFilter
|
||||||
<Checkbox
|
value={$appStateStore.statusFilter}
|
||||||
id="failed-only"
|
onSelect={(value) => {
|
||||||
checked={$appStateStore.failedOnly}
|
setStatusFilter(value);
|
||||||
onCheckedChange={(value) => {
|
resetCurrentPage();
|
||||||
setFailedOnly(value);
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
|
}}
|
||||||
|
onRemove={() => {
|
||||||
|
setStatusFilter(null);
|
||||||
resetCurrentPage();
|
resetCurrentPage();
|
||||||
goto(`/${ToQuery($appStateStore)}`);
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label for="failed-only" class="text-xs">仅失败视频</Label>
|
|
||||||
</div>
|
</div>
|
||||||
<DropdownFilter
|
<!-- 视频源筛选 -->
|
||||||
{filters}
|
<div class="flex items-center gap-1">
|
||||||
selectedLabel={$appStateStore.videoSource}
|
<span class="text-muted-foreground text-xs">来源:</span>
|
||||||
onSelect={(type, id) => {
|
<DropdownFilter
|
||||||
setAll('', 0, { type, id }, $appStateStore.failedOnly);
|
{filters}
|
||||||
goto(`/${ToQuery($appStateStore)}`);
|
selectedLabel={$appStateStore.videoSource}
|
||||||
}}
|
onSelect={(type, id) => {
|
||||||
onRemove={() => {
|
setAll('', 0, { type, id }, $appStateStore.statusFilter);
|
||||||
setAll('', 0, null, $appStateStore.failedOnly);
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
goto(`/${ToQuery($appStateStore)}`);
|
}}
|
||||||
}}
|
onRemove={() => {
|
||||||
/>
|
setAll('', 0, null, $appStateStore.statusFilter);
|
||||||
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user