Files
bili-sync-ai/web/src/routes/+page.svelte

261 lines
7.3 KiB
Svelte

<script lang="ts">
import VideoCard from '$lib/components/video-card.svelte';
import FilterBadge from '$lib/components/filter-badge.svelte';
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 RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
import api from '$lib/api';
import type {
VideosResponse,
VideoSourcesResponse,
ApiError,
ResetAllVideosResponse
} from '$lib/types';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { setVideoSources, videoSourceStore } from '$lib/stores/video-source';
import { VIDEO_SOURCES } from '$lib/consts';
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import {
appStateStore,
clearVideoSourceFilter,
setQuery,
setVideoSourceFilter,
ToQuery
} from '$lib/stores/filter';
import { toast } from 'svelte-sonner';
let videosData: VideosResponse | null = null;
let loading = false;
let currentPage = 0;
const pageSize = 20;
let currentFilter: { type: string; id: string } | null = null;
let lastSearch: string | null = null;
// 重置所有视频相关状态
let resetAllDialogOpen = false;
let resettingAll = false;
// 从URL参数获取筛选条件
function getFilterFromURL(searchParams: URLSearchParams) {
for (const source of Object.values(VIDEO_SOURCES)) {
const value = searchParams.get(source.type);
if (value) {
return { type: source.type, id: value };
}
}
return null;
}
// 获取筛选项名称
function getFilterName(type: string, id: string): string {
const videoSources = $videoSourceStore;
if (!videoSources || !type || !id) return '';
const sources = videoSources[type as keyof VideoSourcesResponse];
const source = sources?.find((s) => s.id.toString() === id);
return source?.name || '';
}
// 获取筛选项标题
function getFilterTitle(type: string): string {
const sourceConfig = Object.values(VIDEO_SOURCES).find((s) => s.type === type);
return sourceConfig?.title || '';
}
async function loadVideos(
query?: string,
pageNum: number = 0,
filter?: { type: string; id: string } | null
) {
loading = true;
try {
const params: Record<string, string | number> = {
page: pageNum,
page_size: pageSize
};
if (query) {
params.query = query;
}
// 添加筛选参数
if (filter) {
params[filter.type] = parseInt(filter.id);
}
const result = await api.getVideos(params);
videosData = result.data;
currentPage = pageNum;
} catch (error) {
console.error('加载视频失败:', error);
toast.error('加载视频失败', {
description: (error as ApiError).message
});
} finally {
loading = false;
}
}
async function handlePageChange(pageNum: number) {
const query = ToQuery($appStateStore);
if (query) {
goto(`/${query}&page=${pageNum}`);
} else {
goto(`/?page=${pageNum}`);
}
}
async function handleSearchParamsChange() {
const query = $page.url.searchParams.get('query');
currentFilter = getFilterFromURL($page.url.searchParams);
setQuery(query || '');
if (currentFilter) {
setVideoSourceFilter(currentFilter.type, currentFilter.id);
} else {
clearVideoSourceFilter();
}
loadVideos(query || '', parseInt($page.url.searchParams.get('page') || '0'), currentFilter);
}
function handleFilterRemove() {
clearVideoSourceFilter();
goto(`/${ToQuery($appStateStore)}`);
}
async function handleResetAllVideos() {
resettingAll = true;
try {
const result = await api.resetAllVideos();
const data = result.data;
if (data.resetted) {
toast.success('重置成功', {
description: `已重置 ${data.resetted_videos_count} 个视频和 ${data.resetted_pages_count} 个分页`
});
// 重新加载当前页面的视频数据
const query = $page.url.searchParams.get('query');
loadVideos(query || '', currentPage, currentFilter);
} else {
toast.info('没有需要重置的视频');
}
} catch (error) {
console.error('重置失败:', error);
toast.error('重置失败', {
description: (error as ApiError).message
});
} finally {
resettingAll = false;
resetAllDialogOpen = false;
}
}
$: if ($page.url.search !== lastSearch) {
lastSearch = $page.url.search;
handleSearchParamsChange();
}
onMount(async () => {
setBreadcrumb([
{
label: '主页',
isActive: true
}
]);
});
$: totalPages = videosData ? Math.ceil(videosData.total_count / pageSize) : 0;
$: filterTitle = currentFilter ? getFilterTitle(currentFilter.type) : '';
$: filterName = currentFilter ? getFilterName(currentFilter.type, currentFilter.id) : '';
</script>
<svelte:head>
<title>主页 - Bili Sync</title>
</svelte:head>
<FilterBadge {filterTitle} {filterName} onRemove={handleFilterRemove} />
<!-- 统计信息 -->
{#if videosData}
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="text-muted-foreground text-sm">
{videosData.total_count} 个视频
</div>
<div class="text-muted-foreground text-sm">
{totalPages}
</div>
</div>
<div class="flex items-center gap-2">
<Button
size="sm"
variant="outline"
class="text-xs"
onclick={() => (resetAllDialogOpen = true)}
disabled={resettingAll || loading}
>
<RotateCcwIcon class="mr-1.5 h-3 w-3 {resettingAll ? 'animate-spin' : ''}" />
重置所有视频
</Button>
</div>
</div>
{/if}
<!-- 视频卡片网格 -->
{#if loading}
<div class="flex items-center justify-center py-12">
<div class="text-muted-foreground">加载中...</div>
</div>
{:else if videosData?.videos.length}
<div
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; width: 100%; max-width: none; justify-items: start;"
>
{#each videosData.videos as video (video.id)}
<div style="max-width: 400px; width: 100%;">
<VideoCard {video} />
</div>
{/each}
</div>
<!-- 翻页组件 -->
<Pagination {currentPage} {totalPages} onPageChange={handlePageChange} />
{:else}
<div class="flex items-center justify-center py-12">
<div class="space-y-2 text-center">
<p class="text-muted-foreground">暂无视频数据</p>
<p class="text-muted-foreground text-sm">尝试搜索或检查视频来源配置</p>
</div>
</div>
{/if}
<!-- 重置所有视频确认对话框 -->
<AlertDialog.Root bind:open={resetAllDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>重置所有视频</AlertDialog.Title>
<AlertDialog.Description>
此操作将重置所有视频和分页的失败状态为未下载状态,使它们在下次下载任务中重新尝试。
<br />
<strong class="text-destructive">此操作不可撤销,确定要继续吗?</strong>
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel disabled={resettingAll}>取消</AlertDialog.Cancel>
<AlertDialog.Action
onclick={handleResetAllVideos}
disabled={resettingAll}
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if resettingAll}
<RotateCcwIcon class="mr-2 h-4 w-4 animate-spin" />
重置中...
{:else}
确认重置
{/if}
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>