feat: 前端添加下载状态卡片 (#385)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-07-10 15:13:25 +08:00
committed by GitHub
parent 655b4389b7
commit cc25749445
18 changed files with 341 additions and 141 deletions

View File

@@ -3,9 +3,6 @@
"workspaces": {
"": {
"name": "my-app",
"dependencies": {
"@tanstack/svelte-query": "^5.81.5",
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
@@ -39,7 +36,7 @@
"tw-animate-css": "^1.3.2",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "7.0.2",
"vite": "7.0.3",
},
},
},
@@ -254,10 +251,6 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.8", "", { "dependencies": { "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A=="],
"@tanstack/query-core": ["@tanstack/query-core@5.81.5", "", {}, "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q=="],
"@tanstack/svelte-query": ["@tanstack/svelte-query@5.81.5", "", { "dependencies": { "@tanstack/query-core": "5.81.5" }, "peerDependencies": { "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" } }, "sha512-P2TaL+dGHWwQ83CyX8I9icb/1lYUSFwqQvGHI8jzFbacOEtVtQQXB0N12fotvn/BDn7Eh+tyCNiiMN56tFuFJw=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
@@ -692,7 +685,7 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vite": ["vite@7.0.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw=="],
"vite": ["vite@7.0.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ=="],
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],

View File

@@ -34,7 +34,7 @@
"tw-animate-css": "^1.3.2",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "7.0.2"
"vite": "7.0.3"
},
"private": true,
"scripts": {
@@ -47,8 +47,5 @@
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"type": "module",
"dependencies": {
"@tanstack/svelte-query": "^5.81.5"
}
"type": "module"
}

View File

@@ -228,7 +228,7 @@ class ApiClient {
onError?: (error: Event) => void
): EventSource {
const token = localStorage.getItem('authToken');
const url = `/api/logs${token ? `?token=${encodeURIComponent(token)}` : ''}`;
const url = `/api/sse/logs${token ? `?token=${encodeURIComponent(token)}` : ''}`;
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
onMessage(event.data);
@@ -244,7 +244,7 @@ class ApiClient {
onError?: (error: Event) => void
): EventSource {
const token = localStorage.getItem('authToken');
const url = `/api/dashboard/sysinfo${token ? `?token=${encodeURIComponent(token)}` : ''}`;
const url = `/api/sse/sysinfo${token ? `?token=${encodeURIComponent(token)}` : ''}`;
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
try {
@@ -259,6 +259,22 @@ class ApiClient {
}
return eventSource;
}
createTasksStream(
onMessage: (data: string) => void,
onError?: (error: Event) => void
): EventSource {
const token = localStorage.getItem('authToken');
const url = `/api/sse/tasks${token ? `?token=${encodeURIComponent(token)}` : ''}`;
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
onMessage(event.data);
};
if (onError) {
eventSource.onerror = onError;
}
return eventSource;
}
}
// 创建默认的 API 客户端实例
@@ -293,6 +309,8 @@ const api = {
) => apiClient.createSysInfoStream(onMessage, onError),
createLogStream: (onMessage: (data: string) => void, onError?: (error: Event) => void) =>
apiClient.createLogStream(onMessage, onError),
createTasksStream: (onMessage: (data: string) => void, onError?: (error: Event) => void) =>
apiClient.createTasksStream(onMessage, onError),
setAuthToken: (token: string) => apiClient.setAuthToken(token),
clearAuthToken: () => apiClient.clearAuthToken()
};

View File

@@ -0,0 +1,14 @@
import { writable } from 'svelte/store';
export interface TaskStatus {
is_running: boolean;
last_run: Date | null;
last_finish: Date | null;
next_run: Date | null;
}
export const taskStatusStore = writable<TaskStatus>(undefined);
export function setTaskStatus(status: TaskStatus) {
taskStatusStore.set(status);
}

View File

@@ -6,6 +6,31 @@
import { breadcrumbStore } from '$lib/stores/breadcrumb';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { Toaster } from '$lib/components/ui/sonner/index.js';
import { onMount } from 'svelte';
import { setTaskStatus, type TaskStatus } from '$lib/stores/tasks';
import api from '$lib/api';
import { toast } from 'svelte-sonner';
let tasksStream: EventSource | undefined;
onMount(() => {
tasksStream = api.createTasksStream(
(data: string) => {
const status: TaskStatus = JSON.parse(data);
setTaskStatus(status);
},
(error: Event) => {
console.error('任务状态流错误:', error);
toast.error('任务状态流错误,请检查网络连接或稍后重试');
}
);
return () => {
if (tasksStream) {
tasksStream.close();
tasksStream = undefined;
}
};
});
</script>
<Toaster />

View File

@@ -9,6 +9,7 @@
import { BarChart, AreaChart } from 'layerchart';
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import { toast } from 'svelte-sonner';
import CloudDownloadIcon from '@lucide/svelte/icons/cloud-download';
import api from '$lib/api';
import type { DashBoardResponse, SysInfoResponse, ApiError } from '$lib/types';
import DatabaseIcon from '@lucide/svelte/icons/database';
@@ -20,6 +21,10 @@
import HardDriveIcon from '@lucide/svelte/icons/hard-drive';
import CpuIcon from '@lucide/svelte/icons/cpu';
import MemoryStickIcon from '@lucide/svelte/icons/memory-stick';
import PlayIcon from '@lucide/svelte/icons/play';
import CheckCircleIcon from '@lucide/svelte/icons/check-circle';
import CalendarIcon from '@lucide/svelte/icons/calendar';
import { taskStatusStore } from '$lib/stores/tasks';
let dashboardData: DashBoardResponse | null = null;
let sysInfo: SysInfoResponse | null = null;
@@ -61,7 +66,7 @@
},
(error) => {
console.error('系统信息流错误:', error);
toast.error('系统信息流出现错误,请稍后重试');
toast.error('系统信息流出现错误,请检查网络连接或稍后重试');
}
);
}
@@ -142,13 +147,6 @@
<svelte:head>
<title>仪表盘 - Bili Sync</title>
<style>
body {
/* 避免最右侧 tooltip 溢出导致的无限抖动 */
overflow-x: hidden;
}
</style>
</svelte:head>
<div class="space-y-6">
@@ -228,8 +226,8 @@
</Card>
</div>
<div class="grid grid-cols-1 gap-4">
<Card>
<div class="grid gap-4 md:grid-cols-3">
<Card class="max-w-full overflow-hidden md:col-span-2">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">最近入库</CardTitle>
<VideoIcon class="text-muted-foreground h-4 w-4" />
@@ -273,18 +271,90 @@
</BarChart>
</Chart.Container>
{:else}
<div class="text-muted-foreground flex h-[300px] items-center justify-center text-sm">
<div class="text-muted-foreground flex h-[200px] items-center justify-center text-sm">
暂无视频统计数据
</div>
{/if}</CardContent
>
</Card>
<Card class="max-w-full md:col-span-1">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">下载任务状态</CardTitle>
<CloudDownloadIcon class="text-muted-foreground h-4 w-4" />
</CardHeader>
<CardContent>
{#if $taskStatusStore}
<div class="space-y-4">
<div class="grid grid-cols-1 gap-6">
<div class="mb-4 space-y-2">
<div class="flex items-center justify-between text-sm">
<span>当前任务状态</span>
<Badge variant={$taskStatusStore.is_running ? 'default' : 'outline'}>
{$taskStatusStore.is_running ? '运行中' : '未运行'}
</Badge>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<PlayIcon class="text-muted-foreground h-4 w-4" />
<span class="text-sm">开始运行</span>
</div>
<span class="text-muted-foreground text-sm">
{$taskStatusStore.last_run
? new Date($taskStatusStore.last_run).toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
})
: '-'}
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<CheckCircleIcon class="text-muted-foreground h-4 w-4" />
<span class="text-sm">运行结束</span>
</div>
<span class="text-muted-foreground text-sm">
{$taskStatusStore.last_finish
? new Date($taskStatusStore.last_finish).toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
})
: '-'}
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<CalendarIcon class="text-muted-foreground h-4 w-4" />
<span class="text-sm">下次运行</span>
</div>
<span class="text-muted-foreground text-sm">
{$taskStatusStore.next_run
? new Date($taskStatusStore.next_run).toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
})
: '-'}
</span>
</div>
</div>
</div>
{:else}
<div class="text-muted-foreground text-sm">加载中...</div>
{/if}
</CardContent>
</Card>
</div>
<!-- 第三行:系统监控 -->
<div class="grid gap-4 md:grid-cols-2">
<!-- 内存使用情况 -->
<Card>
<Card class="overflow-hidden">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">内存使用情况</CardTitle>
<MemoryStickIcon class="text-muted-foreground h-4 w-4" />
@@ -332,12 +402,12 @@
{#snippet tooltip()}
<MyChartTooltip
labelFormatter={(v: Date) => {
return new Intl.DateTimeFormat('en-US', {
return v.toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
}).format(v);
});
}}
valueFormatter={(v: number) => formatBytes(v)}
indicator="line"
@@ -353,12 +423,12 @@
</CardContent>
</Card>
<Card>
<Card class="overflow-hidden">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">CPU 使用情况</CardTitle>
<CpuIcon class="text-muted-foreground h-4 w-4" />
</CardHeader>
<CardContent>
<CardContent class="overflow-hidden">
{#if sysInfo}
<div class="mb-4 space-y-2">
<div class="flex items-center justify-between text-sm">
@@ -399,12 +469,12 @@
{#snippet tooltip()}
<MyChartTooltip
labelFormatter={(v: Date) => {
return new Intl.DateTimeFormat('en-US', {
return v.toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
}).format(v);
});
}}
valueFormatter={(v: number) => formatCpu(v)}
indicator="line"

View File

@@ -34,7 +34,7 @@
},
(error: Event) => {
console.error('日志流错误:', error);
toast.error('日志流出现错误,请稍后重试');
toast.error('日志流出现错误,请检查网络连接或稍后重试');
}
);
}