From cb225c3291bac8b4986ebc6e50092606f960c36d Mon Sep 17 00:00:00 2001 From: mtvpls Date: Thu, 16 Apr 2026 16:01:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=BC=AB=E7=94=BB=E5=B1=95?= =?UTF-8?q?=E9=A6=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + migrations/006_manga.sql | 38 +++ migrations/postgres/006_manga.sql | 38 +++ src/app/admin/page.tsx | 190 +++++++++++- src/app/api/manga/_utils.ts | 23 ++ src/app/api/manga/detail/route.ts | 37 +++ src/app/api/manga/history/route.ts | 90 ++++++ src/app/api/manga/image/route.ts | 65 ++++ src/app/api/manga/pages/route.ts | 24 ++ src/app/api/manga/search/route.ts | 28 ++ src/app/api/manga/shelf/route.ts | 83 ++++++ src/app/api/manga/sources/route.ts | 20 ++ src/app/layout.tsx | 7 + src/app/manga/detail/page.tsx | 180 ++++++++++++ src/app/manga/history/page.tsx | 46 +++ src/app/manga/layout.tsx | 9 + src/app/manga/page.tsx | 143 +++++++++ src/app/manga/read/page.tsx | 423 +++++++++++++++++++++++++++ src/app/manga/shelf/page.tsx | 63 ++++ src/app/page.tsx | 22 +- src/components/MangaCard.tsx | 53 ++++ src/components/MangaSectionNav.tsx | 35 +++ src/components/manga/MangaLayout.tsx | 182 ++++++++++++ src/lib/admin.types.ts | 8 + src/lib/config.ts | 29 ++ src/lib/d1.db.ts | 265 +++++++++++++++++ src/lib/db.client.ts | 326 ++++++++++++++++++++- src/lib/db.ts | 35 +++ src/lib/manga.types.ts | 62 ++++ src/lib/postgres.db.ts | 265 +++++++++++++++++ src/lib/redis-base.db.ts | 72 +++++ src/lib/suwayomi.client.ts | 371 +++++++++++++++++++++++ src/lib/types.ts | 14 + 33 files changed, 3242 insertions(+), 5 deletions(-) create mode 100644 migrations/006_manga.sql create mode 100644 migrations/postgres/006_manga.sql create mode 100644 src/app/api/manga/_utils.ts create mode 100644 src/app/api/manga/detail/route.ts create mode 100644 src/app/api/manga/history/route.ts create mode 100644 src/app/api/manga/image/route.ts create mode 100644 src/app/api/manga/pages/route.ts create mode 100644 src/app/api/manga/search/route.ts create mode 100644 src/app/api/manga/shelf/route.ts create mode 100644 src/app/api/manga/sources/route.ts create mode 100644 src/app/manga/detail/page.tsx create mode 100644 src/app/manga/history/page.tsx create mode 100644 src/app/manga/layout.tsx create mode 100644 src/app/manga/page.tsx create mode 100644 src/app/manga/read/page.tsx create mode 100644 src/app/manga/shelf/page.tsx create mode 100644 src/components/MangaCard.tsx create mode 100644 src/components/MangaSectionNav.tsx create mode 100644 src/components/manga/MangaLayout.tsx create mode 100644 src/lib/manga.types.ts create mode 100644 src/lib/suwayomi.client.ts diff --git a/.gitignore b/.gitignore index f45089d..abcf28e 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ public/workbox-*.js.map # local scripts scripts/tvbox/ +scripts/test \ No newline at end of file diff --git a/migrations/006_manga.sql b/migrations/006_manga.sql new file mode 100644 index 0000000..941636c --- /dev/null +++ b/migrations/006_manga.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS manga_shelf ( + username TEXT NOT NULL, + key TEXT NOT NULL, + source_id TEXT NOT NULL, + source_name TEXT NOT NULL, + manga_id TEXT NOT NULL, + title TEXT NOT NULL, + cover TEXT, + save_time INTEGER NOT NULL, + description TEXT, + author TEXT, + status TEXT, + last_chapter_id TEXT, + last_chapter_name TEXT, + PRIMARY KEY (username, key), + FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_manga_shelf_user_time ON manga_shelf(username, save_time DESC); + +CREATE TABLE IF NOT EXISTS manga_read_records ( + username TEXT NOT NULL, + key TEXT NOT NULL, + source_id TEXT NOT NULL, + source_name TEXT NOT NULL, + manga_id TEXT NOT NULL, + title TEXT NOT NULL, + cover TEXT, + chapter_id TEXT NOT NULL, + chapter_name TEXT NOT NULL, + page_index INTEGER NOT NULL, + page_count INTEGER NOT NULL, + save_time INTEGER NOT NULL, + PRIMARY KEY (username, key), + FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_manga_read_records_user_time ON manga_read_records(username, save_time DESC); diff --git a/migrations/postgres/006_manga.sql b/migrations/postgres/006_manga.sql new file mode 100644 index 0000000..d4af337 --- /dev/null +++ b/migrations/postgres/006_manga.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS manga_shelf ( + username TEXT NOT NULL, + key TEXT NOT NULL, + source_id TEXT NOT NULL, + source_name TEXT NOT NULL, + manga_id TEXT NOT NULL, + title TEXT NOT NULL, + cover TEXT, + save_time BIGINT NOT NULL, + description TEXT, + author TEXT, + status TEXT, + last_chapter_id TEXT, + last_chapter_name TEXT, + PRIMARY KEY (username, key), + FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_manga_shelf_user_time ON manga_shelf(username, save_time DESC); + +CREATE TABLE IF NOT EXISTS manga_read_records ( + username TEXT NOT NULL, + key TEXT NOT NULL, + source_id TEXT NOT NULL, + source_name TEXT NOT NULL, + manga_id TEXT NOT NULL, + title TEXT NOT NULL, + cover TEXT, + chapter_id TEXT NOT NULL, + chapter_name TEXT NOT NULL, + page_index INTEGER NOT NULL, + page_count INTEGER NOT NULL, + save_time BIGINT NOT NULL, + PRIMARY KEY (username, key), + FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_manga_read_records_user_time ON manga_read_records(username, save_time DESC); diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 62aacda..23bc5c7 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -24,6 +24,7 @@ import { CSS } from '@dnd-kit/utilities'; import { AlertCircle, AlertTriangle, + BookOpen, Bot, Cat, Check, @@ -10114,6 +10115,181 @@ const CustomAdFilterConfig = ({ }; // 小雅配置组件 + +const SuwayomiConfigComponent = ({ + config, + refreshConfig, +}: { + config: AdminConfig | null; + refreshConfig: () => Promise; +}) => { + const { alertModal, showAlert, hideAlert } = useAlertModal(); + const { isLoading, withLoading } = useLoadingState(); + const [enabled, setEnabled] = useState(false); + const [serverURL, setServerURL] = useState(''); + const [authToken, setAuthToken] = useState(''); + const [defaultLang, setDefaultLang] = useState('zh'); + const [sourceIds, setSourceIds] = useState(''); + const [maxSources, setMaxSources] = useState(10); + + useEffect(() => { + if (config?.SuwayomiConfig) { + setEnabled(config.SuwayomiConfig.Enabled || false); + setServerURL(config.SuwayomiConfig.ServerURL || ''); + setAuthToken(config.SuwayomiConfig.AuthToken || ''); + setDefaultLang(config.SuwayomiConfig.DefaultLang || 'zh'); + setSourceIds((config.SuwayomiConfig.SourceIds || []).join(',')); + setMaxSources(config.SuwayomiConfig.MaxSources || 10); + } + }, [config]); + + const buildConfig = () => ({ + Enabled: enabled, + ServerURL: serverURL, + AuthToken: authToken, + DefaultLang: defaultLang || 'zh', + SourceIds: sourceIds.split(',').map((item) => item.trim()).filter(Boolean), + MaxSources: Math.max(1, maxSources || 10), + }); + + const handleSave = async () => { + await withLoading('saveSuwayomi', async () => { + try { + if (!config) throw new Error('配置未加载'); + + const response = await fetch('/api/admin/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...config, + SuwayomiConfig: buildConfig(), + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || '保存失败'); + } + + showSuccess('漫画后端配置已保存', showAlert); + await refreshConfig(); + } catch (error) { + showError(error instanceof Error ? error.message : '保存失败', showAlert); + throw error; + } + }); + }; + + return ( +
+
+

+ 关于漫画展馆 / Suwayomi +

+
+

• 漫画展馆通过 Suwayomi Server 的 GraphQL 接口搜索、拉取章节与阅读页。

+

• 可限制默认语言、可用源白名单,以及单次搜索最多查询的源数量。

+

• 保存后漫画模块会优先使用这里的配置,环境变量只作为兜底。

+
+
+ +
+
+
+

启用漫画展馆

+

关闭后仍保留代码,但不建议在未配置时对用户开放入口。

+
+ +
+ +
+ + setServerURL(e.target.value)} + placeholder='http://127.0.0.1:4567' + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' + /> +

只填服务根地址,程序会自动拼接 /api/graphql。

+
+ +
+ + setAuthToken(e.target.value)} + placeholder='可选,若 Suwayomi 开启认证请填写' + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' + /> +
+ +
+
+ + setDefaultLang(e.target.value)} + placeholder='zh' + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' + /> +
+
+ + setMaxSources(parseInt(e.target.value) || 10)} + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' + /> +
+
+ +
+ +