From f7cec7ce262bf1447633d51d01d67352013c3e26 Mon Sep 17 00:00:00 2001 From: Yuhang Wu Date: Thu, 20 Aug 2026 18:02:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81=E4=B8=8E=E7=AB=A0=E8=8A=82?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增全局加载状态管理store与hook,添加全局加载组件 2. 新增获取章节详情、相邻章节的后端接口与前端api调用 3. 优化书籍列表、删除、导入等操作的加载状态提示 4. 完成阅读页面开发,支持分页阅读与章节切换 5. 调整全局悬浮背景色与窗口阴影配置 6. 修复书籍列表搜索回车事件,优化章节列表点击跳转 --- src-tauri/src/commands/book.rs | 45 ++++++ src-tauri/tauri.conf.json | 2 +- src/App.vue | 2 + src/api/book.ts | 34 ++++- src/assets/global.css | 2 +- src/components/GlobalLoading.vue | 58 ++++++++ src/hooks/useLoading.ts | 34 +++++ src/stores/loading.ts | 28 ++++ src/types/components.d.ts | 1 + src/views/BookDetail.vue | 21 ++- src/views/Home.vue | 13 +- src/views/Reader.vue | 247 ++++++++++++++++++++++++++++++- 12 files changed, 465 insertions(+), 22 deletions(-) create mode 100644 src/components/GlobalLoading.vue create mode 100644 src/hooks/useLoading.ts create mode 100644 src/stores/loading.ts diff --git a/src-tauri/src/commands/book.rs b/src-tauri/src/commands/book.rs index 850dbde..cfb5add 100644 --- a/src-tauri/src/commands/book.rs +++ b/src-tauri/src/commands/book.rs @@ -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, 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>, 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)) +} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2f67184..5fe6d1b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -21,7 +21,7 @@ "decorations": false, "alwaysOnTop": false, "skipTaskbar": false, - "shadow": true, + "shadow": false, "visible": true, "center": false } diff --git a/src/App.vue b/src/App.vue index 5083822..588ea71 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,6 +3,7 @@ + @@ -12,6 +13,7 @@ diff --git a/src/hooks/useLoading.ts b/src/hooks/useLoading.ts new file mode 100644 index 0000000..c9f6004 --- /dev/null +++ b/src/hooks/useLoading.ts @@ -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 (fn: () => Promise, text?: string): Promise => { + store.start(text) + try { + return await fn() + } finally { + store.stop() + } + } + + return { + loading: computed(() => store.loading), + text: computed(() => store.text), + start, + stop, + reset, + withLoading, + } +} diff --git a/src/stores/loading.ts b/src/stores/loading.ts new file mode 100644 index 0000000..502eb19 --- /dev/null +++ b/src/stores/loading.ts @@ -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 = "加载中..." + }, + }, +}) diff --git a/src/types/components.d.ts b/src/types/components.d.ts index e4bd399..ca6aadf 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -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'] diff --git a/src/views/BookDetail.vue b/src/views/BookDetail.vue index 5049255..37751b5 100644 --- a/src/views/BookDetail.vue +++ b/src/views/BookDetail.vue @@ -52,7 +52,7 @@ responsive="screen" > -
+
{{ ch.number }} {{ ch.title }} {{ ch.total_chars }} 字 @@ -85,16 +85,17 @@ \ No newline at end of file + +.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%; +} +