From fbc9269e81296b07f6bda400330d41b66a7fd0c6 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 16 Feb 2026 23:23:34 +0800 Subject: [PATCH] Add manage page --- build.js | 2 +- package.json | 3 +- src/db/indexedDb.ts | 78 +++++++++++++++++++++++++++++++- src/db/interfaces.ts | 4 +- src/db/pocketBase.ts | 22 ++++++++- src/manage.html | 13 ++++++ src/manage.tsx | 59 +++++++++++++++++++++++++ src/manage/Home.tsx | 23 ++++++++++ src/manage/dbProvider.tsx | 8 ++++ src/manage/qd/Book.tsx | 49 ++++++++++++++++++++ src/manage/qd/Books.module.css | 20 +++++++++ src/manage/qd/Books.tsx | 81 ++++++++++++++++++++++++++++++++++ src/types.ts | 11 +++++ yarn.lock | 18 ++++++++ 14 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 src/manage.html create mode 100644 src/manage.tsx create mode 100644 src/manage/Home.tsx create mode 100644 src/manage/dbProvider.tsx create mode 100644 src/manage/qd/Book.tsx create mode 100644 src/manage/qd/Books.module.css create mode 100644 src/manage/qd/Books.tsx diff --git a/build.js b/build.js index c3ad43a..355c38f 100644 --- a/build.js +++ b/build.js @@ -92,4 +92,4 @@ fs.rmSync('dist', { recursive: true, force: true }); fs.mkdirSync('dist', { recursive: true }); await build('qdchapter'); await build('qdbook'); -await buildTsx(['popup', 'settings'], ['background']); +await buildTsx(['popup', 'settings', 'manage'], ['background']); diff --git a/package.json b/package.json index 70231e7..2046a5d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "esbuild": "^0.27.3", "pocketbase": "^0.26.8", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "react-router": "^7.13.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "scripts": { diff --git a/src/db/indexedDb.ts b/src/db/indexedDb.ts index 99e4936..c9e8b9d 100644 --- a/src/db/indexedDb.ts +++ b/src/db/indexedDb.ts @@ -1,5 +1,5 @@ import { IndexedDbConfig } from "../config"; -import type { QdChapterInfo, QdBookInfo } from "../types"; +import type { QdChapterInfo, QdBookInfo, PagedData } from "../types"; import { compress, isServiceWorker } from "../utils"; import { hash_qdchapter_info } from "../utils/qd"; import type { Db } from "./interfaces"; @@ -74,6 +74,76 @@ async function get_keys(db: IDBDatabase, st } }); } + +async function get_count(db: IDBDatabase, storeName: string, key?: IDBValidKey | IDBKeyRange, index?: string): Promise { + return new Promise((resolve, reject) => { + const tx = db.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + const req = index ? store.index(index).count(key) : store.count(key); + req.onsuccess = () => { + resolve(req.result); + } + req.onerror = () => { + reject(req.error); + } + }); +} + +async function get_paged_data(db: IDBDatabase, storeName: string, page: number, pageSize: number, key?: IDBValidKey | IDBKeyRange, index?: string): Promise> { + const count = await get_count(db, storeName, key, index); + const offset = (page - 1) * pageSize; + const totalPages = Math.ceil(count / pageSize); + if (page > totalPages || page < 1 || count === 0) { + return { + total: count, + page, + totalPages, + pageSize, + items: [], + }; + } + return new Promise>((resolve, reject) => { + const tx = db.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + const req = index ? store.index(index).openCursor(key) : store.openCursor(key); + const items: T[] = []; + let advanced = false; + req.onsuccess = () => { + const cursor = req.result; + if (cursor) { + if (!advanced && offset > 0) { + cursor.advance(offset); + advanced = true; + } else { + items.push(cursor.value); + } + if (items.length < pageSize) { + cursor.continue(); + } else { + resolve({ + total: count, + page, + totalPages, + pageSize, + items, + }); + } + } else { + resolve({ + total: count, + page, + totalPages, + pageSize, + items, + }); + } + } + req.onerror = () => { + reject(req.error); + } + }); +} + type CompressedQdChapterInfo = { compressed: Uint8Array; bookId: number; @@ -151,6 +221,12 @@ export class IndexedDb implements Db { async saveQdBook(info: QdBookInfo) { await save_data(this.qddb, 'books', info); } + async getQdBooks(page: number, pageSize: number): Promise> { + return await get_paged_data(this.qddb, 'books', page, pageSize); + } + async getQdBook(id: number): Promise { + return await get_data(this.qddb, 'books', id); + } close() { this.qddb.close(); } diff --git a/src/db/interfaces.ts b/src/db/interfaces.ts index 590638c..e21ecd5 100644 --- a/src/db/interfaces.ts +++ b/src/db/interfaces.ts @@ -1,7 +1,7 @@ import { DbConfig, DbType } from "../config"; import { IndexedDb } from "./indexedDb"; import { PocketBaseDb } from "./pocketBase"; -import type { QdChapterInfo, QdBookInfo } from "../types"; +import type { QdChapterInfo, QdBookInfo, PagedData } from "../types"; export interface Db { init(): Promise; @@ -15,6 +15,8 @@ export interface Db { * @param info Book info to save. if id is matched in the database, update the existing record. */ saveQdBook(info: QdBookInfo): Promise; + getQdBook(id: number): Promise; + getQdBooks(page: number, pageSize: number): Promise>; close(): void; } diff --git a/src/db/pocketBase.ts b/src/db/pocketBase.ts index cf39f27..29f6e8e 100644 --- a/src/db/pocketBase.ts +++ b/src/db/pocketBase.ts @@ -2,7 +2,7 @@ import type { Db } from "./interfaces"; import PocketBase from "pocketbase"; import type { CollectionModel } from "pocketbase"; import { PocketBaseConfig } from "../config"; -import { QdChapterInfo, QdBookInfo } from "../types"; +import { QdChapterInfo, QdBookInfo, PagedData } from "../types"; import { hash_qdchapter_info } from "../utils/qd"; const QD_CHAPTERS_FIELDS = [ @@ -173,6 +173,26 @@ export class PocketBaseDb implements Db { }); return records.totalItems > 0; } + async getQdBooks(page: number, pageSize: number): Promise> { + const records = await this.client.collection(`${this.cfg.prefix}qd_books`).getList(page, pageSize, { + fields: 'data', + sort: 'bookId', + }); + return { + total: records.totalItems, + page, + totalPages: records.totalPages, + pageSize, + items: records.items.map(item => item.data), + }; + } + async getQdBook(id: number): Promise { + const records = await this.client.collection(`${this.cfg.prefix}qd_books`).getList(1, 1, { + filter: `bookId = ${id}`, + fields: 'data', + }); + return records.totalItems > 0 ? records.items[0].data : undefined; + } close(): void { this.client.cancelAllRequests(); this.client.authStore.clear(); diff --git a/src/manage.html b/src/manage.html new file mode 100644 index 0000000..2458d2a --- /dev/null +++ b/src/manage.html @@ -0,0 +1,13 @@ + + + + + + 管理数据库 + + + +
+ + + diff --git a/src/manage.tsx b/src/manage.tsx new file mode 100644 index 0000000..a15c334 --- /dev/null +++ b/src/manage.tsx @@ -0,0 +1,59 @@ +import { createRoot } from 'react-dom/client'; +import { useEffect, useState } from 'react'; +import { createHashRouter } from "react-router"; +import { RouterProvider } from "react-router/dom"; +import { Db, createDb } from "./db/interfaces"; +import Home from "./manage/Home"; +import QdBooks from "./manage/qd/Books"; +import { Result } from 'antd'; +import { DbContext } from "./manage/dbProvider"; +import QdBook from "./manage/qd/Book"; + +const router = createHashRouter([ + { + path: "/", + element: , + }, + { + path: "/qd/books", + element: , + }, + { + path: "/qd/book/:id", + element: , + } +]); + +async function connectDb() { + const db = await createDb(); + await db.init(); + return db; +} + +function Manage() { + const [db, setDb] = useState(null); + const [err, setErr] = useState(null); + useEffect(() => { + connectDb().then(setDb).catch(e => { + setErr(e instanceof Error ? e.message : String(e)); + }); + }, []); + if (err) { + return + } + if (!db) { + return + } + return + + +} + +createRoot(document.getElementById("root")!).render(); diff --git a/src/manage/Home.tsx b/src/manage/Home.tsx new file mode 100644 index 0000000..dcf46bc --- /dev/null +++ b/src/manage/Home.tsx @@ -0,0 +1,23 @@ +import { Breadcrumb, Button, Card, Divider, Space, Typography } from 'antd'; +import { useNavigate } from 'react-router'; + +const { Text } = Typography; + +export default function Home() { + const navigate = useNavigate(); + return ( + <> + + + + + + + + + ) +} diff --git a/src/manage/dbProvider.tsx b/src/manage/dbProvider.tsx new file mode 100644 index 0000000..6ad99a2 --- /dev/null +++ b/src/manage/dbProvider.tsx @@ -0,0 +1,8 @@ +import { createContext, useContext } from "react"; +import type { Db } from "../db/interfaces"; + +export const DbContext = createContext(null as any); + +export function useDb() { + return useContext(DbContext); +} diff --git a/src/manage/qd/Book.tsx b/src/manage/qd/Book.tsx new file mode 100644 index 0000000..0230377 --- /dev/null +++ b/src/manage/qd/Book.tsx @@ -0,0 +1,49 @@ +import { Breadcrumb, Result, Button, Skeleton } from "antd"; +import { NavLink, useParams } from "react-router"; +import { useDb } from "../dbProvider"; +import type { QdBookInfo } from "../../types"; +import { useEffect, useState } from "react"; + +export default function Book() { + const db = useDb(); + const { id } = useParams(); + const [book, setBook] = useState(null); + const [err, setErr] = useState(null); + async function load() { + const data = await db.getQdBook(Number(id)); + if (data) { + setBook(data); + } else { + setErr("书籍不存在"); + } + } + function handle() { + load().catch(e => { + setErr(e instanceof Error ? e.message : String(e)); + }); + } + useEffect(() => { + handle(); + }, [id]); + return ( + <> + 主页 + }, { + title: '起点' + }, { + title: 按书籍管理 + }, { + title: book ? `书籍详情:${book.bookName}` : '书籍详情' + }] + } /> + {!book && !err && } + {err && { setErr(null); handle(); }}>重试} />} + + ); +} diff --git a/src/manage/qd/Books.module.css b/src/manage/qd/Books.module.css new file mode 100644 index 0000000..7f68e93 --- /dev/null +++ b/src/manage/qd/Books.module.css @@ -0,0 +1,20 @@ +.books-card { + margin: 8px; + max-width: 400px; + min-width: 350px; + cursor: pointer; +} + +.books-card .name { + margin: 0 8px; + text-align: center; +} + +.books-card .author { + margin: 0 8px; + text-align: right; +} + +.books-card .intro { + margin-left: 8px; +} diff --git a/src/manage/qd/Books.tsx b/src/manage/qd/Books.tsx new file mode 100644 index 0000000..1b4aad4 --- /dev/null +++ b/src/manage/qd/Books.tsx @@ -0,0 +1,81 @@ +import { Breadcrumb, Divider, Affix, Pagination, Skeleton, Result, Button, Card, Flex, Row, Col, Image, Typography } from 'antd'; +import { NavLink, useNavigate, useSearchParams } from 'react-router'; +import { useDb } from '../dbProvider'; +import { useEffect, useState } from 'react'; +import type { PagedData, QdBookInfo } from '../../types'; +import styles from './Books.module.css'; + +const { Paragraph } = Typography; + +export default function Books() { + const db = useDb(); + const [searchParams, setSearchParams] = useSearchParams(); + const [container, setContainer] = useState(null); + const [page, setPage] = useState(Number(searchParams.get('page') || '1')); + const [pageSize, setPageSize] = useState(Number(searchParams.get('pageSize') || '10')); + const [totalCount, setTotalCount] = useState(null); + const [pageData, setPageData] = useState | null>(null); + const [err, setErr] = useState(null); + const navigate = useNavigate(); + async function loadPage() { + const data = await db.getQdBooks(page, pageSize); + setPageData(data); + setTotalCount(data.total); + } + function handle() { + setPageData(null); + loadPage().catch(e => { + setErr(e instanceof Error ? e.message : String(e)); + }); + } + useEffect(() => { + handle(); + }, [page, pageSize]); + return ( + <> + 主页 + }, { + title: '起点' + }, { + title: '按书籍管理' + }] + } /> + +
+ {totalCount !== null && ( container}> + { setPage(page); setPageSize(pageSize); setSearchParams({ page: String(page), pageSize: String(pageSize) }); }} + showTotal={(total, range) => `共 ${total} 条,当前显示 ${range[0]} - ${range[1]} 条`} + showSizeChanger={true} + /> + )} + {!pageData && !err && } + {err && { setErr(null); handle(); }}>重试} />} + {pageData && + {pageData.items.map(book => ( + navigate(`/qd/book/${book.id}`)}> + + + + + +

{book.bookName}

+ {book.bookInfo.pageJson.authorInfo.authorName} 著 + {book.intro} + +
+
+ ))} +
} +
+ + ) +} diff --git a/src/types.ts b/src/types.ts index 6be91f5..d4d2f1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,3 +73,14 @@ export type UrlParamsMap = { } export type UrlParams = DiscriminatedUnion<"page", UrlParamsMap>; + +export type PagedData = { + /**Total number of items */ + total: number; + /**Start at 1*/ + page: number; + /**Total number of pages */ + totalPages: number; + pageSize: number; + items: T[]; +} diff --git a/yarn.lock b/yarn.lock index ec74ffc..f83fa0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -742,6 +742,11 @@ compute-scroll-into-view@^3.0.2: resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz#02c3386ec531fb6a9881967388e53e8564f3e9aa" integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw== +cookie@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + csstype@^3.1.3, csstype@^3.2.2: version "3.2.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" @@ -813,6 +818,14 @@ react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +react-router@^7.13.0: + version "7.13.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.13.0.tgz#de9484aee764f4f65b93275836ff5944d7f5bd3b" + integrity sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + react@^19.2.4: version "19.2.4" resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" @@ -830,6 +843,11 @@ scroll-into-view-if-needed@^3.1.0: dependencies: compute-scroll-into-view "^3.0.2" +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + string-convert@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"