feat: 优化书籍详情页与封面组件,新增默认封面与布局
1. 重构BookCover组件,移除渐变 fallback 逻辑,新增默认封面图片 2. 调整书籍详情页布局,使用Grid组件重构章节列表,优化阅读体验 3. 为简介和章节区域添加滚动容器,优化长内容展示 4. 完善返回按钮样式与页面整体排版
This commit is contained in:
parent
c12cbaaabc
commit
a5dd0c446d
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
|
|
@ -1,19 +1,13 @@
|
|||
<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">
|
||||
<p class="cover-title">{{ title }}</p>
|
||||
<span class="cover-author">{{ author || "佚名" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<img :src="resolvedSrc" :alt="title" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { convertFileSrc } from "@tauri-apps/api/core"
|
||||
import defaultCover from "@/assets/cover.jpg"
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
|
|
@ -21,30 +15,13 @@ const props = defineProps<{
|
|||
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)
|
||||
if (cover) {
|
||||
if (/^(https?:|data:|blob:|asset:)/.test(cover)) return cover
|
||||
return convertFileSrc(cover)
|
||||
}
|
||||
return defaultCover
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -63,52 +40,4 @@ const resolvedSrc = computed(() => {
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ declare module 'vue' {
|
|||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
<template>
|
||||
<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>
|
||||
<n-button class="back-btn" quaternary circle aria-label="返回" @click="goBack">
|
||||
<template #icon>
|
||||
<n-icon :component="ArrowLeftOutlined" />
|
||||
</template>
|
||||
</n-button>
|
||||
|
||||
<n-scrollbar class="page-scrollbar">
|
||||
<section class="detail-header">
|
||||
<div class="book-summary">
|
||||
<div class="summary-cover">
|
||||
<BookCover :title="book?.title ?? ''" :author="book?.author" :cover="book?.cover" />
|
||||
|
|
@ -25,7 +24,9 @@
|
|||
<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">
|
||||
<div class="intro-content">{{ book?.introduction || "暂无简介" }}</div>
|
||||
<n-scrollbar class="intro-scrollbar">
|
||||
<div class="intro-content">{{ book?.introduction || "暂无简介" }}</div>
|
||||
</n-scrollbar>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</div>
|
||||
|
|
@ -42,15 +43,22 @@
|
|||
|
||||
<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">
|
||||
<n-grid
|
||||
v-if="chapterList.length > 0"
|
||||
class="chapter-grid"
|
||||
:cols="2"
|
||||
:x-gap="16"
|
||||
:y-gap="6"
|
||||
responsive="screen"
|
||||
>
|
||||
<n-gi 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-title" :title="ch.title">{{ ch.title }}</span>
|
||||
<span class="chapter-chars">{{ ch.total_chars }} 字</span>
|
||||
</div>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-empty
|
||||
v-if="!loadingChapters && chapterList.length === 0"
|
||||
class="chapter-empty"
|
||||
|
|
@ -72,6 +80,7 @@
|
|||
/>
|
||||
</div>
|
||||
</section>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -164,25 +173,47 @@ onMounted(() => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.book-detail {
|
||||
height: 90%;
|
||||
position: relative;
|
||||
height: 93%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 24px;
|
||||
z-index: 10;
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.page-scrollbar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.n-scrollbar-container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.n-scrollbar-content) {
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
@ -216,9 +247,19 @@ onMounted(() => {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.intro-scrollbar {
|
||||
height: 120px;
|
||||
|
||||
:deep(.n-scrollbar-container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.intro-content {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
|
|
@ -231,10 +272,9 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.chapter-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chapter-header {
|
||||
|
|
@ -257,8 +297,7 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.chapter-scrollbar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: 420px;
|
||||
|
||||
:deep(.n-scrollbar-container) {
|
||||
height: 100%;
|
||||
|
|
@ -272,13 +311,20 @@ onMounted(() => {
|
|||
.chapter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surface-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-index {
|
||||
flex-shrink: 0;
|
||||
min-width: 40px;
|
||||
min-width: 32px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue