fix: 正确处理“我追的合集 / 收藏夹”中的收藏夹条目,以及一些样式、文本调整 (#553)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-12-05 16:38:10 +08:00
committed by GitHub
parent f37d9af678
commit b5ef76b0ed
14 changed files with 277 additions and 242 deletions

View File

@@ -4,7 +4,7 @@
import BotIcon from '@lucide/svelte/icons/bot';
import ChartPieIcon from '@lucide/svelte/icons/chart-pie';
import HeartIcon from '@lucide/svelte/icons/heart';
import FolderIcon from '@lucide/svelte/icons/folder';
import FoldersIcon from '@lucide/svelte/icons/folders';
import UserIcon from '@lucide/svelte/icons/user';
import Settings2Icon from '@lucide/svelte/icons/settings-2';
import SquareTerminalIcon from '@lucide/svelte/icons/square-terminal';
@@ -62,12 +62,12 @@
href: '/me/favorites'
},
{
title: '我关注的合集',
icon: FolderIcon,
title: '我的合集 / 收藏夹',
icon: FoldersIcon,
href: '/me/collections'
},
{
title: '我关注的 up 主',
title: '我关注的 UP 主',
icon: UserIcon,
href: '/me/uppers'
}

View File

@@ -10,28 +10,20 @@
import CheckIcon from '@lucide/svelte/icons/check';
import PlusIcon from '@lucide/svelte/icons/plus';
import XIcon from '@lucide/svelte/icons/x';
import type {
FavoriteWithSubscriptionStatus,
CollectionWithSubscriptionStatus,
UpperWithSubscriptionStatus
} from '$lib/types';
import type { Followed } from '$lib/types';
export let item:
| FavoriteWithSubscriptionStatus
| CollectionWithSubscriptionStatus
| UpperWithSubscriptionStatus;
export let type: 'favorites' | 'collections' | 'submissions' = 'favorites';
export let item: Followed;
export let onSubscriptionSuccess: (() => void) | null = null;
let dialogOpen = false;
function getIcon() {
switch (type) {
case 'favorites':
switch (item.type) {
case 'favorite':
return HeartIcon;
case 'collections':
case 'collection':
return FolderIcon;
case 'submissions':
case 'upper':
return UserIcon;
default:
return VideoIcon;
@@ -39,12 +31,12 @@
}
function getTypeLabel() {
switch (type) {
case 'favorites':
switch (item.type) {
case 'favorite':
return '收藏夹';
case 'collections':
case 'collection':
return '合集';
case 'submissions':
case 'upper':
return 'UP 主';
default:
return '';
@@ -52,55 +44,52 @@
}
function getTitle(): string {
switch (type) {
case 'favorites':
return (item as FavoriteWithSubscriptionStatus).title;
case 'collections':
return (item as CollectionWithSubscriptionStatus).title;
case 'submissions':
return (item as UpperWithSubscriptionStatus).uname;
switch (item.type) {
case 'favorite':
case 'collection':
return item.title;
case 'upper':
return item.uname;
default:
return '';
}
}
function getSubtitle(): string {
switch (type) {
case 'favorites':
return `uid: ${(item as FavoriteWithSubscriptionStatus).mid}`;
case 'collections':
return `uid: ${(item as CollectionWithSubscriptionStatus).mid}`;
switch (item.type) {
case 'favorite':
case 'collection':
return `UID${item.mid}`;
default:
return '';
}
}
function getDescription(): string {
switch (type) {
case 'submissions':
return (item as UpperWithSubscriptionStatus).sign || '';
switch (item.type) {
case 'upper':
return item.sign || '';
default:
return '';
}
}
function isDisabled(): boolean {
switch (type) {
case 'collections':
return (item as CollectionWithSubscriptionStatus).invalid;
case 'submissions': {
return (item as UpperWithSubscriptionStatus).invalid;
}
switch (item.type) {
case 'collection':
case 'upper':
case 'favorite':
return item.invalid;
default:
return false;
}
}
function getDisabledReason(): string {
switch (type) {
case 'collections':
switch (item.type) {
case 'collection':
return '已失效';
case 'submissions':
case 'upper':
return '账号已注销';
default:
return '';
@@ -108,22 +97,19 @@
}
function getCount(): number | null {
switch (type) {
case 'favorites':
return (item as FavoriteWithSubscriptionStatus).media_count;
switch (item.type) {
case 'favorite':
case 'collection':
return item.media_count;
default:
return null;
}
}
function getCountLabel(): string {
return '个视频';
}
function getAvatarUrl(): string {
switch (type) {
case 'submissions':
return (item as UpperWithSubscriptionStatus).face;
switch (item.type) {
case 'upper':
return item.face;
default:
return '';
}
@@ -149,7 +135,6 @@
const subtitle = getSubtitle();
const description = getDescription();
const count = getCount();
const countLabel = getCountLabel();
const avatarUrl = getAvatarUrl();
const subscribed = item.subscribed;
const disabled = isDisabled();
@@ -161,7 +146,7 @@
? 'opacity-60'
: ''}"
>
<CardHeader class="flex-shrink-0 pb-4">
<CardHeader class="flex-shrink-0">
<div class="flex items-start gap-3">
<!-- 头像或图标 - 简化设计 -->
<div
@@ -169,7 +154,7 @@
? 'opacity-50'
: ''}"
>
{#if avatarUrl && type === 'submissions'}
{#if avatarUrl && item.type === 'upper'}
<img
src={avatarUrl}
alt={title}
@@ -197,7 +182,7 @@
{#if disabled}
<Badge variant="destructive" class="shrink-0 text-xs">不可用</Badge>
{:else}
<Badge variant={subscribed ? 'outline' : 'secondary'} class="shrink-0 text-xs">
<Badge variant="secondary" class="shrink-0 text-xs">
{subscribed ? '已订阅' : typeLabel}
</Badge>
{/if}
@@ -211,25 +196,26 @@
</div>
{/if}
<!-- 计数信息 -->
{#if count !== null && !disabled}
<div class="text-muted-foreground flex items-center gap-1 text-sm">
<VideoIcon class="h-3 w-3 shrink-0" />
<span class="truncate">视频数:{count}</span>
</div>
{/if}
<!-- 描述信息 -->
{#if description && !disabled}
<p class="text-muted-foreground line-clamp-1 text-sm" title={description}>
{description}
</p>
{/if}
<!-- 计数信息 -->
{#if count !== null && !disabled}
<div class="text-muted-foreground text-sm">
{count}
{countLabel}
</div>
{/if}
</div>
</div>
</CardHeader>
<!-- 底部按钮区域 -->
<CardContent class="flex min-w-0 flex-1 flex-col justify-end pt-0 pb-4">
<CardContent class="flex min-w-0 flex-1 flex-col justify-end">
<div class="flex justify-end">
{#if disabled}
<Button
@@ -262,4 +248,4 @@
</Card>
<!-- 订阅对话框 -->
<SubscriptionDialog bind:open={dialogOpen} {item} {type} onSuccess={handleSubscriptionSuccess} />
<SubscriptionDialog bind:open={dialogOpen} {item} onSuccess={handleSubscriptionSuccess} />

View File

@@ -13,9 +13,7 @@
} from '$lib/components/ui/sheet/index.js';
import api from '$lib/api';
import type {
FavoriteWithSubscriptionStatus,
CollectionWithSubscriptionStatus,
UpperWithSubscriptionStatus,
Followed,
InsertFavoriteRequest,
InsertCollectionRequest,
InsertSubmissionRequest,
@@ -24,38 +22,37 @@
interface Props {
open: boolean;
item:
| FavoriteWithSubscriptionStatus
| CollectionWithSubscriptionStatus
| UpperWithSubscriptionStatus
| null;
type: 'favorites' | 'collections' | 'submissions';
item: Followed | null;
onSuccess: (() => void) | null;
}
let {
open = $bindable(false),
item = null,
type = 'favorites',
onSuccess = null
}: Props = $props();
let { open = $bindable(false), item = null, onSuccess = null }: Props = $props();
let customPath = $state('');
let loading = $state(false);
// 根据类型和 item 生成默认路径
async function generateDefaultPath(): Promise<string> {
if (!itemTitle) return '';
return (await api.getDefaultPath(type, itemTitle)).data;
if (!item || !itemTitle) return '';
// 根据 item.type 映射到对应的 API 类型
const apiType =
item.type === 'favorite'
? 'favorites'
: item.type === 'collection'
? 'collections'
: 'submissions';
return (await api.getDefaultPath(apiType, itemTitle)).data;
}
function getTypeLabel(): string {
switch (type) {
case 'favorites':
if (!item) return '';
switch (item.type) {
case 'favorite':
return '收藏夹';
case 'collections':
case 'collection':
return '合集';
case 'submissions':
case 'upper':
return 'UP 主';
default:
return '';
@@ -65,13 +62,12 @@
function getItemTitle(): string {
if (!item) return '';
switch (type) {
case 'favorites':
return (item as FavoriteWithSubscriptionStatus).title;
case 'collections':
return (item as CollectionWithSubscriptionStatus).title;
case 'submissions':
return (item as UpperWithSubscriptionStatus).uname;
switch (item.type) {
case 'favorite':
case 'collection':
return item.title;
case 'upper':
return item.uname;
default:
return '';
}
@@ -84,30 +80,27 @@
try {
let response;
switch (type) {
case 'favorites': {
const favorite = item as FavoriteWithSubscriptionStatus;
switch (item.type) {
case 'favorite': {
const request: InsertFavoriteRequest = {
fid: favorite.fid,
fid: item.fid,
path: customPath.trim()
};
response = await api.insertFavorite(request);
break;
}
case 'collections': {
const collection = item as CollectionWithSubscriptionStatus;
case 'collection': {
const request: InsertCollectionRequest = {
sid: collection.sid,
mid: collection.mid,
sid: item.sid,
mid: item.mid,
path: customPath.trim()
};
response = await api.insertCollection(request);
break;
}
case 'submissions': {
const upper = item as UpperWithSubscriptionStatus;
case 'upper': {
const request: InsertSubmissionRequest = {
upper_id: upper.mid,
upper_id: item.mid,
path: customPath.trim()
};
response = await api.insertSubmission(request);
@@ -176,21 +169,16 @@
<span class="text-muted-foreground text-sm font-medium">{typeLabel}名称:</span>
<span class="text-sm">{itemTitle}</span>
</div>
{#if type === 'favorites'}
{@const favorite = item as FavoriteWithSubscriptionStatus}
{#if item!.type !== 'upper'}
<div class="flex items-center gap-2">
<span class="text-muted-foreground text-sm font-medium">视频数量:</span>
<span class="text-sm">{favorite.media_count} </span>
<span class="text-sm">{item!.media_count} </span>
</div>
{:else if item!.sign}
<div class="flex items-start gap-2">
<span class="text-muted-foreground text-sm font-medium">个人简介:</span>
<span class="text-muted-foreground text-sm">{item!.sign}</span>
</div>
{/if}
{#if type === 'submissions'}
{@const upper = item as UpperWithSubscriptionStatus}
{#if upper.sign}
<div class="flex items-start gap-2">
<span class="text-muted-foreground text-sm font-medium">个人简介:</span>
<span class="text-muted-foreground text-sm">{upper.sign}</span>
</div>
{/if}
{/if}
</div>
</div>

View File

@@ -110,45 +110,46 @@ export interface ResetRequest {
force: boolean;
}
// 收藏夹相关类型
export interface FavoriteWithSubscriptionStatus {
title: string;
media_count: number;
fid: number;
mid: number;
subscribed: boolean;
}
export type Followed =
| {
type: 'favorite';
title: string;
media_count: number;
fid: number;
mid: number;
invalid: boolean;
subscribed: boolean;
}
| {
type: 'collection';
title: string;
sid: number;
mid: number;
media_count: number;
invalid: boolean;
subscribed: boolean;
}
| {
type: 'upper';
mid: number;
uname: string;
face: string;
sign: string;
invalid: boolean;
subscribed: boolean;
};
export interface FavoritesResponse {
favorites: FavoriteWithSubscriptionStatus[];
}
// 合集相关类型
export interface CollectionWithSubscriptionStatus {
title: string;
sid: number;
mid: number;
invalid: boolean;
subscribed: boolean;
favorites: Followed[];
}
export interface CollectionsResponse {
collections: CollectionWithSubscriptionStatus[];
collections: Followed[];
total: number;
}
// UP 主相关类型
export interface UpperWithSubscriptionStatus {
mid: number;
uname: string;
face: string;
sign: string;
invalid: boolean;
subscribed: boolean;
}
export interface UppersResponse {
uppers: UpperWithSubscriptionStatus[];
uppers: Followed[];
total: number;
}

View File

@@ -1,5 +1,16 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import type { Followed } from './types';
export function getFollowedKey(followed: Followed): number {
if (followed.type == 'favorite') {
return followed.fid;
} else if (followed.type == 'collection') {
return followed.sid;
} else {
return followed.mid;
}
}
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));

View File

@@ -5,9 +5,10 @@
import Pagination from '$lib/components/pagination.svelte';
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import api from '$lib/api';
import type { CollectionWithSubscriptionStatus, ApiError } from '$lib/types';
import type { Followed, ApiError } from '$lib/types';
import { getFollowedKey } from '$lib/utils';
let collections: CollectionWithSubscriptionStatus[] = [];
let collections: Followed[] = [];
let totalCount = 0;
let currentPage = 0;
let loading = false;
@@ -21,8 +22,8 @@
collections = response.data.collections;
totalCount = response.data.total;
} catch (error) {
console.error('加载合集失败:', error);
toast.error('加载合集失败', {
console.error('加载合集 / 收藏夹失败:', error);
toast.error('加载合集 / 收藏夹失败', {
description: (error as ApiError).message
});
} finally {
@@ -43,7 +44,7 @@
onMount(async () => {
setBreadcrumb([
{
label: '我关注的合集'
label: '我的合集 / 收藏夹'
}
]);
await loadCollections();
@@ -53,14 +54,19 @@
</script>
<svelte:head>
<title>关注的合集 - Bili Sync</title>
<title>我追的合集 / 收藏夹 - Bili Sync</title>
</svelte:head>
<div>
<div class="mb-6 flex items-center justify-between">
<div class="text-sm font-medium">
<div class="flex items-center gap-6">
{#if !loading}
{totalCount} 个合集
<div class=" text-sm font-medium">
{totalCount} 个合集 / 收藏夹
</div>
<div class=" text-sm font-medium">
当前第 {currentPage + 1} / {totalPages}
</div>
{/if}
</div>
</div>
@@ -73,13 +79,9 @@
<div
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; width: 100%; max-width: none; justify-items: start;"
>
{#each collections as collection (collection.sid)}
{#each collections as collection (getFollowedKey(collection))}
<div style="max-width: 450px; width: 100%;">
<SubscriptionCard
item={collection}
type="collections"
onSubscriptionSuccess={handleSubscriptionSuccess}
/>
<SubscriptionCard item={collection} onSubscriptionSuccess={handleSubscriptionSuccess} />
</div>
{/each}
</div>
@@ -91,8 +93,10 @@
{: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">请先在 B 站关注一些合集,或检查账号配置</p>
<p class="text-muted-foreground">暂无合集 / 收藏夹数据</p>
<p class="text-muted-foreground text-sm">
请先在 B 站关注一些合集 / 收藏夹,或检查账号配置
</p>
</div>
</div>
{/if}

View File

@@ -6,9 +6,10 @@
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import api from '$lib/api';
import type { FavoriteWithSubscriptionStatus, ApiError } from '$lib/types';
import type { Followed, ApiError } from '$lib/types';
import { getFollowedKey } from '$lib/utils';
let favorites: FavoriteWithSubscriptionStatus[] = [];
let favorites: Followed[] = [];
let loading = false;
async function loadFavorites() {
@@ -39,7 +40,7 @@
</script>
<svelte:head>
<title>我的收藏夹 - Bili Sync</title>
<title>创建的收藏夹 - Bili Sync</title>
</svelte:head>
<div>
@@ -59,13 +60,9 @@
<div
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; width: 100%; max-width: none; justify-items: start;"
>
{#each favorites as favorite (favorite.fid)}
{#each favorites as favorite (getFollowedKey(favorite))}
<div style="max-width: 450px; width: 100%;">
<SubscriptionCard
item={favorite}
type="favorites"
onSubscriptionSuccess={handleSubscriptionSuccess}
/>
<SubscriptionCard item={favorite} onSubscriptionSuccess={handleSubscriptionSuccess} />
</div>
{/each}
</div>

View File

@@ -5,9 +5,9 @@
import Pagination from '$lib/components/pagination.svelte';
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import api from '$lib/api';
import type { UpperWithSubscriptionStatus, ApiError } from '$lib/types';
import type { Followed, ApiError } from '$lib/types';
let uppers: UpperWithSubscriptionStatus[] = [];
let uppers: Followed[] = [];
let totalCount = 0;
let currentPage = 0;
let loading = false;
@@ -49,7 +49,7 @@
</script>
<svelte:head>
<title>关注的UP主 - Bili Sync</title>
<title>关注的 UP 主 - Bili Sync</title>
</svelte:head>
<div>
@@ -76,11 +76,7 @@
>
{#each uppers as upper (upper.mid)}
<div style="max-width: 450px; width: 100%;">
<SubscriptionCard
item={upper}
type="submissions"
onSubscriptionSuccess={handleSubscriptionSuccess}
/>
<SubscriptionCard item={upper} onSubscriptionSuccess={handleSubscriptionSuccess} />
</div>
{/each}
</div>