diff --git a/apps/desktop/src/pages/Admin.tsx b/apps/desktop/src/pages/Admin.tsx index 9b8823e8..613fa11b 100644 --- a/apps/desktop/src/pages/Admin.tsx +++ b/apps/desktop/src/pages/Admin.tsx @@ -4,6 +4,27 @@ import { Routes, Route, Link, useLocation } from 'react-router-dom'; import { getApiClient, User, EbookSource, Book } from '@bookdock/api-client'; import { getCoverImageUrl } from '../utils/network'; import { Button, Card, CardHeader, CardTitle, CardContent, Input } from '@bookdock/ui'; + +function BookCover({ book, className = "" }: { book: Book; className?: string }) { + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); + return ( +
+ {coverSrc && !coverError ? ( + {book.title} setCoverError(true)} + /> + ) : ( +
+ {book.title.charAt(0)} +
+ )} +
+ ); +} import { BookOpen, FileText, @@ -786,20 +807,7 @@ function BooksManagement() {
-
- {book.coverUrl ? ( - {book.title} { (e.target as HTMLImageElement).style.display = 'none'; }} - /> - ) : ( -
- {book.title.charAt(0)} -
- )} -
+ {book.title}
diff --git a/apps/desktop/src/pages/AuthorDetail.tsx b/apps/desktop/src/pages/AuthorDetail.tsx index 9e9c9bc1..4f16f1e0 100644 --- a/apps/desktop/src/pages/AuthorDetail.tsx +++ b/apps/desktop/src/pages/AuthorDetail.tsx @@ -5,6 +5,29 @@ import { getApiClient, type Book, type Author } from "@bookdock/api-client"; import { getCoverImageUrl } from "../utils/network"; import { ArrowLeft, BookOpen } from "lucide-react"; +function BookCover({ book, className = "" }: { book: Book; className?: string }) { + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); + return ( +
+ {coverSrc && !coverError ? ( + {book.title} setCoverError(true)} + /> + ) : ( +
+ + {book.title.charAt(0)} + +
+ )} +
+ ); +} + function getBookGradient(title: string): string { const gradients = [ "from-blue-400 to-purple-500", @@ -146,23 +169,7 @@ export default function AuthorDetail() { className="w-36 cursor-pointer" onClick={() => handleBookSelect(book)} > -
- {book.coverUrl ? ( - {book.title} - ) : ( -
- - {book.title.charAt(0)} - -
- )} -
+

{book.title}

diff --git a/apps/desktop/src/pages/BookDetail.tsx b/apps/desktop/src/pages/BookDetail.tsx index 87d986d4..03dd2348 100644 --- a/apps/desktop/src/pages/BookDetail.tsx +++ b/apps/desktop/src/pages/BookDetail.tsx @@ -17,6 +17,29 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { getCoverImageUrl } from "../utils/network"; +function BookCover({ book, className = "" }: { book: Book; className?: string }) { + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); + return ( +
+ {coverSrc && !coverError ? ( + {book.title} setCoverError(true)} + /> + ) : ( +
+ + {book.title.charAt(0)} + +
+ )} +
+ ); +} + function getBookGradient(title: string): string { const gradients = [ "from-blue-400 to-purple-500", @@ -314,23 +337,7 @@ export default function BookDetail() {
{/* 封面 */}
-
- {book.coverUrl ? ( - {book.title} - ) : ( -
- - {book.title.charAt(0)} - -
- )} -
+
{/* 信息 */} diff --git a/apps/desktop/src/pages/CollectionDetail.tsx b/apps/desktop/src/pages/CollectionDetail.tsx index 4e01009e..1c5be9cc 100644 --- a/apps/desktop/src/pages/CollectionDetail.tsx +++ b/apps/desktop/src/pages/CollectionDetail.tsx @@ -5,6 +5,27 @@ import { getApiClient, Book, Collection } from "@bookdock/api-client"; import { getCoverImageUrl } from "../utils/network"; import { ArrowLeft, X, Trash2 } from "lucide-react"; +function BookCover({ book, className = "", onClick }: { book: Book; className?: string; onClick?: () => void }) { + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); + return ( +
+ {coverSrc && !coverError ? ( + {book.title} setCoverError(true)} + /> + ) : ( +
+ {book.title.charAt(0)} +
+ )} +
+ ); +} + function getBookGradient(title: string): string { const gradients = [ "from-blue-400 to-purple-500", @@ -107,24 +128,11 @@ export default function CollectionDetail() { key={book.id} className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg group" > -
navigate(`/book/${book.id}/detail`)} - > - {book.coverUrl ? ( - {book.title} - ) : ( -
- {book.title.charAt(0)} -
- )} -
+ />
navigate(`/book/${book.id}/detail`)} diff --git a/apps/desktop/src/pages/Library.tsx b/apps/desktop/src/pages/Library.tsx index f4c8889f..362775fc 100644 --- a/apps/desktop/src/pages/Library.tsx +++ b/apps/desktop/src/pages/Library.tsx @@ -81,6 +81,8 @@ const BookCard: React.FC<{ book: Book; onSelect: () => void }> = ({ : progress >= 100 ? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" : "bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"; + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); return (
void }> = ({ > {/* Cover */}
- {book.coverUrl ? ( + {coverSrc && !coverError ? ( {book.title} setCoverError(true)} /> ) : (
@@ -150,6 +153,68 @@ const BookCard: React.FC<{ book: Book; onSelect: () => void }> = ({ ); }; +const BookListItem: React.FC<{ book: Book; onSelect: () => void }> = ({ book, onSelect }) => { + const progress = book.readingProgress ?? 0; + const statusText = progress === 0 ? "未读" : progress >= 100 ? "已读完" : "在读"; + const statusColor = progress === 0 + ? "bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400" + : progress >= 100 + ? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" + : "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400"; + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); + + return ( +
+ {/* Cover */} +
+ {coverSrc && !coverError ? ( + {book.title} setCoverError(true)} + /> + ) : ( +
+ + {book.title.charAt(0)} + +
+ )} +
+ + {/* Title + Author */} +
+

+ {book.title} +

+

+ {book.author || "未知作者"} +

+
+ + {/* Format */} + + {(book.fileType || book.format || '').toUpperCase()} + + + {/* Status badge */} + + {statusText} + + + {/* File size */} + + {formatFileSize(book.fileSize)} + +
+ ); +}; + export default function Library() { const navigate = useNavigate(); const { books, isLoading, error, searchQuery, fetchBooks, setSearchQuery } = @@ -629,65 +694,9 @@ export default function Library() {
) : (
- {filteredBooks.map((book) => { - const progress = book.readingProgress ?? 0; - const statusText = progress === 0 ? "未读" : progress >= 100 ? "已读完" : "在读"; - const statusColor = progress === 0 - ? "bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400" - : progress >= 100 - ? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400" - : "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400"; - - return ( -
handleBookSelect(book)} - > - {/* Cover */} -
- {book.coverUrl ? ( - {book.title} - ) : ( -
- - {book.title.charAt(0)} - -
- )} -
- - {/* Title + Author */} -
-

- {book.title} -

-

- {book.author || "未知作者"} -

-
- - {/* Format */} - - {(book.fileType || book.format || '').toUpperCase()} - - - {/* Status badge */} - - {statusText} - - - {/* File size */} - - {formatFileSize(book.fileSize)} - -
- ); - })} + {filteredBooks.map((book) => ( + handleBookSelect(book)} /> + ))}
)} diff --git a/apps/desktop/src/pages/Profile.tsx b/apps/desktop/src/pages/Profile.tsx index 5e3a2d09..2cd6d078 100644 --- a/apps/desktop/src/pages/Profile.tsx +++ b/apps/desktop/src/pages/Profile.tsx @@ -176,27 +176,30 @@ export default function Profile() { { key: "downloads", label: "下载", icon: Download }, ]; - const renderBookCard = (book: Book) => ( -
navigate(`/book/${book.id}/detail`)} - > -
- {book.coverUrl ? ( - {book.title} - ) : ( -
- {book.title.charAt(0)} -
- )} -
-
-

{book.title}

-

{book.author || "未知作者"}

+ function BookCard({ book }: { book: Book }) { + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); + return ( +
navigate(`/book/${book.id}/detail`)} + > +
+ {coverSrc && !coverError ? ( + {book.title} setCoverError(true)} /> + ) : ( +
+ {book.title.charAt(0)} +
+ )} +
+
+

{book.title}

+

{book.author || "未知作者"}

+
-
- ); + ); + } const renderNotesContent = () => { const totalPages = Math.ceil(noteTotal / noteLimit); @@ -406,7 +409,7 @@ export default function Profile() { {inProgressBooks.length === 0 ? (

暂无在读书籍

) : ( - inProgressBooks.map(renderBookCard) + inProgressBooks.map((book) => ) )}
); @@ -416,7 +419,7 @@ export default function Profile() { {favorites.length === 0 ? (

暂无收藏

) : ( - favorites.map(renderBookCard) + favorites.map((book) => ) )}
); @@ -426,7 +429,7 @@ export default function Profile() { {downloadedBooks.length === 0 ? (

暂无下载

) : ( - downloadedBooks.map((b) => renderBookCard(b as unknown as Book)) + downloadedBooks.map((b) => ) )}
); diff --git a/apps/desktop/src/pages/Reader-TTS.tsx b/apps/desktop/src/pages/Reader-TTS.tsx index 0ba5af93..9c8084ad 100644 --- a/apps/desktop/src/pages/Reader-TTS.tsx +++ b/apps/desktop/src/pages/Reader-TTS.tsx @@ -48,6 +48,29 @@ import React, { import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { getCoverImageUrl } from "../utils/network"; +function BookCover({ book, className = "" }: { book: Book; className?: string }) { + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); + return ( +
+ {coverSrc && !coverError ? ( + {book.title} setCoverError(true)} + /> + ) : ( +
+ + {book.title.charAt(0)} + +
+ )} +
+ ); +} + const TTS_CONFIG_KEY = "bookdock-tts-config"; const TTS_LANG_KEY = "bookdock-tts-language"; @@ -709,21 +732,7 @@ export default function ReaderTTS() {
{/* Book info */}
-
- {getCoverImageUrl(book.coverUrl) ? ( - {book.title} - ) : ( -
- - {book.title.charAt(0)} - -
- )} -
+

{book.title}

diff --git a/apps/desktop/src/pages/Recommend.tsx b/apps/desktop/src/pages/Recommend.tsx index 8d87341c..f8029a64 100644 --- a/apps/desktop/src/pages/Recommend.tsx +++ b/apps/desktop/src/pages/Recommend.tsx @@ -39,6 +39,30 @@ function formatDate(dateStr?: string): string { }); } +function BookCover({ book, className = "", children }: { book: Book; className?: string; children?: React.ReactNode }) { + const [coverError, setCoverError] = useState(false); + const coverSrc = getCoverImageUrl(book.coverUrl); + return ( +
+ {coverSrc && !coverError ? ( + {book.title} setCoverError(true)} + /> + ) : ( +
+ + {book.title.charAt(0)} + +
+ )} + {children} +
+ ); +} + export default function Recommend() { const navigate = useNavigate(); const { books, fetchBooks, isLoading } = useLibraryStore(); @@ -119,29 +143,14 @@ export default function Recommend() { className="flex-shrink-0 w-36 cursor-pointer" onClick={() => handleBookSelect(book)} > -
- {book.coverUrl ? ( - {book.title} - ) : ( -
- - {book.title.charAt(0)} - -
- )} +
-
+

{book.title}

@@ -189,23 +198,7 @@ export default function Recommend() { className="cursor-pointer" onClick={() => handleBookSelect(book)} > -
- {book.coverUrl ? ( - {book.title} - ) : ( -
- - {book.title.charAt(0)} - -
- )} -
+

{book.title}

@@ -233,23 +226,7 @@ export default function Recommend() { className="cursor-pointer" onClick={() => handleBookSelect(book)} > -
- {book.coverUrl ? ( - {book.title} - ) : ( -
- - {book.title.charAt(0)} - -
- )} -
+

{book.title}