feat(reader): P1 前端——首页影视/阅读切换 + 书架/搜索/书源/详情/阅读器

- 首页顶部「影视/阅读」分段切换(zustand 持久化,阅读模式直渲染书架)
- /reader/* 独立全屏路由(不套影视 Layout):
  书架(响应式网格 3→8 列)、搜索页(结果流+失败书源明细)、
  书源管理(导入/启停/删除/快速调试日志)、详情页(目录+加书架)
- 阅读器仿 legado ReadMenu:6 套内置主题(日/夜色值对齐 readConfig.json)、
  CSS 多栏分页 + 平移翻页/滚动双模式、三区点击 + 键盘翻页(桌面)、
  上下章、目录抽屉(Virtuoso 虚拟滚动)、字号/行距/段距调节、
  进度记忆(服务端同步 + 下一章预取)
- 桌面兼容:内容列 900px 限宽居中、鼠标悬停态、键盘快捷键
- 后端:详情/目录/正文接口支持 source_url 定位书源(书架链路)
This commit is contained in:
truewhile
2026-09-30 16:07:28 +08:00
parent b8e6418326
commit d0b4255aa8
15 changed files with 1763 additions and 12 deletions
+87
View File
@@ -0,0 +1,87 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
// 阅读主题(色值对齐 legado assets/defaultData/readConfig.json 的 6 套内置主题,
// 含日/夜两态,见 docs/reader-ui-spec.md 第 2/10 节)。
export interface ReaderTheme {
id: string
name: string
bg: string
text: string
accent: string
nightBg: string
nightText: string
nightAccent: string
}
export const READER_THEMES: ReaderTheme[] = [
{ id: 'weread', name: '微信读书', bg: '#C0EDC6', text: '#0B0B0B', accent: '#E53935', nightBg: '#000000', nightText: '#ADADAD', nightAccent: '#FE4D55' },
{ id: 'preset1', name: '预设1', bg: '#FFFFFF', text: '#000000', accent: '#E53935', nightBg: '#000000', nightText: '#FFFFFF', nightAccent: '#FE4D55' },
{ id: 'preset2', name: '羊皮纸', bg: '#DDC090', text: '#3E3422', accent: '#834E00', nightBg: '#3C3F43', nightText: '#DCDFE1', nightAccent: '#FE4D55' },
{ id: 'preset3', name: '护眼绿', bg: '#C2D8AA', text: '#596C44', accent: '#E53935', nightBg: '#3C3F43', nightText: '#88C16F', nightAccent: '#FE4D55' },
{ id: 'preset4', name: '粉紫', bg: '#DBB8E2', text: '#68516C', accent: '#801314', nightBg: '#3C3F43', nightText: '#F6AEAE', nightAccent: '#90BFF5' },
{ id: 'preset5', name: '淡蓝', bg: '#ABCEE0', text: '#3D4C54', accent: '#E53935', nightBg: '#3C3F43', nightText: '#90BFF5', nightAccent: '#FE4D55' },
]
export function getReaderTheme(themeId: string, night: boolean): { bg: string; text: string; accent: string } {
const t = READER_THEMES.find((x) => x.id === themeId) ?? READER_THEMES[1]
return night
? { bg: t.nightBg, text: t.nightText, accent: t.nightAccent }
: { bg: t.bg, text: t.text, accent: t.accent }
}
export type ReaderPageMode = 'page' | 'scroll'
export type ReaderHomeMode = 'media' | 'reading'
interface ReaderSettingsState {
// 首页模式切换(影视 / 阅读)
homeMode: ReaderHomeMode
setHomeMode: (mode: ReaderHomeMode) => void
themeId: string
setThemeId: (id: string) => void
night: boolean
toggleNight: () => void
pageMode: ReaderPageMode
setPageMode: (mode: ReaderPageMode) => void
fontSize: number
setFontSize: (size: number) => void
lineHeight: number
setLineHeight: (lh: number) => void
paragraphSpacing: number
setParagraphSpacing: (v: number) => void
}
export const useReaderSettingsStore = create<ReaderSettingsState>()(
persist(
(set) => ({
homeMode: 'media',
setHomeMode: (mode) => set({ homeMode: mode }),
themeId: 'preset1',
setThemeId: (themeId) => set({ themeId }),
night: false,
toggleNight: () => set((s) => ({ night: !s.night })),
pageMode: 'page',
setPageMode: (pageMode) => set({ pageMode }),
fontSize: 20,
setFontSize: (fontSize) => set({ fontSize: Math.min(32, Math.max(14, fontSize)) }),
lineHeight: 1.8,
setLineHeight: (lineHeight) => set({ lineHeight: Math.min(2.6, Math.max(1.4, Math.round(lineHeight * 10) / 10)) }),
paragraphSpacing: 8,
setParagraphSpacing: (paragraphSpacing) =>
set({ paragraphSpacing: Math.min(32, Math.max(0, paragraphSpacing)) }),
}),
{ name: 'mebox-reader-settings' },
),
)