feat: 添加全局加载状态与章节相关功能,优化界面样式
1. 新增全局加载状态管理store与hook,添加全局加载组件 2. 新增获取章节详情、相邻章节的后端接口与前端api调用 3. 优化书籍列表、删除、导入等操作的加载状态提示 4. 完成阅读页面开发,支持分页阅读与章节切换 5. 调整全局悬浮背景色与窗口阴影配置 6. 修复书籍列表搜索回车事件,优化章节列表点击跳转
This commit is contained in:
parent
a5dd0c446d
commit
f7cec7ce26
|
|
@ -180,3 +180,48 @@ pub async fn chapter_page(
|
|||
page_size: limit,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取书籍章节详情
|
||||
* @param id 章节ID
|
||||
* @returns 章节详情
|
||||
*/
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn chapter_detail(
|
||||
pool: State<'_, SqlitePool>,
|
||||
id: i64,
|
||||
) -> Result<ApiResponse<Chapters>, String> {
|
||||
let chapter = sqlx::query_as::<_, Chapters>("SELECT * FROM chapters WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(ApiResponse::success(chapter))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相邻章节
|
||||
* @param book_id 书籍ID
|
||||
* @param number 当前章节号
|
||||
* @param offset 偏移量(-1 上一章,1 下一章)
|
||||
* @returns 相邻章节,不存在则为 null
|
||||
*/
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn chapter_nav(
|
||||
pool: State<'_, SqlitePool>,
|
||||
book_id: i64,
|
||||
number: i32,
|
||||
offset: i32,
|
||||
) -> Result<ApiResponse<Option<Chapters>>, String> {
|
||||
let chapter = sqlx::query_as::<_, Chapters>(
|
||||
"SELECT * FROM chapters WHERE book_id = ? AND number = ?",
|
||||
)
|
||||
.bind(book_id)
|
||||
.bind(number + offset)
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(ApiResponse::success(chapter))
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
"decorations": false,
|
||||
"alwaysOnTop": false,
|
||||
"skipTaskbar": false,
|
||||
"shadow": true,
|
||||
"shadow": false,
|
||||
"visible": true,
|
||||
"center": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<n-message-provider>
|
||||
<n-notification-provider>
|
||||
<n-dialog-provider>
|
||||
<GlobalLoading />
|
||||
<RouterView />
|
||||
</n-dialog-provider>
|
||||
</n-notification-provider>
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { zhCN, dateZhCN } from "naive-ui"
|
||||
import GlobalLoading from "@/components/GlobalLoading.vue"
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { invoke } from "@tauri-apps/api/core"
|
|||
import { ApiResponse, PageResult, Books, Chapters, BookSaveParams } from "@/types/global"
|
||||
|
||||
const BookApi = {
|
||||
/**
|
||||
* 导入书籍
|
||||
* @param path 书籍文件路径
|
||||
* @returns 导入结果
|
||||
*/
|
||||
import: async (path: string) => {
|
||||
return await invoke<ApiResponse<any>>("book_import", { path })
|
||||
},
|
||||
/**
|
||||
* 导入书籍
|
||||
* @param path 书籍文件路径
|
||||
* @returns 导入结果
|
||||
*/
|
||||
import: async (path: string) => {
|
||||
return await invoke<ApiResponse<any>>("book_import", { path })
|
||||
},
|
||||
/**
|
||||
* 获取书籍列表
|
||||
* @param title 书籍标题模糊查询
|
||||
|
|
@ -52,6 +52,24 @@ const BookApi = {
|
|||
chapters: async (bookId: number, page: number, limit: number) => {
|
||||
return await invoke<ApiResponse<PageResult<Chapters>>>("chapter_page", { bookId, page, limit })
|
||||
},
|
||||
/**
|
||||
* 获取章节详情
|
||||
* @param id 章节ID
|
||||
* @returns 章节详情
|
||||
*/
|
||||
chapterDetail: async (id: number) => {
|
||||
return await invoke<ApiResponse<Chapters>>("chapter_detail", { id })
|
||||
},
|
||||
/**
|
||||
* 获取相邻章节
|
||||
* @param bookId 书籍ID
|
||||
* @param number 当前章节号
|
||||
* @param offset 偏移量(-1 上一章,1 下一章)
|
||||
* @returns 相邻章节,不存在则为 null
|
||||
*/
|
||||
chapterNav: async (bookId: number, number: number, offset: number) => {
|
||||
return await invoke<ApiResponse<Chapters | null>>("chapter_nav", { bookId, number, offset })
|
||||
},
|
||||
}
|
||||
|
||||
export default BookApi
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/* 窗口与页面背景 */
|
||||
--color-window-bg: #f6f7f9;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-hover: #fafafa;
|
||||
--color-surface-hover: #e6e6e6;
|
||||
|
||||
/* 文字 */
|
||||
--color-text-primary: #1d1d1f;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="loading" class="global-loading">
|
||||
<div class="global-loading-box">
|
||||
<n-spin size="large" />
|
||||
<span class="global-loading-text">{{ text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { useLoadingStore } from "@/stores/loading"
|
||||
|
||||
const store = useLoadingStore()
|
||||
const loading = computed(() => store.loading)
|
||||
const text = computed(() => store.text)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.global-loading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.global-loading-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 28px 36px;
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
|
||||
.global-loading-text {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { computed } from "vue"
|
||||
import { useLoadingStore } from "@/stores/loading"
|
||||
|
||||
/**
|
||||
* 全局加载状态 hook
|
||||
* @example
|
||||
* const { withLoading } = useLoading()
|
||||
* await withLoading(() => BookApi.import(path), "导入中...")
|
||||
*/
|
||||
export function useLoading() {
|
||||
const store = useLoadingStore()
|
||||
|
||||
const start = (text?: string) => store.start(text)
|
||||
const stop = () => store.stop()
|
||||
const reset = () => store.reset()
|
||||
|
||||
const withLoading = async <T>(fn: () => Promise<T>, text?: string): Promise<T> => {
|
||||
store.start(text)
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
store.stop()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading: computed(() => store.loading),
|
||||
text: computed(() => store.text),
|
||||
start,
|
||||
stop,
|
||||
reset,
|
||||
withLoading,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { defineStore } from "pinia"
|
||||
|
||||
/**
|
||||
* 全局加载状态 store
|
||||
* 支持嵌套调用(count 计数),loading 为 true 表示当前有任务在进行
|
||||
*/
|
||||
export const useLoadingStore = defineStore("loading", {
|
||||
state: () => ({
|
||||
count: 0,
|
||||
text: "加载中...",
|
||||
}),
|
||||
getters: {
|
||||
loading: (state) => state.count > 0,
|
||||
},
|
||||
actions: {
|
||||
start(text?: string) {
|
||||
if (text) this.text = text
|
||||
this.count++
|
||||
},
|
||||
stop() {
|
||||
this.count = Math.max(0, this.count - 1)
|
||||
},
|
||||
reset() {
|
||||
this.count = 0
|
||||
this.text = "加载中..."
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -12,6 +12,7 @@ export {}
|
|||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BookCover: typeof import('./../components/BookCover.vue')['default']
|
||||
GlobalLoading: typeof import('./../components/GlobalLoading.vue')['default']
|
||||
NAnchor: typeof import('naive-ui')['NAnchor']
|
||||
NAnchorLink: typeof import('naive-ui')['NAnchorLink']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
responsive="screen"
|
||||
>
|
||||
<n-gi v-for="ch in chapterList" :key="ch.id">
|
||||
<div class="chapter-item">
|
||||
<div class="chapter-item" @click="readChapter(ch)">
|
||||
<span class="chapter-index">{{ ch.number }}</span>
|
||||
<span class="chapter-title" :title="ch.title">{{ ch.title }}</span>
|
||||
<span class="chapter-chars">{{ ch.total_chars }} 字</span>
|
||||
|
|
@ -85,16 +85,17 @@
|
|||
</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 { useLoading } from "@/hooks/useLoading"
|
||||
import type { Books, Chapters } from "@/types/global"
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
const { withLoading } = useLoading()
|
||||
|
||||
const bookId = computed(() => Number(route.query.id) || 0)
|
||||
|
||||
|
|
@ -107,6 +108,10 @@ const loadingChapters = ref(false)
|
|||
|
||||
const goBack = () => router.back()
|
||||
|
||||
const readChapter = (ch: Chapters) => {
|
||||
router.push({ name: "Reader", query: { id: ch.id } })
|
||||
}
|
||||
|
||||
const readProgress = computed(() => {
|
||||
const b = book.value
|
||||
if (!b || b.total_chapters <= 0) return "—"
|
||||
|
|
@ -128,7 +133,7 @@ const formatTime = (t?: string | null) => {
|
|||
|
||||
const loadBook = async () => {
|
||||
if (!bookId.value) return
|
||||
const res = await BookApi.detail(bookId.value)
|
||||
const res = await withLoading(() => BookApi.detail(bookId.value), "加载中...")
|
||||
if (res.code === 0) {
|
||||
book.value = res.data
|
||||
} else {
|
||||
|
|
@ -140,7 +145,10 @@ const loadChapters = async () => {
|
|||
if (!bookId.value) return
|
||||
loadingChapters.value = true
|
||||
try {
|
||||
const res = await BookApi.chapters(bookId.value, page.value, limit.value)
|
||||
const res = await withLoading(
|
||||
() => BookApi.chapters(bookId.value, page.value, limit.value),
|
||||
"加载章节中..."
|
||||
)
|
||||
if (res.code === 0) {
|
||||
chapterList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
|
|
@ -154,6 +162,10 @@ const loadChapters = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const goReader = (chapterId: number) => {
|
||||
router.push({ name: "Reader", query: { id: chapterId } })
|
||||
}
|
||||
|
||||
const onPageChange = (p: number) => {
|
||||
page.value = p
|
||||
loadChapters()
|
||||
|
|
@ -315,6 +327,7 @@ onMounted(() => {
|
|||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<header class="home-header">
|
||||
<n-input v-model:value="searchValue" class="search-input" round clearable placeholder="搜索书籍">
|
||||
<n-input v-model:value="searchValue" class="search-input" round clearable placeholder="搜索书籍" @click.enter="getBookList">
|
||||
<template #prefix>
|
||||
<n-icon :component="BookSearch24Regular" />
|
||||
</template>
|
||||
|
|
@ -130,10 +130,12 @@ import {
|
|||
import { NIcon } from "naive-ui"
|
||||
import BookApi from "@/api/book"
|
||||
import BookCover from "@/components/BookCover.vue"
|
||||
import { useLoading } from "@/hooks/useLoading"
|
||||
import type { Books, BookSaveParams } from "@/types/global"
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const { withLoading } = useLoading()
|
||||
|
||||
const bookList = ref<Books[]>([])
|
||||
const searchValue = ref("")
|
||||
|
|
@ -171,7 +173,7 @@ const rules = {
|
|||
}
|
||||
|
||||
const getBookList = async () => {
|
||||
const res = await BookApi.list(searchValue.value)
|
||||
const res = await withLoading(() => BookApi.list(searchValue.value), "加载中...")
|
||||
if (res.code === 0) {
|
||||
bookList.value = res.data
|
||||
} else {
|
||||
|
|
@ -241,10 +243,11 @@ const confirmDelete = (book: Books | null) => {
|
|||
}
|
||||
|
||||
const doDelete = async () => {
|
||||
if (!deleteBook.value || deleting.value) return
|
||||
const book = deleteBook.value
|
||||
if (!book || deleting.value) return
|
||||
deleting.value = true
|
||||
try {
|
||||
const res = await BookApi.del(deleteBook.value.id)
|
||||
const res = await withLoading(() => BookApi.del(book.id), "删除中...")
|
||||
if (res.code === 0) {
|
||||
message.success("删除成功")
|
||||
showDeleteModal.value = false
|
||||
|
|
@ -265,7 +268,7 @@ const importBook = async () => {
|
|||
filters: [{ name: "TXT", extensions: ["txt", "epub"] }]
|
||||
})
|
||||
if (filePath) {
|
||||
const res = await BookApi.import(filePath)
|
||||
const res = await withLoading(() => BookApi.import(filePath), "导入中...")
|
||||
if (res.code === 0) {
|
||||
message.success("导入成功")
|
||||
getBookList()
|
||||
|
|
|
|||
|
|
@ -1,20 +1,261 @@
|
|||
<template>
|
||||
<div class="glass-panel">
|
||||
<div class="glass-panel" data-tauri-drag-region>
|
||||
<div class="reading-wrap">
|
||||
<div class="chapter-name">{{ chapter?.title || "加载中..." }}</div>
|
||||
|
||||
<div ref="pageEl" class="page-body" @click="onPageClick">
|
||||
<div class="page-text">{{ currentText }}</div>
|
||||
</div>
|
||||
|
||||
<div class="page-nav">
|
||||
<span class="page-info">{{ pageInfo }}</span>
|
||||
<n-space justify="space-between" class="nav-btns">
|
||||
<n-space>
|
||||
<n-button size="small" text @click="switchChapter(-1)">上一章</n-button>
|
||||
<n-button size="small" text :disabled="currentPage <= 0" @click="prevPage">上一页</n-button>
|
||||
</n-space>
|
||||
<n-button size="small" text @click="goBack">返回</n-button>
|
||||
<n-space>
|
||||
<n-button size="small" text :disabled="currentPage >= pages.length - 1" @click="nextPage">
|
||||
下一页
|
||||
</n-button>
|
||||
<n-button size="small" text @click="switchChapter(1)">下一章</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick } from "vue"
|
||||
import { ArrowLeftOutlined } from "@vicons/antd"
|
||||
import BookApi from "@/api/book"
|
||||
import { useLoading } from "@/hooks/useLoading"
|
||||
import type { Chapters } from "@/types/global"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const { withLoading } = useLoading()
|
||||
|
||||
const chapterId = computed(() => Number(route.query.id) || 0)
|
||||
const chapter = ref<Chapters | null>(null)
|
||||
|
||||
const pageEl = ref<HTMLElement | null>(null)
|
||||
const pages = ref<string[]>([])
|
||||
const currentPage = ref(0)
|
||||
|
||||
const goBack = () => router.back()
|
||||
|
||||
const currentText = computed(() => pages.value[currentPage.value] ?? "")
|
||||
|
||||
const pageInfo = computed(() => {
|
||||
if (pages.value.length === 0) return ""
|
||||
return `第 ${currentPage.value + 1} / ${pages.value.length} 页`
|
||||
})
|
||||
|
||||
const measureCharWidth = (fontSize: number) => {
|
||||
const probe = document.createElement("span")
|
||||
probe.textContent = "国"
|
||||
probe.style.cssText = `font-size:${fontSize}px;position:absolute;visibility:hidden;white-space:nowrap;`
|
||||
document.body.appendChild(probe)
|
||||
const width = probe.getBoundingClientRect().width
|
||||
document.body.removeChild(probe)
|
||||
return width || fontSize
|
||||
}
|
||||
|
||||
const paginate = () => {
|
||||
const content = chapter.value?.content || ""
|
||||
if (!content) {
|
||||
pages.value = []
|
||||
currentPage.value = 0
|
||||
return
|
||||
}
|
||||
const el = pageEl.value
|
||||
if (!el) return
|
||||
|
||||
const fontSize = 6
|
||||
const lineHeight = 1.8
|
||||
const width = el.clientWidth
|
||||
const height = el.clientHeight
|
||||
const charWidth = measureCharWidth(fontSize)
|
||||
const charsPerLine = Math.max(1, Math.floor(width / charWidth))
|
||||
const linesPerPage = Math.max(1, Math.floor(height / (fontSize * lineHeight)))
|
||||
|
||||
// 把内容按行数拆分成逻辑行,再按每页行数分组
|
||||
const logicalLines: string[] = []
|
||||
for (const para of content.split(/\r?\n/)) {
|
||||
if (para === "") {
|
||||
logicalLines.push("")
|
||||
continue
|
||||
}
|
||||
for (let i = 0; i < para.length; i += charsPerLine) {
|
||||
logicalLines.push(para.slice(i, i + charsPerLine))
|
||||
}
|
||||
}
|
||||
|
||||
const arr: string[] = []
|
||||
for (let i = 0; i < logicalLines.length; i += linesPerPage) {
|
||||
arr.push(logicalLines.slice(i, i + linesPerPage).join("\n"))
|
||||
}
|
||||
pages.value = arr.length ? arr : [""]
|
||||
currentPage.value = 0
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage.value > 0) currentPage.value--
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage.value < pages.value.length - 1) currentPage.value++
|
||||
}
|
||||
|
||||
const onPageClick = (e: MouseEvent) => {
|
||||
const el = e.currentTarget as HTMLElement
|
||||
const rect = el.getBoundingClientRect()
|
||||
if (e.clientX - rect.left < rect.width / 2) prevPage()
|
||||
else nextPage()
|
||||
}
|
||||
|
||||
const onKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowLeft" || e.key === "PageUp") {
|
||||
prevPage()
|
||||
} else if (e.key === "ArrowRight" || e.key === "PageDown" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
nextPage()
|
||||
}
|
||||
}
|
||||
|
||||
const onResize = () => paginate()
|
||||
|
||||
const loadChapter = async () => {
|
||||
if (!chapterId.value) {
|
||||
message.error("章节参数错误")
|
||||
return
|
||||
}
|
||||
const res = await withLoading(() => BookApi.chapterDetail(chapterId.value), "加载中...")
|
||||
if (res.code === 0) {
|
||||
chapter.value = res.data
|
||||
nextTick(paginate)
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
const switchChapter = async (offset: number) => {
|
||||
const cur = chapter.value
|
||||
if (!cur) return
|
||||
const res = await withLoading(
|
||||
() => BookApi.chapterNav(cur.book_id, cur.number, offset),
|
||||
"加载中..."
|
||||
)
|
||||
if (res.code === 0) {
|
||||
if (res.data) {
|
||||
chapter.value = res.data
|
||||
nextTick(paginate)
|
||||
} else {
|
||||
message.info(offset > 0 ? "已经是最后一章了" : "已经是第一章了")
|
||||
}
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", onKeydown)
|
||||
window.addEventListener("resize", onResize)
|
||||
loadChapter()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeydown)
|
||||
window.removeEventListener("resize", onResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.glass-panel {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(20, 20, 20, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
/* border: 1px solid rgba(255, 255, 255, 0.2); */
|
||||
padding: 20px;
|
||||
color: #3b3b3b;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 16px;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.reading-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 44px 40px 16px;
|
||||
}
|
||||
|
||||
.chapter-name {
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #3b3b3b;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-text {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
color: #3b3b3b;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.page-nav {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.nav-btns {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue