feat: 前端支持根据 ID 手动添加订阅 (#374)
This commit is contained in:
@@ -623,8 +623,8 @@
|
||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
bind:value={formData.nfo_time_type}
|
||||
>
|
||||
<option value="FavTime">收藏时间</option>
|
||||
<option value="PubTime">发布时间</option>
|
||||
<option value="favtime">收藏时间</option>
|
||||
<option value="pubtime">发布时间</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import EditIcon from '@lucide/svelte/icons/edit';
|
||||
import SaveIcon from '@lucide/svelte/icons/save';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
@@ -11,6 +14,7 @@
|
||||
import HeartIcon from '@lucide/svelte/icons/heart';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
import ClockIcon from '@lucide/svelte/icons/clock';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -22,6 +26,16 @@
|
||||
let loading = false;
|
||||
let activeTab = 'favorites';
|
||||
|
||||
// 添加对话框状态
|
||||
let showAddDialog = false;
|
||||
let addDialogType: 'favorites' | 'collections' | 'submissions' = 'favorites';
|
||||
let adding = false;
|
||||
|
||||
// 表单数据
|
||||
let favoriteForm = { fid: '', path: '' };
|
||||
let collectionForm = { sid: '', mid: '', collection_type: '2', path: '' }; // 默认为合集
|
||||
let submissionForm = { upper_id: '', path: '' };
|
||||
|
||||
type ExtendedVideoSource = VideoSourceDetail & {
|
||||
type: string;
|
||||
originalIndex: number;
|
||||
@@ -31,10 +45,10 @@
|
||||
};
|
||||
|
||||
const TAB_CONFIG = {
|
||||
favorites: { label: '收藏夹', icon: HeartIcon, color: 'bg-red-500' },
|
||||
collections: { label: '合集 / 列表', icon: FolderIcon, color: 'bg-blue-500' },
|
||||
submissions: { label: '用户投稿', icon: UserIcon, color: 'bg-green-500' },
|
||||
watch_later: { label: '稍后再看', icon: ClockIcon, color: 'bg-yellow-500' }
|
||||
favorites: { label: '收藏夹', icon: HeartIcon },
|
||||
collections: { label: '合集 / 列表', icon: FolderIcon },
|
||||
submissions: { label: '用户投稿', icon: UserIcon },
|
||||
watch_later: { label: '稍后再看', icon: ClockIcon }
|
||||
} as const;
|
||||
|
||||
// 数据加载
|
||||
@@ -123,6 +137,67 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 打开添加对话框
|
||||
function openAddDialog(type: 'favorites' | 'collections' | 'submissions') {
|
||||
addDialogType = type;
|
||||
// 重置表单
|
||||
favoriteForm = { fid: '', path: '' };
|
||||
collectionForm = { sid: '', mid: '', collection_type: '2', path: '' };
|
||||
submissionForm = { upper_id: '', path: '' };
|
||||
showAddDialog = true;
|
||||
}
|
||||
|
||||
// 处理添加
|
||||
async function handleAdd() {
|
||||
adding = true;
|
||||
try {
|
||||
switch (addDialogType) {
|
||||
case 'favorites':
|
||||
if (!favoriteForm.fid || !favoriteForm.path.trim()) {
|
||||
toast.error('请填写完整的收藏夹信息');
|
||||
return;
|
||||
}
|
||||
await api.insertFavorite({
|
||||
fid: parseInt(favoriteForm.fid),
|
||||
path: favoriteForm.path
|
||||
});
|
||||
break;
|
||||
case 'collections':
|
||||
if (!collectionForm.sid || !collectionForm.mid || !collectionForm.path.trim()) {
|
||||
toast.error('请填写完整的合集信息');
|
||||
return;
|
||||
}
|
||||
await api.insertCollection({
|
||||
sid: parseInt(collectionForm.sid),
|
||||
mid: parseInt(collectionForm.mid),
|
||||
collection_type: parseInt(collectionForm.collection_type),
|
||||
path: collectionForm.path
|
||||
});
|
||||
break;
|
||||
case 'submissions':
|
||||
if (!submissionForm.upper_id || !submissionForm.path.trim()) {
|
||||
toast.error('请填写完整的用户投稿信息');
|
||||
return;
|
||||
}
|
||||
await api.insertSubmission({
|
||||
upper_id: parseInt(submissionForm.upper_id),
|
||||
path: submissionForm.path
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
toast.success('添加成功');
|
||||
showAddDialog = false;
|
||||
loadVideoSources(); // 重新加载数据
|
||||
} catch (error) {
|
||||
toast.error('添加失败', {
|
||||
description: (error as ApiError).message
|
||||
});
|
||||
} finally {
|
||||
adding = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMount(() => {
|
||||
setBreadcrumb([
|
||||
@@ -142,36 +217,33 @@
|
||||
<title>视频源管理 - Bili Sync</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-6xl">
|
||||
<div class="space-y-6">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="text-muted-foreground">加载中...</div>
|
||||
</div>
|
||||
{:else if videoSourcesData}
|
||||
<Tabs.Root bind:value={activeTab} class="w-full">
|
||||
<Tabs.List class="grid h-12 w-full grid-cols-4 bg-transparent p-0">
|
||||
<Tabs.List class="grid w-full grid-cols-4">
|
||||
{#each Object.entries(TAB_CONFIG) as [key, config] (key)}
|
||||
{@const sources = getSourcesForTab(key)}
|
||||
<Tabs.Trigger
|
||||
value={key}
|
||||
class="data-[state=active]:bg-muted/50 data-[state=active]:text-foreground text-muted-foreground hover:bg-muted/30 hover:text-foreground mx-1 flex min-w-0 items-center justify-center gap-2 rounded-lg bg-transparent px-2 py-3 text-sm font-medium transition-all sm:px-4"
|
||||
>
|
||||
<div
|
||||
class="flex h-5 w-5 items-center justify-center rounded-full {config.color} flex-shrink-0"
|
||||
>
|
||||
<svelte:component this={config.icon} class="h-3 w-3 text-white" />
|
||||
</div>
|
||||
<span class="hidden truncate sm:inline">{config.label}</span>
|
||||
<span
|
||||
class="bg-background/50 flex-shrink-0 rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
>{sources.length}</span
|
||||
>
|
||||
<Tabs.Trigger value={key} class="relative">
|
||||
{config.label}({sources.length})
|
||||
</Tabs.Trigger>
|
||||
{/each}
|
||||
</Tabs.List>
|
||||
{#each Object.entries(TAB_CONFIG) as [key, config] (key)}
|
||||
{@const sources = getSourcesForTab(key)}
|
||||
<Tabs.Content value={key} class="mt-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium">{config.label}管理</h3>
|
||||
{#if key === 'favorites' || key === 'collections' || key === 'submissions'}
|
||||
<Button size="sm" onclick={() => openAddDialog(key)} class="flex items-center gap-2">
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
手动添加
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if sources.length > 0}
|
||||
<div class="overflow-x-auto">
|
||||
<Table.Root>
|
||||
@@ -259,15 +331,25 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center py-12">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-full {config.color} mb-4"
|
||||
>
|
||||
<svelte:component this={config.icon} class="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div class="text-muted-foreground mb-2">暂无{config.label}</div>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
请先添加{config.label}订阅
|
||||
<svelte:component this={config.icon} class="text-muted-foreground mb-4 h-12 w-12" />
|
||||
<div class="text-muted-foreground mb-2 text-lg font-medium">暂无{config.label}</div>
|
||||
<p class="text-muted-foreground mb-4 text-center text-sm">
|
||||
{#if key === 'favorites'}
|
||||
还没有添加任何收藏夹订阅
|
||||
{:else if key === 'collections'}
|
||||
还没有添加任何合集或列表订阅
|
||||
{:else if key === 'submissions'}
|
||||
还没有添加任何用户投稿订阅
|
||||
{:else}
|
||||
还没有添加稍后再看订阅
|
||||
{/if}
|
||||
</p>
|
||||
{#if key === 'favorites' || key === 'collections' || key === 'submissions'}
|
||||
<Button onclick={() => openAddDialog(key)} class="flex items-center gap-2">
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
手动添加
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
@@ -280,4 +362,134 @@
|
||||
<Button class="mt-4" onclick={loadVideoSources}>重新加载</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Root bind:open={showAddDialog}>
|
||||
<Dialog.Overlay class="data-[state=open]:animate-overlay-show fixed inset-0 bg-black/30" />
|
||||
<Dialog.Content
|
||||
class="data-[state=open]:animate-content-show bg-background fixed top-1/2 left-1/2 z-50 max-h-[85vh] w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 rounded-lg border p-6 shadow-md outline-none"
|
||||
>
|
||||
<Dialog.Title class="text-lg font-semibold">
|
||||
{#if addDialogType === 'favorites'}
|
||||
添加收藏夹
|
||||
{:else if addDialogType === 'collections'}
|
||||
添加合集
|
||||
{:else}
|
||||
添加用户投稿
|
||||
{/if}
|
||||
</Dialog.Title>
|
||||
<div class="mt-4">
|
||||
{#if addDialogType === 'favorites'}
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label for="fid" class="text-sm font-medium">收藏夹ID (fid)</Label>
|
||||
<Input
|
||||
id="fid"
|
||||
type="number"
|
||||
bind:value={favoriteForm.fid}
|
||||
placeholder="请输入收藏夹ID"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if addDialogType === 'collections'}
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label for="collection-type" class="text-sm font-medium">合集类型</Label>
|
||||
<select
|
||||
id="collection-type"
|
||||
bind:value={collectionForm.collection_type}
|
||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring mt-1 flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="1">列表 (Series)</option>
|
||||
<option value="2">合集 (Season)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label for="sid" class="text-sm font-medium">
|
||||
{collectionForm.collection_type === '1'
|
||||
? '列表ID (series_id)'
|
||||
: '合集ID (season_id)'}
|
||||
</Label>
|
||||
<Input
|
||||
id="sid"
|
||||
type="number"
|
||||
bind:value={collectionForm.sid}
|
||||
placeholder={collectionForm.collection_type === '1'
|
||||
? '请输入列表ID'
|
||||
: '请输入合集ID'}
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="mid" class="text-sm font-medium">用户ID (mid)</Label>
|
||||
<Input
|
||||
id="mid"
|
||||
type="number"
|
||||
bind:value={collectionForm.mid}
|
||||
placeholder="请输入用户ID"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted-foreground text-xs">可从合集/列表页面URL中获取相应ID</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label for="upper_id" class="text-sm font-medium">UP主ID (mid)</Label>
|
||||
<Input
|
||||
id="upper_id"
|
||||
type="number"
|
||||
bind:value={submissionForm.upper_id}
|
||||
placeholder="请输入UP主ID"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4">
|
||||
<Label for="path" class="text-sm font-medium">下载路径</Label>
|
||||
{#if addDialogType === 'favorites'}
|
||||
<Input
|
||||
id="path"
|
||||
type="text"
|
||||
bind:value={favoriteForm.path}
|
||||
placeholder="请输入下载路径,例如:/path/to/download"
|
||||
class="mt-1"
|
||||
/>
|
||||
{:else if addDialogType === 'collections'}
|
||||
<Input
|
||||
id="path"
|
||||
type="text"
|
||||
bind:value={collectionForm.path}
|
||||
placeholder="请输入下载路径,例如:/path/to/download"
|
||||
class="mt-1"
|
||||
/>
|
||||
{:else}
|
||||
<Input
|
||||
id="path"
|
||||
type="text"
|
||||
bind:value={submissionForm.path}
|
||||
placeholder="请输入下载路径,例如:/path/to/download"
|
||||
class="mt-1"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => (showAddDialog = false)}
|
||||
disabled={adding}
|
||||
class="px-4"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button onclick={handleAdd} disabled={adding} class="px-4">
|
||||
{adding ? '添加中...' : '添加'}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user