feat: videos页面中新增仅失败过滤选项 (#605)
* videos页面中新增 仅失败过滤选项 * 仅失败筛选时才计算失败标记,避免额外的分页查询 * 去除[仅失败]多余的逻辑判定 * refactor: 后端调整:1)为 status -> sql 加入一个中间层方便拓展;2)将 Option<bool> 改为带有 default 的 bool;3)failed 统一改成 failed_only * refactor: 前端调整:1)前端也统一改成 failed_only;2)修复很多地方在 loadVideo 前没有读取 failedOnly;3)略微调整前端样式 * format --------- Co-authored-by: kaixin1995 <admin@haokaikai.cn> Co-authored-by: amtoaer <amtoaer@gmail.com>
This commit is contained in:
@@ -7,19 +7,21 @@ export interface AppState {
|
||||
type: string;
|
||||
id: string;
|
||||
} | null;
|
||||
failedOnly: boolean;
|
||||
}
|
||||
|
||||
export const appStateStore = writable<AppState>({
|
||||
query: '',
|
||||
currentPage: 0,
|
||||
videoSource: null
|
||||
videoSource: null,
|
||||
failedOnly: false
|
||||
});
|
||||
|
||||
export const ToQuery = (state: AppState): string => {
|
||||
const { query, videoSource } = state;
|
||||
const { query, videoSource, currentPage, failedOnly } = state;
|
||||
const params = new URLSearchParams();
|
||||
if (state.currentPage > 0) {
|
||||
params.set('page', String(state.currentPage));
|
||||
if (currentPage > 0) {
|
||||
params.set('page', String(currentPage));
|
||||
}
|
||||
if (query.trim()) {
|
||||
params.set('query', query);
|
||||
@@ -27,6 +29,9 @@ export const ToQuery = (state: AppState): string => {
|
||||
if (videoSource && videoSource.type && videoSource.id) {
|
||||
params.set(videoSource.type, videoSource.id);
|
||||
}
|
||||
if (failedOnly) {
|
||||
params.set('failed_only', 'true');
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return queryString ? `videos?${queryString}` : 'videos';
|
||||
};
|
||||
@@ -40,6 +45,7 @@ export const ToFilterParams = (
|
||||
favorite?: number;
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
failed_only?: boolean;
|
||||
} => {
|
||||
const params: {
|
||||
query?: string;
|
||||
@@ -47,6 +53,7 @@ export const ToFilterParams = (
|
||||
favorite?: number;
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
failed_only?: boolean;
|
||||
} = {};
|
||||
|
||||
if (state.query.trim()) {
|
||||
@@ -57,13 +64,15 @@ export const ToFilterParams = (
|
||||
const { type, id } = state.videoSource;
|
||||
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
|
||||
}
|
||||
|
||||
if (state.failedOnly) {
|
||||
params.failed_only = true;
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
// 检查是否有活动的筛选条件
|
||||
export const hasActiveFilters = (state: AppState): boolean => {
|
||||
return !!(state.query.trim() || state.videoSource);
|
||||
return !!(state.query.trim() || state.videoSource || state.failedOnly);
|
||||
};
|
||||
|
||||
export const setQuery = (query: string) => {
|
||||
@@ -94,6 +103,13 @@ export const setCurrentPage = (page: number) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const setFailedOnly = (failedOnly: boolean) => {
|
||||
appStateStore.update((state) => ({
|
||||
...state,
|
||||
failedOnly
|
||||
}));
|
||||
};
|
||||
|
||||
export const resetCurrentPage = () => {
|
||||
appStateStore.update((state) => ({
|
||||
...state,
|
||||
@@ -104,12 +120,14 @@ export const resetCurrentPage = () => {
|
||||
export const setAll = (
|
||||
query: string,
|
||||
currentPage: number,
|
||||
videoSource: { type: string; id: string } | null
|
||||
videoSource: { type: string; id: string } | null,
|
||||
failedOnly: boolean
|
||||
) => {
|
||||
appStateStore.set({
|
||||
query,
|
||||
currentPage,
|
||||
videoSource
|
||||
videoSource,
|
||||
failedOnly
|
||||
});
|
||||
};
|
||||
|
||||
@@ -117,6 +135,7 @@ export const clearAll = () => {
|
||||
appStateStore.set({
|
||||
query: '',
|
||||
currentPage: 0,
|
||||
videoSource: null
|
||||
videoSource: null,
|
||||
failedOnly: false
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface VideosRequest {
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
query?: string;
|
||||
failed_only?: boolean;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
@@ -106,6 +107,8 @@ export interface UpdateFilteredVideoStatusRequest {
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
query?: string;
|
||||
// 仅更新下载失败
|
||||
failed_only?: boolean;
|
||||
video_updates?: StatusUpdate[];
|
||||
page_updates?: StatusUpdate[];
|
||||
}
|
||||
@@ -120,6 +123,8 @@ export interface ResetFilteredVideoStatusRequest {
|
||||
submission?: number;
|
||||
watch_later?: number;
|
||||
query?: string;
|
||||
// 仅重置下载失败
|
||||
failed_only?: boolean;
|
||||
force: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
setAll,
|
||||
setCurrentPage,
|
||||
setQuery,
|
||||
setFailedOnly,
|
||||
ToQuery,
|
||||
ToFilterParams,
|
||||
hasActiveFilters
|
||||
@@ -61,9 +62,13 @@
|
||||
videoSource = { type: source.type, id: value };
|
||||
}
|
||||
}
|
||||
// 支持从 URL 里还原失败筛选
|
||||
const failedParam = searchParams.get('failed_only');
|
||||
const failedOnly = failedParam === 'true' || failedParam === '1';
|
||||
return {
|
||||
query: searchParams.get('query') || '',
|
||||
videoSource,
|
||||
failedOnly,
|
||||
pageNum: parseInt(searchParams.get('page') || '0')
|
||||
};
|
||||
}
|
||||
@@ -71,11 +76,12 @@
|
||||
async function loadVideos(
|
||||
query: string,
|
||||
pageNum: number = 0,
|
||||
filter?: { type: string; id: string } | null
|
||||
filter?: { type: string; id: string } | null,
|
||||
failedOnly: boolean = false
|
||||
) {
|
||||
loading = true;
|
||||
try {
|
||||
const params: Record<string, string | number> = {
|
||||
const params: Record<string, string | number | boolean> = {
|
||||
page: pageNum,
|
||||
page_size: pageSize
|
||||
};
|
||||
@@ -85,6 +91,7 @@
|
||||
if (filter) {
|
||||
params[filter.type] = parseInt(filter.id);
|
||||
}
|
||||
params.failed_only = failedOnly;
|
||||
const result = await api.getVideos(params);
|
||||
videosData = result.data;
|
||||
} catch (error) {
|
||||
@@ -103,9 +110,9 @@
|
||||
}
|
||||
|
||||
async function handleSearchParamsChange(searchParams: URLSearchParams) {
|
||||
const { query, videoSource, pageNum } = getApiParams(searchParams);
|
||||
setAll(query, pageNum, videoSource);
|
||||
loadVideos(query, pageNum, videoSource);
|
||||
const { query, videoSource, pageNum, failedOnly } = getApiParams(searchParams);
|
||||
setAll(query, pageNum, videoSource, failedOnly);
|
||||
loadVideos(query, pageNum, videoSource, failedOnly);
|
||||
}
|
||||
|
||||
async function handleResetVideo(id: number, forceReset: boolean) {
|
||||
@@ -116,8 +123,8 @@
|
||||
toast.success('重置成功', {
|
||||
description: `视频「${data.video.name}」已重置`
|
||||
});
|
||||
const { query, currentPage, videoSource } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource);
|
||||
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource, failedOnly);
|
||||
} else {
|
||||
toast.info('重置无效', {
|
||||
description: `视频「${data.video.name}」没有失败的状态,无需重置`
|
||||
@@ -144,8 +151,8 @@
|
||||
description: `视频「${data.video.name}」已清空重置`
|
||||
});
|
||||
}
|
||||
const { query, currentPage, videoSource } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource);
|
||||
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource, failedOnly);
|
||||
} catch (error) {
|
||||
console.error('清空重置失败:', error);
|
||||
toast.error('清空重置失败', {
|
||||
@@ -168,8 +175,8 @@
|
||||
toast.success('重置成功', {
|
||||
description: `已重置 ${data.resetted_videos_count} 个视频和 ${data.resetted_pages_count} 个分页`
|
||||
});
|
||||
const { query, currentPage, videoSource } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource);
|
||||
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource, failedOnly);
|
||||
} else {
|
||||
toast.info('没有需要重置的视频');
|
||||
}
|
||||
@@ -199,8 +206,8 @@
|
||||
toast.success('更新成功', {
|
||||
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
|
||||
});
|
||||
const { query, currentPage, videoSource } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource);
|
||||
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
||||
await loadVideos(query, currentPage, videoSource, failedOnly);
|
||||
} else {
|
||||
toast.info('没有视频被更新');
|
||||
}
|
||||
@@ -234,6 +241,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.push(`仅失败视频:${state.failedOnly}`);
|
||||
return parts;
|
||||
}
|
||||
|
||||
@@ -291,15 +299,29 @@
|
||||
></SearchBar>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground text-sm">筛选:</span>
|
||||
<div
|
||||
class="bg-secondary text-secondary-foreground inline-flex items-center gap-1 rounded-lg px-2 py-1 text-xs font-medium"
|
||||
>
|
||||
<Checkbox
|
||||
id="failed-only"
|
||||
checked={$appStateStore.failedOnly}
|
||||
onCheckedChange={(value) => {
|
||||
setFailedOnly(value);
|
||||
resetCurrentPage();
|
||||
goto(`/${ToQuery($appStateStore)}`);
|
||||
}}
|
||||
/>
|
||||
<Label for="failed-only" class="text-xs">仅失败视频</Label>
|
||||
</div>
|
||||
<DropdownFilter
|
||||
{filters}
|
||||
selectedLabel={$appStateStore.videoSource}
|
||||
onSelect={(type, id) => {
|
||||
setAll('', 0, { type, id });
|
||||
setAll('', 0, { type, id }, $appStateStore.failedOnly);
|
||||
goto(`/${ToQuery($appStateStore)}`);
|
||||
}}
|
||||
onRemove={() => {
|
||||
setAll('', 0, null);
|
||||
setAll('', 0, null, $appStateStore.failedOnly);
|
||||
goto(`/${ToQuery($appStateStore)}`);
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user