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' + /> +
+
+ +
+ +