From 8177cf9ab6e916f18d84949e0afd73bebc035edc Mon Sep 17 00:00:00 2001 From: Yuhang Wu Date: Thu, 20 Aug 2026 15:56:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=B9=A6=E7=B1=8D?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=AF=BC=E5=85=A5=E3=80=81=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E3=80=81=E7=BC=96=E8=BE=91=E7=AD=89=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增书籍数据模型与数据库表结构 - 实现后端书籍CRUD与章节分页接口 - 添加前端书籍列表页面与导入功能 - 补充类型定义与API封装 --- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/book.rs | 144 +++++++++++++++++++- src-tauri/src/models.rs | 46 +++---- src-tauri/src/services/db.rs | 13 +- src-tauri/src/services/mod.rs | 3 +- src-tauri/src/services/parser.rs | 9 ++ src/api/book.ts | 57 ++++++++ src/types/global.ts | 47 +++++++ src/views/Home.vue | 219 +++++++++++++++++++++++++++++-- 9 files changed, 488 insertions(+), 52 deletions(-) create mode 100644 src-tauri/src/services/parser.rs create mode 100644 src/api/book.ts create mode 100644 src/types/global.ts diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e6f9852..7649541 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,7 +34,7 @@ sqlx = { version = "0.9.0", features = [ "tls-native-tls", "chrono", ] } -chrono = "0.4.45" +chrono = { version = "0.4.45", features = ["serde"] } tauri-plugin-dialog = "2.7.2" encoding_rs = "0.8.35" regex = "1.13.1" diff --git a/src-tauri/src/commands/book.rs b/src-tauri/src/commands/book.rs index 1f72208..30be69f 100644 --- a/src-tauri/src/commands/book.rs +++ b/src-tauri/src/commands/book.rs @@ -1,6 +1,18 @@ +use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; +use tauri::State; use tauri_helper::auto_collect_command; -use crate::models::ApiResponse; +use crate::models::{ApiResponse, Books, Chapters, PageResult}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct BookSaveReq { + pub id: i64, + pub title: String, + pub author: String, + pub cover: String, + pub instruction: String, +} /** * 导入书籍 @@ -9,17 +21,137 @@ use crate::models::ApiResponse; */ #[tauri::command] #[auto_collect_command] -pub async fn import_book(path: String) -> Result, String> { - let content = std::fs::read_to_string(&path).map_err(|e| e.to_string()); - Ok(ApiResponse::success(())) +pub async fn import_book( + pool: State<'_, SqlitePool>, + path: String, +) -> Result, String> { + let title = std::path::Path::new(&path) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("未命名") + .to_string(); + sqlx::query("INSERT INTO books (title, file_path) VALUES (?,?)") + .bind(&title) + .bind(&path) + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; + Ok(ApiResponse::<()>::success_empty()) } /** * 获取书籍列表 + * + * @param title 书籍标题模糊查询 * @returns 书籍列表 */ #[tauri::command] #[auto_collect_command] -pub async fn book_list() -> Result>, String> { - Ok(ApiResponse::success(Vec::new())) +pub async fn book_list( + pool: State<'_, SqlitePool>, + title: String, +) -> Result>, String> { + let title = title.trim().to_string(); + let books = sqlx::query_as::<_, Books>("SELECT * FROM books WHERE title LIKE ?") + .bind(format!("%{}%", title)) + .fetch_all(&*pool) + .await + .map_err(|e| e.to_string())?; + Ok(ApiResponse::success(books)) +} + +/** + * 更新书籍 + * @param id 书籍ID + * @returns 更新结果 + */ +#[tauri::command] +#[auto_collect_command] +pub async fn book_edit( + pool: State<'_, SqlitePool>, + params: BookSaveReq, +) -> Result, String> { + sqlx::query("UPDATE books SET title = ?, author = ?, cover = ?, instruction = ? WHERE id = ?") + .bind(params.title) + .bind(params.author) + .bind(params.cover) + .bind(params.instruction) + .bind(params.id) + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; + Ok(ApiResponse::<()>::success_empty()) +} + +/** + * 删除书籍 + * @param id 书籍ID + * @returns 删除结果 + */ +#[tauri::command] +#[auto_collect_command] +pub async fn book_del(pool: State<'_, SqlitePool>, id: i64) -> Result, String> { + sqlx::query("DELETE FROM books WHERE id = ?") + .bind(id) + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; + Ok(ApiResponse::<()>::success_empty()) +} + +/** + * 获取书籍详情 + * @param id 书籍ID + * @returns 书籍详情 + */ +#[tauri::command] +#[auto_collect_command] +pub async fn book_detail( + pool: State<'_, SqlitePool>, + id: i64, +) -> Result, String> { + let book = sqlx::query_as::<_, Books>("SELECT * FROM books WHERE id = ?") + .bind(id) + .fetch_one(&*pool) + .await + .map_err(|e| e.to_string())?; + Ok(ApiResponse::success(book)) +} + +/** + * 获取书籍章节列表 + * @param book_id 书籍ID + * @param page 页码 + * @param limit 每页数量 + * @returns 章节列表 + */ +#[tauri::command] +#[auto_collect_command] +pub async fn chapter_page( + pool: State<'_, SqlitePool>, + book_id: i64, + page: i32, + limit: i32, +) -> Result>, String> { + let offset = (page - 1) * limit; + let total: i64 = + sqlx::query_scalar::<_, i64>("SELECT COUNT(id) FROM chapters WHERE book_id = ?") + .bind(book_id) + .fetch_one(&*pool) + .await + .map_err(|e| e.to_string())?; + let chapters = + sqlx::query_as::<_, Chapters>("SELECT * FROM chapters WHERE book_id = ? LIMIT ?,?") + .bind(book_id) + .bind(offset) + .bind(limit) + .fetch_all(&*pool) + .await + .map_err(|e| e.to_string())?; + Ok(ApiResponse::success(PageResult { + total, + list: chapters, + page, + page_size: limit, + })) } diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 36307f5..5997a66 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use sqlx::{FromRow}; #[derive(Debug, Serialize, Deserialize)] pub struct PageResult { @@ -21,7 +22,7 @@ impl ApiResponse { pub fn success(data: T) -> Self { Self { success: true, - code: 200, + code: 0, msg: "操作成功".to_string(), data: Some(data), } @@ -30,7 +31,7 @@ impl ApiResponse { pub fn success_empty() -> ApiResponse<()> { ApiResponse { success: true, - code: 200, + code: 0, msg: "操作成功".to_string(), data: None, } @@ -46,35 +47,28 @@ impl ApiResponse { } } -#[derive(Debug, Serialize)] -pub struct BookInfo { +#[derive(Debug, Serialize, Deserialize, FromRow)] +pub struct Books { pub id: i64, pub title: String, pub author: String, pub cover: String, - pub total_chapters: i32, - pub total_chars: i32, - pub introduction: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Chapter { - pub id: i64, - pub number: i32, - pub title: String, - pub content: String, - pub char_count: i32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Book { - pub id: Option, - pub title: String, - pub author: String, - pub cover: String, - pub introduction: String, pub file_path: String, pub total_chapters: i32, pub total_chars: i32, - pub chapters: Vec, + pub introduction: String, + pub create_time: chrono::NaiveDateTime, + pub last_read_chapter_id: i64, + pub last_read_position: i32, + pub last_read_time: chrono::NaiveDateTime, +} + +#[derive(Debug, FromRow, Serialize, Deserialize, Clone)] +pub struct Chapters { + pub id: i64, + pub book_id: i64, + pub number: i32, + pub title: String, + pub content: String, + pub total_chars: i32, } \ No newline at end of file diff --git a/src-tauri/src/services/db.rs b/src-tauri/src/services/db.rs index 08d1d1b..2c933c8 100644 --- a/src-tauri/src/services/db.rs +++ b/src-tauri/src/services/db.rs @@ -1,4 +1,4 @@ -use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; use tauri::{AppHandle, Manager}; pub async fn init_pool(app: &AppHandle) -> Result { @@ -6,12 +6,14 @@ pub async fn init_pool(app: &AppHandle) -> Result { let app_dir = app.path().app_data_dir().expect("无法获取APP数据目录"); std::fs::create_dir_all(&app_dir).unwrap(); let db_path = app_dir.join("reader.db"); - let db_url = format!("sqlite:{}", db_path.to_str().unwrap()); // 初始化数据库连接池 + let options = SqliteConnectOptions::new() + .filename(&db_path) + .create_if_missing(true); let pool = SqlitePoolOptions::new() .max_connections(5) .min_connections(1) - .connect(&db_url) + .connect_with(options) .await?; sqlx::query( " @@ -20,6 +22,7 @@ pub async fn init_pool(app: &AppHandle) -> Result { title TEXT DEFAULT '', author TEXT DEFAULT '', cover TEXT DEFAULT '', + introduction TEXT DEFAULT '', file_path TEXT DEFAULT '', total_chapters INTEGER DEFAULT 0, total_chars INTEGER DEFAULT 0, @@ -33,7 +36,7 @@ pub async fn init_pool(app: &AppHandle) -> Result { CREATE TABLE IF NOT EXISTS chapters ( id INTEGER, book_id INTEGER NOT NULL, - chapter_number INTEGER NOT NULL, + number INTEGER NOT NULL, title TEXT NOT NULL DEFAULT '', content TEXT NOT NULL, total_chars INTEGER DEFAULT 0, @@ -41,7 +44,7 @@ pub async fn init_pool(app: &AppHandle) -> Result { ); CREATE INDEX IF NOT EXISTS idx_chapters_book - ON chapters(book_id, chapter_number); + ON chapters(book_id, number); ", ) .execute(&pool) diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 8c5eabd..e459940 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1 +1,2 @@ -pub mod db; \ No newline at end of file +pub mod db; +pub mod parser; \ No newline at end of file diff --git a/src-tauri/src/services/parser.rs b/src-tauri/src/services/parser.rs new file mode 100644 index 0000000..f629cd1 --- /dev/null +++ b/src-tauri/src/services/parser.rs @@ -0,0 +1,9 @@ +// use regex::Regex; +// use std::path::Path; + +// use crate::models::{Books, Chapters}; + +// static CHAPTER_RE: Lazy = Lazy::new(|| { +// Regex::new(r"(?m)^[ \t]*(第[0-9一二三四五六七八九十百千万零两]+[章节回卷集部篇][^\n]*|Chapter\s+\d+[^\n]*|楔子|序章|前言|引子|后记|尾声|番外[^\n]*)\s*$").unwrap() +// }); + diff --git a/src/api/book.ts b/src/api/book.ts new file mode 100644 index 0000000..2421adc --- /dev/null +++ b/src/api/book.ts @@ -0,0 +1,57 @@ +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>("import_book", { path }) + }, + /** + * 获取书籍列表 + * @param title 书籍标题模糊查询 + * @returns 书籍列表 + */ + list: async (title: string) => { + return await invoke>("book_list", { title }) + }, + /** + * 编辑书籍 + * @param params 书籍参数 + * @returns 编辑后的书籍 + */ + edit: async (params: BookSaveParams) => { + return await invoke>("book_edit", { params }) + }, + /** + * 删除书籍 + * @param id 书籍ID + * @returns 删除结果 + */ + del: async (id: number) => { + return await invoke>("book_del", { id }) + }, + /** + * 获取书籍详情 + * @param id 书籍ID + * @returns 书籍详情 + */ + detail: async (id: number) => { + return await invoke>("book_detail", { id }) + }, + /** + * 获取书籍章节列表 + * @param book_id 书籍ID + * @param page 页码 + * @param limit 每页数量 + * @returns 书籍章节列表 + */ + chapters: async (book_id: number, page: number, limit: number) => { + return await invoke>>("chapter_page", { book_id, page, limit }) + }, +} + +export default BookApi diff --git a/src/types/global.ts b/src/types/global.ts new file mode 100644 index 0000000..fb4ce36 --- /dev/null +++ b/src/types/global.ts @@ -0,0 +1,47 @@ +interface ApiResponse { + success: boolean + code: number + msg: string + data: T +} + +interface PageResult { + total: number + list: T[] + page: number + size: number +} + +interface Books { + id: number + title: string + author: string + cover: string + instruction: string + file_path: string + total_chapters: number + total_chars: number + create_time: string + last_read_time: string + last_read_chapter: number + last_read_position: number +} + +interface Chapters { + id: number + book_id: number + number: number + title: string + content: string + total_chars: number +} + +interface BookSaveParams { + id: number + title: string + author: string + cover: string + instruction: string +} + +export type { ApiResponse, PageResult, Books, Chapters, BookSaveParams } diff --git a/src/views/Home.vue b/src/views/Home.vue index e3715d8..88d0dc6 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,12 +1,7 @@ -