@@ -81,6 +81,8 @@ const BookCard: React.FC<{ book: Book; onSelect: () => void }> = ({
8181 : progress >= 100
8282 ? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"
8383 : "bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" ;
84+ const [ coverError , setCoverError ] = useState ( false ) ;
85+ const coverSrc = getCoverImageUrl ( book . coverUrl ) ;
8486
8587 return (
8688 < div
@@ -89,11 +91,12 @@ const BookCard: React.FC<{ book: Book; onSelect: () => void }> = ({
8991 >
9092 { /* Cover */ }
9193 < div className = "aspect-[2/3] rounded-xl overflow-hidden relative shadow-sm group-hover:shadow-md transition-shadow" >
92- { book . coverUrl ? (
94+ { coverSrc && ! coverError ? (
9395 < img
94- src = { getCoverImageUrl ( book . coverUrl ) }
96+ src = { coverSrc }
9597 alt = { book . title }
9698 className = "w-full h-full object-cover"
99+ onError = { ( ) => setCoverError ( true ) }
97100 />
98101 ) : (
99102 < div className = { `w-full h-full flex items-center justify-center bg-gradient-to-br ${ getBookGradient ( book . title ) } ` } >
@@ -150,6 +153,68 @@ const BookCard: React.FC<{ book: Book; onSelect: () => void }> = ({
150153 ) ;
151154} ;
152155
156+ const BookListItem : React . FC < { book : Book ; onSelect : ( ) => void } > = ( { book, onSelect } ) => {
157+ const progress = book . readingProgress ?? 0 ;
158+ const statusText = progress === 0 ? "未读" : progress >= 100 ? "已读完" : "在读" ;
159+ const statusColor = progress === 0
160+ ? "bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
161+ : progress >= 100
162+ ? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"
163+ : "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400" ;
164+ const [ coverError , setCoverError ] = useState ( false ) ;
165+ const coverSrc = getCoverImageUrl ( book . coverUrl ) ;
166+
167+ return (
168+ < div
169+ className = "flex items-center gap-4 py-4 cursor-pointer group hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors -mx-4 px-4 rounded-lg"
170+ onClick = { onSelect }
171+ >
172+ { /* Cover */ }
173+ < div className = "w-12 h-16 rounded-lg overflow-hidden flex-shrink-0 shadow-sm" >
174+ { coverSrc && ! coverError ? (
175+ < img
176+ src = { coverSrc }
177+ alt = { book . title }
178+ className = "w-full h-full object-cover"
179+ onError = { ( ) => setCoverError ( true ) }
180+ />
181+ ) : (
182+ < div className = { `w-full h-full flex items-center justify-center bg-gradient-to-br ${ getBookGradient ( book . title ) } ` } >
183+ < span className = "text-sm text-white font-bold" >
184+ { book . title . charAt ( 0 ) }
185+ </ span >
186+ </ div >
187+ ) }
188+ </ div >
189+
190+ { /* Title + Author */ }
191+ < div className = "flex-1 min-w-0" >
192+ < h3 className = "font-medium text-gray-900 dark:text-white truncate" >
193+ { book . title }
194+ </ h3 >
195+ < p className = "text-sm text-gray-400 dark:text-gray-500 truncate" >
196+ { book . author || "未知作者" }
197+ </ p >
198+ </ div >
199+
200+ { /* Format */ }
201+ < span className = "text-sm text-orange-500 dark:text-orange-400 flex-shrink-0" >
202+ { ( book . fileType || book . format || '' ) . toUpperCase ( ) }
203+ </ span >
204+
205+ { /* Status badge */ }
206+ < span className = { `px-2.5 py-0.5 rounded-full text-xs font-medium flex-shrink-0 ${ statusColor } ` } >
207+ { statusText }
208+ </ span >
209+
210+ { /* File size */ }
211+ < span className = "text-sm text-gray-400 dark:text-gray-500 flex-shrink-0 w-20 text-right" >
212+ { formatFileSize ( book . fileSize ) }
213+ </ span >
214+ </ div >
215+ ) ;
216+ } ;
217+
153218export default function Library ( ) {
154219 const navigate = useNavigate ( ) ;
155220 const { books, isLoading, error, searchQuery, fetchBooks, setSearchQuery } =
@@ -629,65 +694,9 @@ export default function Library() {
629694 </ div >
630695 ) : (
631696 < div className = "divide-y divide-gray-100 dark:divide-gray-800" >
632- { filteredBooks . map ( ( book ) => {
633- const progress = book . readingProgress ?? 0 ;
634- const statusText = progress === 0 ? "未读" : progress >= 100 ? "已读完" : "在读" ;
635- const statusColor = progress === 0
636- ? "bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
637- : progress >= 100
638- ? "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"
639- : "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400" ;
640-
641- return (
642- < div
643- key = { book . id }
644- className = "flex items-center gap-4 py-4 cursor-pointer group hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors -mx-4 px-4 rounded-lg"
645- onClick = { ( ) => handleBookSelect ( book ) }
646- >
647- { /* Cover */ }
648- < div className = "w-12 h-16 rounded-lg overflow-hidden flex-shrink-0 shadow-sm" >
649- { book . coverUrl ? (
650- < img
651- src = { getCoverImageUrl ( book . coverUrl ) }
652- alt = { book . title }
653- className = "w-full h-full object-cover"
654- />
655- ) : (
656- < div className = { `w-full h-full flex items-center justify-center bg-gradient-to-br ${ getBookGradient ( book . title ) } ` } >
657- < span className = "text-sm text-white font-bold" >
658- { book . title . charAt ( 0 ) }
659- </ span >
660- </ div >
661- ) }
662- </ div >
663-
664- { /* Title + Author */ }
665- < div className = "flex-1 min-w-0" >
666- < h3 className = "font-medium text-gray-900 dark:text-white truncate" >
667- { book . title }
668- </ h3 >
669- < p className = "text-sm text-gray-400 dark:text-gray-500 truncate" >
670- { book . author || "未知作者" }
671- </ p >
672- </ div >
673-
674- { /* Format */ }
675- < span className = "text-sm text-orange-500 dark:text-orange-400 flex-shrink-0" >
676- { ( book . fileType || book . format || '' ) . toUpperCase ( ) }
677- </ span >
678-
679- { /* Status badge */ }
680- < span className = { `px-2.5 py-0.5 rounded-full text-xs font-medium flex-shrink-0 ${ statusColor } ` } >
681- { statusText }
682- </ span >
683-
684- { /* File size */ }
685- < span className = "text-sm text-gray-400 dark:text-gray-500 flex-shrink-0 w-20 text-right" >
686- { formatFileSize ( book . fileSize ) }
687- </ span >
688- </ div >
689- ) ;
690- } ) }
697+ { filteredBooks . map ( ( book ) => (
698+ < BookListItem key = { book . id } book = { book } onSelect = { ( ) => handleBookSelect ( book ) } />
699+ ) ) }
691700 </ div >
692701 ) }
693702 </ section >
0 commit comments