feat(book): 添加章节标题模糊搜索功能

1. 后端接口新增keyword参数支持章节标题模糊查询
2. 前端书籍详情页添加章节搜索输入框,调用更新后的接口
3. 调整章节头部布局适配搜索框
This commit is contained in:
sparksfly 2026-08-23 23:32:46 +08:00
parent e6329919be
commit dd7504aae7
3 changed files with 37 additions and 8 deletions

View File

@ -155,19 +155,22 @@ pub async fn book_detail(
pub async fn chapter_page(
pool: State<'_, SqlitePool>,
book_id: i64,
keyword: String,
page: i32,
limit: i32,
) -> Result<ApiResponse<PageResult<Chapters>>, String> {
let offset = (page - 1) * limit;
let total: i64 =
sqlx::query_scalar::<_, i64>("SELECT COUNT(id) FROM chapters WHERE book_id = ?")
sqlx::query_scalar::<_, i64>("SELECT COUNT(id) FROM chapters WHERE book_id = ? AND title LIKE ?")
.bind(book_id)
.bind(format!("%{}%", keyword))
.fetch_one(&*pool)
.await
.map_err(|e| e.to_string())?;
let chapters =
sqlx::query_as::<_, Chapters>("SELECT * FROM chapters WHERE book_id = ? LIMIT ?,?")
sqlx::query_as::<_, Chapters>("SELECT * FROM chapters WHERE book_id = ? AND title LIKE ? LIMIT ?,?")
.bind(book_id)
.bind(format!("%{}%", keyword))
.bind(offset)
.bind(limit)
.fetch_all(&*pool)

View File

@ -45,12 +45,13 @@ const BookApi = {
/**
*
* @param bookId ID
* @param keyword
* @param page
* @param limit
* @returns
*/
chapters: async (bookId: number, page: number, limit: number) => {
return await invoke<ApiResponse<PageResult<Chapters>>>("chapter_page", { bookId, page, limit })
chapters: async (bookId: number, keyword: string, page: number, limit: number) => {
return await invoke<ApiResponse<PageResult<Chapters>>>("chapter_page", { bookId, keyword, page, limit })
},
/**
*

View File

@ -53,9 +53,19 @@
<section class="chapter-section">
<div class="chapter-header">
<div class="chapter-header-left">
<span class="chapter-name">章节列表</span>
<span class="chapter-count"> {{ total }} </span>
</div>
<n-input
v-model:value="keyword"
class="chapter-search"
placeholder="搜索章节"
clearable
size="small"
@update:value="loadChapters"
/>
</div>
<n-scrollbar class="chapter-scrollbar">
<n-spin :show="loadingChapters">
@ -122,6 +132,7 @@ const total = ref(0)
const page = ref(1)
const limit = ref(50)
const loadingChapters = ref(false)
const keyword = ref("")
const goBack = () => router.back()
@ -163,7 +174,7 @@ const loadChapters = async () => {
loadingChapters.value = true
try {
const res = await withLoading(
() => BookApi.chapters(bookId.value, page.value, limit.value),
() => BookApi.chapters(bookId.value, keyword.value, page.value, limit.value),
"加载章节中..."
)
if (res.code === 0) {
@ -304,11 +315,24 @@ onMounted(() => {
.chapter-header {
flex-shrink: 0;
display: flex;
align-items: baseline;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.chapter-header-left {
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.chapter-search {
width: 220px;
flex-shrink: 0;
}
.chapter-name {
font-size: 15px;
font-weight: 600;
@ -337,6 +361,7 @@ onMounted(() => {
display: flex;
align-items: center;
gap: 10px;
box-sizing: border-box;
width: 100%;
padding: 8px 10px;
border-radius: var(--radius-sm);