refactor: 重构书籍列表与详情页面,优化代码结构
1. 新增BookCover通用封面组件,统一处理封面展示与缺省样式 2. 调整类型定义:修复字段命名、补充可选类型适配空值场景 3. 替换首页旧的封面实现,复用新组件并移除冗余代码 4. 新增书籍详情页面,完善书籍信息展示与章节列表分页功能 5. 更新全局组件注册,补充新增组件与naive-ui组件类型声明 6. 调整首页工具栏按钮,替换阅读按钮为导入按钮
This commit is contained in:
parent
8177cf9ab6
commit
117fbe5ac4
|
|
@ -60,7 +60,7 @@ pub struct Books {
|
|||
pub create_time: chrono::NaiveDateTime,
|
||||
pub last_read_chapter_id: i64,
|
||||
pub last_read_position: i32,
|
||||
pub last_read_time: chrono::NaiveDateTime,
|
||||
pub last_read_time: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize, Clone)]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div class="book-cover">
|
||||
<img v-if="resolvedSrc" :src="resolvedSrc" :alt="title" />
|
||||
<div v-else class="cover-fallback" :style="{ background: coverGradient(title) }">
|
||||
<span class="cover-spine"></span>
|
||||
<div class="cover-meta">
|
||||
<span class="cover-title">{{ title }}</span>
|
||||
<span class="cover-author">{{ author || "佚名" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { convertFileSrc } from "@tauri-apps/api/core"
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
author?: string
|
||||
cover?: string
|
||||
}>()
|
||||
|
||||
const coverGradients = [
|
||||
"linear-gradient(135deg, #667eea, #764ba2)",
|
||||
"linear-gradient(135deg, #f093fb, #f5576c)",
|
||||
"linear-gradient(135deg, #4facfe, #00f2fe)",
|
||||
"linear-gradient(135deg, #43e97b, #38f9d7)",
|
||||
"linear-gradient(135deg, #fa709a, #fee140)",
|
||||
"linear-gradient(135deg, #30cfd0, #330867)",
|
||||
"linear-gradient(135deg, #ff9a9e, #fecfef)",
|
||||
"linear-gradient(135deg, #a18cd1, #fbc2eb)",
|
||||
]
|
||||
|
||||
const coverGradient = (title: string) => {
|
||||
let hash = 0
|
||||
for (let i = 0; i < title.length; i++) {
|
||||
hash = (hash * 31 + title.charCodeAt(i)) >>> 0
|
||||
}
|
||||
return coverGradients[hash % coverGradients.length]
|
||||
}
|
||||
|
||||
const resolvedSrc = computed(() => {
|
||||
const cover = props.cover
|
||||
if (!cover) return ""
|
||||
if (/^(https?:|data:|blob:|asset:)/.test(cover)) return cover
|
||||
return convertFileSrc(cover)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.book-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-fallback {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 12px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cover-spine {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.cover-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cover-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.cover-author {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -11,24 +11,31 @@ export {}
|
|||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BookCover: typeof import('./../components/BookCover.vue')['default']
|
||||
NAnchor: typeof import('naive-ui')['NAnchor']
|
||||
NAnchorLink: typeof import('naive-ui')['NAnchorLink']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDescriptions: typeof import('naive-ui')['NDescriptions']
|
||||
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface PageResult<T> {
|
|||
total: number
|
||||
list: T[]
|
||||
page: number
|
||||
size: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
interface Books {
|
||||
|
|
@ -17,13 +17,13 @@ interface Books {
|
|||
title: string
|
||||
author: string
|
||||
cover: string
|
||||
instruction: string
|
||||
introduction: string
|
||||
file_path: string
|
||||
total_chapters: number
|
||||
total_chars: number
|
||||
create_time: string
|
||||
last_read_time: string
|
||||
last_read_chapter: number
|
||||
last_read_time: string | null
|
||||
last_read_chapter_id: number
|
||||
last_read_position: number
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,291 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="book-detail">
|
||||
<section class="detail-header">
|
||||
<div class="header-toolbar">
|
||||
<n-button quaternary circle aria-label="返回" @click="goBack">
|
||||
<template #icon>
|
||||
<n-icon :component="ArrowLeftOutlined" />
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="book-summary">
|
||||
<div class="summary-cover">
|
||||
<BookCover :title="book?.title ?? ''" :author="book?.author" :cover="book?.cover" />
|
||||
</div>
|
||||
|
||||
<div class="summary-meta">
|
||||
<h2 class="summary-title">{{ book?.title || "加载中..." }}</h2>
|
||||
|
||||
<n-descriptions :column="2" label-placement="left" size="small">
|
||||
<n-descriptions-item label="作者">{{ book?.author || "佚名" }}</n-descriptions-item>
|
||||
<n-descriptions-item label="总章节">{{ book?.total_chapters ?? 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="总字数">{{ formatChars(book?.total_chars) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="导入时间">{{ formatTime(book?.create_time) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="上次阅读">{{ formatTime(book?.last_read_time) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="阅读进度">{{ readProgress }}</n-descriptions-item>
|
||||
<n-descriptions-item label="简介" :span="2">
|
||||
{{ book?.introduction || "暂无简介" }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-divider class="detail-divider" />
|
||||
|
||||
<section class="chapter-section">
|
||||
<div class="chapter-header">
|
||||
<span class="chapter-name">章节列表</span>
|
||||
<span class="chapter-count">共 {{ total }} 章</span>
|
||||
</div>
|
||||
|
||||
<n-scrollbar class="chapter-scrollbar">
|
||||
<n-spin :show="loadingChapters">
|
||||
<n-list hoverable class="chapter-list">
|
||||
<n-list-item v-for="ch in chapterList" :key="ch.id">
|
||||
<div class="chapter-item">
|
||||
<span class="chapter-index">{{ ch.number }}</span>
|
||||
<span class="chapter-title">{{ ch.title }}</span>
|
||||
<span class="chapter-chars">{{ ch.total_chars }} 字</span>
|
||||
</div>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<n-empty
|
||||
v-if="!loadingChapters && chapterList.length === 0"
|
||||
class="chapter-empty"
|
||||
description="暂无章节"
|
||||
/>
|
||||
</n-spin>
|
||||
</n-scrollbar>
|
||||
|
||||
<div class="pagination-wrap">
|
||||
<n-pagination
|
||||
v-model:page="page"
|
||||
:page-size="limit"
|
||||
:item-count="total"
|
||||
:page-slot="7"
|
||||
show-size-picker
|
||||
:page-sizes="[20, 50, 100]"
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { ArrowLeftOutlined } from "@vicons/antd"
|
||||
import dayjs from "dayjs"
|
||||
import BookApi from "@/api/book"
|
||||
import BookCover from "@/components/BookCover.vue"
|
||||
import type { Books, Chapters } from "@/types/global"
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
|
||||
const bookId = computed(() => Number(route.query.id) || 0)
|
||||
|
||||
const book = ref<Books | null>(null)
|
||||
const chapterList = ref<Chapters[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const limit = ref(50)
|
||||
const loadingChapters = ref(false)
|
||||
|
||||
const goBack = () => router.back()
|
||||
|
||||
const readProgress = computed(() => {
|
||||
const b = book.value
|
||||
if (!b || b.total_chapters <= 0) return "—"
|
||||
const percent = Math.round((b.last_read_chapter_id / b.total_chapters) * 100)
|
||||
return `第 ${b.last_read_chapter_id} 章 · ${percent}%`
|
||||
})
|
||||
|
||||
const formatChars = (chars?: number) => {
|
||||
if (!chars) return "0 字"
|
||||
if (chars >= 10000) return `${(chars / 10000).toFixed(1)} 万字`
|
||||
return `${chars} 字`
|
||||
}
|
||||
|
||||
const formatTime = (t?: string | null) => {
|
||||
if (!t) return "—"
|
||||
const d = dayjs(t)
|
||||
return d.isValid() ? d.format("YYYY-MM-DD HH:mm") : t
|
||||
}
|
||||
|
||||
const loadBook = async () => {
|
||||
if (!bookId.value) return
|
||||
const res = await BookApi.detail(bookId.value)
|
||||
if (res.code === 0) {
|
||||
book.value = res.data
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
const loadChapters = async () => {
|
||||
if (!bookId.value) return
|
||||
loadingChapters.value = true
|
||||
try {
|
||||
const res = await BookApi.chapters(bookId.value, page.value, limit.value)
|
||||
if (res.code === 0) {
|
||||
chapterList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
} finally {
|
||||
loadingChapters.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onPageChange = (p: number) => {
|
||||
page.value = p
|
||||
loadChapters()
|
||||
}
|
||||
|
||||
const onPageSizeChange = (size: number) => {
|
||||
limit.value = size
|
||||
page.value = 1
|
||||
loadChapters()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBook()
|
||||
loadChapters()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.book-detail {
|
||||
height: 90%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
</style>
|
||||
.detail-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.book-summary {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.summary-cover {
|
||||
width: 120px;
|
||||
aspect-ratio: 2 / 3;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.summary-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.detail-divider {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.chapter-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chapter-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.chapter-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.chapter-count {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.chapter-scrollbar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chapter-index {
|
||||
flex-shrink: 0;
|
||||
min-width: 40px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chapter-chars {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chapter-empty {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
<button class="setting-btn" title="设置" aria-label="设置" @click="goSettingView">
|
||||
<n-icon :size="20" :component="SettingOutlined" />
|
||||
</button>
|
||||
<button class="setting-btn" title="阅读" aria-label="阅读" @click="goReaderView">
|
||||
<n-icon :size="20" :component="SettingOutlined" />
|
||||
<button class="setting-btn" title="导入" aria-label="导入" @click="importBook">
|
||||
<n-icon :size="20" :component="CloudUploadOutlined" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
|
|
@ -35,23 +35,8 @@
|
|||
class="book-card"
|
||||
@click="goDetail(book)"
|
||||
>
|
||||
<div class="book-cover">
|
||||
<img
|
||||
v-if="resolveCover(book.cover)"
|
||||
:src="resolveCover(book.cover)"
|
||||
:alt="book.title"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="cover-fallback"
|
||||
:style="{ background: coverGradient(book.title) }"
|
||||
>
|
||||
<span class="cover-spine"></span>
|
||||
<div class="cover-meta">
|
||||
<span class="cover-title">{{ book.title }}</span>
|
||||
<span class="cover-author">{{ book.author || "佚名" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="book-cover-wrap">
|
||||
<BookCover :title="book.title" :author="book.author" :cover="book.cover" />
|
||||
</div>
|
||||
|
||||
<div class="book-info">
|
||||
|
|
@ -67,10 +52,10 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { open } from "@tauri-apps/plugin-dialog"
|
||||
import { convertFileSrc } from "@tauri-apps/api/core"
|
||||
import { BookSearch24Regular } from "@vicons/fluent"
|
||||
import { SettingOutlined } from "@vicons/antd"
|
||||
import { SettingOutlined, CloudUploadOutlined } from "@vicons/antd"
|
||||
import BookApi from "@/api/book"
|
||||
import BookCover from "@/components/BookCover.vue"
|
||||
import type { Books } from "@/types/global"
|
||||
|
||||
const router = useRouter()
|
||||
|
|
@ -79,31 +64,6 @@ const message = useMessage()
|
|||
const bookList = ref<Books[]>([])
|
||||
const searchValue = ref("")
|
||||
|
||||
const coverGradients = [
|
||||
"linear-gradient(135deg, #667eea, #764ba2)",
|
||||
"linear-gradient(135deg, #f093fb, #f5576c)",
|
||||
"linear-gradient(135deg, #4facfe, #00f2fe)",
|
||||
"linear-gradient(135deg, #43e97b, #38f9d7)",
|
||||
"linear-gradient(135deg, #fa709a, #fee140)",
|
||||
"linear-gradient(135deg, #30cfd0, #330867)",
|
||||
"linear-gradient(135deg, #ff9a9e, #fecfef)",
|
||||
"linear-gradient(135deg, #a18cd1, #fbc2eb)",
|
||||
]
|
||||
|
||||
const coverGradient = (title: string) => {
|
||||
let hash = 0
|
||||
for (let i = 0; i < title.length; i++) {
|
||||
hash = (hash * 31 + title.charCodeAt(i)) >>> 0
|
||||
}
|
||||
return coverGradients[hash % coverGradients.length]
|
||||
}
|
||||
|
||||
const resolveCover = (cover: string) => {
|
||||
if (!cover) return ""
|
||||
if (/^(https?:|data:|blob:|asset:)/.test(cover)) return cover
|
||||
return convertFileSrc(cover)
|
||||
}
|
||||
|
||||
const getBookList = async () => {
|
||||
const res = await BookApi.list(searchValue.value)
|
||||
if (res.code === 0) {
|
||||
|
|
@ -264,75 +224,19 @@ onMounted(() => {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
position: relative;
|
||||
.book-cover-wrap {
|
||||
aspect-ratio: 2 / 3;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.book-card:hover .book-cover {
|
||||
.book-card:hover .book-cover-wrap {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
|
||||
.cover-fallback {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 12px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cover-spine {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.cover-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cover-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.cover-author {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in New Issue