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%; +} +