feat: 完成书籍导入、管理与详情页优化
1. 重构书籍导入逻辑,新增本地小说解析功能,自动提取书名、简介、章节 2. 修复参数命名不统一问题,统一使用introduction替代instruction 3. 新增右键菜单编辑删除书籍功能,添加编辑弹窗与表单校验 4. 优化书籍封面与简介展示样式,修复章节加载异常处理 5. 完善数据库表结构,兼容旧版本数据库 6. 替换上传图标为更贴合的导入图标,新增缺失的naive-ui组件类型声明
This commit is contained in:
parent
117fbe5ac4
commit
c12cbaaabc
|
|
@ -4,6 +4,7 @@ use tauri::State;
|
|||
use tauri_helper::auto_collect_command;
|
||||
|
||||
use crate::models::{ApiResponse, Books, Chapters, PageResult};
|
||||
use crate::services::parser::parse_book;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct BookSaveReq {
|
||||
|
|
@ -11,7 +12,7 @@ pub struct BookSaveReq {
|
|||
pub title: String,
|
||||
pub author: String,
|
||||
pub cover: String,
|
||||
pub instruction: String,
|
||||
pub introduction: String,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,21 +22,45 @@ pub struct BookSaveReq {
|
|||
*/
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn import_book(
|
||||
pub async fn book_import(
|
||||
pool: State<'_, SqlitePool>,
|
||||
path: String,
|
||||
) -> Result<ApiResponse<()>, 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)
|
||||
let parsed = parse_book(&path)?;
|
||||
|
||||
let mut tx = pool.begin().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO books (title, author, introduction, file_path, total_chapters, total_chars) VALUES (?,?,?,?,?,?)",
|
||||
)
|
||||
.bind(&parsed.title)
|
||||
.bind(&parsed.author)
|
||||
.bind(&parsed.introduction)
|
||||
.bind(&path)
|
||||
.bind(parsed.total_chapters)
|
||||
.bind(parsed.total_chars)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let book_id = result.last_insert_rowid();
|
||||
|
||||
for chapter in &parsed.chapters {
|
||||
sqlx::query(
|
||||
"INSERT INTO chapters (book_id, number, title, content, total_chars) VALUES (?,?,?,?,?)",
|
||||
)
|
||||
.bind(book_id)
|
||||
.bind(chapter.number)
|
||||
.bind(&chapter.title)
|
||||
.bind(&chapter.content)
|
||||
.bind(chapter.total_chars)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
tx.commit().await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(ApiResponse::<()>::success_empty())
|
||||
}
|
||||
|
||||
|
|
@ -71,11 +96,11 @@ pub async fn book_edit(
|
|||
pool: State<'_, SqlitePool>,
|
||||
params: BookSaveReq,
|
||||
) -> Result<ApiResponse<()>, String> {
|
||||
sqlx::query("UPDATE books SET title = ?, author = ?, cover = ?, instruction = ? WHERE id = ?")
|
||||
sqlx::query("UPDATE books SET title = ?, author = ?, cover = ?, introduction = ? WHERE id = ?")
|
||||
.bind(params.title)
|
||||
.bind(params.author)
|
||||
.bind(params.cover)
|
||||
.bind(params.instruction)
|
||||
.bind(params.introduction)
|
||||
.bind(params.id)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ pub async fn init_pool(app: &AppHandle) -> Result<SqlitePool, sqlx::Error> {
|
|||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chapters (
|
||||
id INTEGER,
|
||||
id INTEGER PRIMARY KEY,
|
||||
book_id INTEGER NOT NULL,
|
||||
number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
|
|
@ -49,5 +49,38 @@ pub async fn init_pool(app: &AppHandle) -> Result<SqlitePool, sqlx::Error> {
|
|||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
// 兼容旧库:确保 chapters.id 为主键(自增)
|
||||
let id_is_pk: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('chapters') WHERE name = 'id' AND pk > 0",
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
if id_is_pk == 0 {
|
||||
sqlx::query("DROP TABLE IF EXISTS chapters")
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"
|
||||
CREATE TABLE IF NOT EXISTS chapters (
|
||||
id INTEGER PRIMARY KEY,
|
||||
book_id INTEGER NOT NULL,
|
||||
number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
total_chars INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (book_id) REFERENCES books ON DELETE CASCADE
|
||||
);
|
||||
",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
sqlx::query("CREATE INDEX IF NOT EXISTS idx_chapters_book ON chapters(book_id, number)")
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,105 @@
|
|||
// use regex::Regex;
|
||||
// use std::path::Path;
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
// use crate::models::{Books, Chapters};
|
||||
use chardetng::{EncodingDetector, Iso2022JpDetection, Utf8Detection};
|
||||
use regex::Regex;
|
||||
|
||||
// static CHAPTER_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
// Regex::new(r"(?m)^[ \t]*(第[0-9一二三四五六七八九十百千万零两]+[章节回卷集部篇][^\n]*|Chapter\s+\d+[^\n]*|楔子|序章|前言|引子|后记|尾声|番外[^\n]*)\s*$").unwrap()
|
||||
// });
|
||||
static CHAPTER_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?m)^[ \t]*(第[0-9一二三四五六七八九十百千万零两]+[章节回卷集部篇][^\n]*|Chapter\s+\d+[^\n]*|楔子|序章|前言|引子|后记|尾声|番外[^\n]*)\s*$").unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedChapter {
|
||||
pub number: i32,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub total_chars: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedBook {
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub introduction: String,
|
||||
pub total_chapters: i32,
|
||||
pub total_chars: i32,
|
||||
pub chapters: Vec<ParsedChapter>,
|
||||
}
|
||||
|
||||
/// 读取文件内容,自动识别编码并解码为 UTF-8 字符串
|
||||
pub fn read_to_string(path: &str) -> Result<String, String> {
|
||||
let bytes = std::fs::read(path).map_err(|e| format!("读取文件失败: {e}"))?;
|
||||
if bytes.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
let mut detector = EncodingDetector::new(Iso2022JpDetection::Deny);
|
||||
detector.feed(&bytes, true);
|
||||
let encoding = detector.guess(None, Utf8Detection::Allow);
|
||||
let (text, _, _) = encoding.decode(&bytes);
|
||||
Ok(text.into_owned())
|
||||
}
|
||||
|
||||
/// 解析小说文件,返回书籍信息与章节列表
|
||||
pub fn parse_book(file_path: &str) -> Result<ParsedBook, String> {
|
||||
let text = read_to_string(file_path)?;
|
||||
if text.trim().is_empty() {
|
||||
return Err("文件内容为空".to_string());
|
||||
}
|
||||
|
||||
let title = Path::new(file_path)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("未命名")
|
||||
.to_string();
|
||||
|
||||
let mut positions: Vec<(usize, usize)> = Vec::new();
|
||||
for m in CHAPTER_RE.find_iter(&text) {
|
||||
positions.push((m.start(), m.end()));
|
||||
}
|
||||
|
||||
let mut chapters: Vec<ParsedChapter> = Vec::new();
|
||||
let mut introduction = String::new();
|
||||
|
||||
if positions.is_empty() {
|
||||
let content = text.trim().to_string();
|
||||
let total_chars = content.chars().count() as i32;
|
||||
chapters.push(ParsedChapter {
|
||||
number: 1,
|
||||
title: "正文".to_string(),
|
||||
content,
|
||||
total_chars,
|
||||
});
|
||||
} else {
|
||||
introduction = text[..positions[0].0].trim().to_string();
|
||||
|
||||
for (i, &(start, end)) in positions.iter().enumerate() {
|
||||
let chapter_title = text[start..end].trim().to_string();
|
||||
let content_start = end;
|
||||
let content_end = if i + 1 < positions.len() {
|
||||
positions[i + 1].0
|
||||
} else {
|
||||
text.len()
|
||||
};
|
||||
let content = text[content_start..content_end].trim().to_string();
|
||||
let total_chars = content.chars().count() as i32;
|
||||
chapters.push(ParsedChapter {
|
||||
number: i as i32 + 1,
|
||||
title: chapter_title,
|
||||
content,
|
||||
total_chars,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let total_chars = chapters.iter().map(|c| c.total_chars).sum();
|
||||
let total_chapters = chapters.len() as i32;
|
||||
|
||||
Ok(ParsedBook {
|
||||
title,
|
||||
author: String::new(),
|
||||
introduction,
|
||||
total_chapters,
|
||||
total_chars,
|
||||
chapters,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const BookApi = {
|
|||
* @returns 导入结果
|
||||
*/
|
||||
import: async (path: string) => {
|
||||
return await invoke<ApiResponse<any>>("import_book", { path })
|
||||
return await invoke<ApiResponse<any>>("book_import", { path })
|
||||
},
|
||||
/**
|
||||
* 获取书籍列表
|
||||
|
|
@ -44,13 +44,13 @@ const BookApi = {
|
|||
},
|
||||
/**
|
||||
* 获取书籍章节列表
|
||||
* @param book_id 书籍ID
|
||||
* @param bookId 书籍ID
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @returns 书籍章节列表
|
||||
*/
|
||||
chapters: async (book_id: number, page: number, limit: number) => {
|
||||
return await invoke<ApiResponse<PageResult<Chapters>>>("chapter_page", { book_id, page, limit })
|
||||
chapters: async (bookId: number, page: number, limit: number) => {
|
||||
return await invoke<ApiResponse<PageResult<Chapters>>>("chapter_page", { bookId, page, limit })
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div v-else class="cover-fallback" :style="{ background: coverGradient(title) }">
|
||||
<span class="cover-spine"></span>
|
||||
<div class="cover-meta">
|
||||
<span class="cover-title">{{ title }}</span>
|
||||
<p class="cover-title">{{ title }}</p>
|
||||
<span class="cover-author">{{ author || "佚名" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ declare module 'vue' {
|
|||
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
|
|
@ -29,6 +32,7 @@ declare module 'vue' {
|
|||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ interface BookSaveParams {
|
|||
title: string
|
||||
author: string
|
||||
cover: string
|
||||
instruction: string
|
||||
introduction: string
|
||||
}
|
||||
|
||||
export type { ApiResponse, PageResult, Books, Chapters, BookSaveParams }
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<n-descriptions-item label="上次阅读">{{ formatTime(book?.last_read_time) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="阅读进度">{{ readProgress }}</n-descriptions-item>
|
||||
<n-descriptions-item label="简介" :span="2">
|
||||
{{ book?.introduction || "暂无简介" }}
|
||||
<div class="intro-content">{{ book?.introduction || "暂无简介" }}</div>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</div>
|
||||
|
|
@ -138,6 +138,8 @@ const loadChapters = async () => {
|
|||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
} catch (e) {
|
||||
message.error("章节加载失败")
|
||||
} finally {
|
||||
loadingChapters.value = false
|
||||
}
|
||||
|
|
@ -188,8 +190,9 @@ onMounted(() => {
|
|||
|
||||
.summary-cover {
|
||||
width: 120px;
|
||||
aspect-ratio: 2 / 3;
|
||||
height: 180px;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
|
|
@ -213,6 +216,16 @@ onMounted(() => {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.intro-content {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.detail-divider {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
|
@ -246,6 +259,14 @@ onMounted(() => {
|
|||
.chapter-scrollbar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.n-scrollbar-container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.n-scrollbar-content) {
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<n-icon :size="20" :component="SettingOutlined" />
|
||||
</button>
|
||||
<button class="setting-btn" title="导入" aria-label="导入" @click="importBook">
|
||||
<n-icon :size="20" :component="CloudUploadOutlined" />
|
||||
<n-icon :size="20" :component="ImportOutlined" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
:key="book.id"
|
||||
class="book-card"
|
||||
@click="goDetail(book)"
|
||||
@contextmenu.prevent="onContextMenu($event, book)"
|
||||
>
|
||||
<div class="book-cover-wrap">
|
||||
<BookCover :title="book.title" :author="book.author" :cover="book.cover" />
|
||||
|
|
@ -47,16 +48,89 @@
|
|||
</div>
|
||||
</n-scrollbar>
|
||||
</section>
|
||||
|
||||
<n-dropdown
|
||||
trigger="manual"
|
||||
:show="showDropdown"
|
||||
:x="dropdownX"
|
||||
:y="dropdownY"
|
||||
:options="dropdownOptions"
|
||||
@select="handleDropdownSelect"
|
||||
@clickoutside="showDropdown = false"
|
||||
/>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showEditModal"
|
||||
preset="card"
|
||||
title="编辑书籍"
|
||||
:bordered="false"
|
||||
style="width: 480px"
|
||||
>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="editForm"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="64"
|
||||
require-mark-placement="left"
|
||||
>
|
||||
<n-form-item label="书名" path="title">
|
||||
<n-input v-model:value="editForm.title" placeholder="请输入书名" />
|
||||
</n-form-item>
|
||||
<n-form-item label="作者" path="author">
|
||||
<n-input v-model:value="editForm.author" placeholder="请输入作者" />
|
||||
</n-form-item>
|
||||
<n-form-item label="简介" path="introduction">
|
||||
<n-input
|
||||
v-model:value="editForm.introduction"
|
||||
type="textarea"
|
||||
placeholder="请输入简介"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showEditModal = false">取消</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="submitEdit">保存</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showDeleteModal"
|
||||
preset="card"
|
||||
title="删除书籍"
|
||||
:bordered="false"
|
||||
style="width: 400px"
|
||||
>
|
||||
<div class="delete-tip">
|
||||
确定要删除《{{ deleteBook?.title }}》吗?删除后不可恢复。
|
||||
</div>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showDeleteModal = false">取消</n-button>
|
||||
<n-button type="error" :loading="deleting" @click="doDelete">删除</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, type Component } from "vue"
|
||||
import { open } from "@tauri-apps/plugin-dialog"
|
||||
import { BookSearch24Regular } from "@vicons/fluent"
|
||||
import { SettingOutlined, CloudUploadOutlined } from "@vicons/antd"
|
||||
import {
|
||||
SettingOutlined,
|
||||
ImportOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@vicons/antd"
|
||||
import { NIcon } from "naive-ui"
|
||||
import BookApi from "@/api/book"
|
||||
import BookCover from "@/components/BookCover.vue"
|
||||
import type { Books } from "@/types/global"
|
||||
import type { Books, BookSaveParams } from "@/types/global"
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
|
@ -64,6 +138,38 @@ const message = useMessage()
|
|||
const bookList = ref<Books[]>([])
|
||||
const searchValue = ref("")
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
const deleting = ref(false)
|
||||
const deleteBook = ref<Books | null>(null)
|
||||
|
||||
const showDropdown = ref(false)
|
||||
const dropdownX = ref(0)
|
||||
const dropdownY = ref(0)
|
||||
const contextBook = ref<Books | null>(null)
|
||||
|
||||
const renderDropdownIcon = (icon: Component) => () => h(NIcon, null, { default: () => h(icon) })
|
||||
|
||||
const dropdownOptions = [
|
||||
{ label: "编辑", key: "edit", icon: renderDropdownIcon(EditOutlined) },
|
||||
{ label: "删除", key: "delete", icon: renderDropdownIcon(DeleteOutlined) },
|
||||
]
|
||||
|
||||
const showEditModal = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref<any>()
|
||||
|
||||
const editForm = reactive<BookSaveParams>({
|
||||
id: 0,
|
||||
title: "",
|
||||
author: "",
|
||||
cover: "",
|
||||
introduction: "",
|
||||
})
|
||||
|
||||
const rules = {
|
||||
title: { required: true, message: "请输入书名", trigger: ["blur", "input"] },
|
||||
}
|
||||
|
||||
const getBookList = async () => {
|
||||
const res = await BookApi.list(searchValue.value)
|
||||
if (res.code === 0) {
|
||||
|
|
@ -77,20 +183,87 @@ const goSettingView = () => {
|
|||
router.push({ name: "Setting" })
|
||||
}
|
||||
|
||||
const goReaderView = () => {
|
||||
router.push({ name: "Reader" })
|
||||
}
|
||||
|
||||
const goDetail = (book: Books) => {
|
||||
router.push({ name: "BookDetail", query: { id: book.id } })
|
||||
}
|
||||
|
||||
const onContextMenu = (e: MouseEvent, book: Books) => {
|
||||
contextBook.value = book
|
||||
dropdownX.value = e.clientX
|
||||
dropdownY.value = e.clientY
|
||||
showDropdown.value = true
|
||||
}
|
||||
|
||||
const handleDropdownSelect = (key: string | number) => {
|
||||
showDropdown.value = false
|
||||
if (key === "edit") {
|
||||
openEditModal(contextBook.value)
|
||||
} else if (key === "delete") {
|
||||
confirmDelete(contextBook.value)
|
||||
}
|
||||
}
|
||||
|
||||
const openEditModal = (book: Books | null) => {
|
||||
if (!book) return
|
||||
editForm.id = book.id
|
||||
editForm.title = book.title
|
||||
editForm.author = book.author
|
||||
editForm.cover = book.cover
|
||||
editForm.introduction = book.introduction || ""
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
const submitEdit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await BookApi.edit({ ...editForm })
|
||||
if (res.code === 0) {
|
||||
message.success("保存成功")
|
||||
showEditModal.value = false
|
||||
getBookList()
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = (book: Books | null) => {
|
||||
if (!book) return
|
||||
deleteBook.value = book
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
const doDelete = async () => {
|
||||
if (!deleteBook.value || deleting.value) return
|
||||
deleting.value = true
|
||||
try {
|
||||
const res = await BookApi.del(deleteBook.value.id)
|
||||
if (res.code === 0) {
|
||||
message.success("删除成功")
|
||||
showDeleteModal.value = false
|
||||
getBookList()
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
} catch (e) {
|
||||
message.error("删除失败,请重试")
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const importBook = async () => {
|
||||
const filePath = await open({
|
||||
multiple: false,
|
||||
filters: [{ name: "TXT", extensions: ["txt", "epub"] }]
|
||||
})
|
||||
console.log(filePath)
|
||||
if (filePath) {
|
||||
const res = await BookApi.import(filePath)
|
||||
if (res.code === 0) {
|
||||
|
|
@ -215,6 +388,7 @@ onMounted(() => {
|
|||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 5px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
|
|
@ -260,4 +434,11 @@ onMounted(() => {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.delete-tip {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue