feat: add NovelPage component with bookshelf, open book, and reading views

This commit is contained in:
2026-06-02 08:19:38 +08:00
parent 54e17c9795
commit 81b0d0ea1f
39 changed files with 8057 additions and 64 deletions
+30 -5
View File
@@ -1,14 +1,16 @@
// frontend-react/src/App.jsx
import React, { useCallback, useEffect, useRef } from 'react'; // ✅ 移除 useState
import React, { useCallback, useEffect, useRef, useState } from 'react';
import TopBar from './components/TopBar';
import { ChatBox } from './components/Mid';
import SideBarLeft from './components/SideBarLeft';
import SideBarRight from './components/SideBarRight';
import PlaceholderPage from './components/PlaceholderPage';
import NovelPage from './components/Novel/NovelPage';
import StudioEditPage from './components/Studio/StudioEditPage';
import StudioRunPage from './components/Studio/StudioRunPage';
import useAppLayoutStore from './Store/AppLayoutSlice'; // ✅ 新增
import useStudioStore from './Store/Studio/StudioSlice';
import useNovelStore from './Store/Novel/NovelSlice';
import useApiConfigStore from './Store/SideBarLeft/ApiConfigSlice'; // ✅ 引入 API 配置 Store
import usePresetStore from './Store/SideBarLeft/PresetSlice'; // ✅ 引入预设 Store
import useCharacterStore from './Store/SideBarLeft/CharacterSlice'; // ✅ 引入角色卡 Store
@@ -194,22 +196,41 @@ function App() {
const initStudio = useStudioStore((s) => s.initStudio);
const initStudioRun = useStudioStore((s) => s.initStudioRun);
const initNovel = useNovelStore((s) => s.initNovel);
const novelView = useNovelStore((s) => s.view);
const readingChromeVisible = useNovelStore((s) => s.readingChromeVisible);
const isStudioEditPage = activePage === 'studio_edit';
const isStudioRunPage = activePage === 'studio_run';
const isNovelPage = activePage === 'novel';
const isStudioPage = isStudioEditPage || isStudioRunPage;
const [isMobileViewport, setIsMobileViewport] = useState(() =>
typeof window !== 'undefined' && window.matchMedia('(max-width: 768px)').matches
);
useEffect(() => {
const mq = window.matchMedia('(max-width: 768px)');
const onChange = (e) => setIsMobileViewport(e.matches);
mq.addEventListener('change', onChange);
return () => mq.removeEventListener('change', onChange);
}, []);
const hideTopBar =
isNovelPage && novelView === 'reading' && isMobileViewport && !readingChromeVisible;
useEffect(() => {
if (isStudioEditPage) {
initStudio();
} else if (isStudioRunPage) {
initStudioRun();
} else if (isNovelPage) {
initNovel();
}
}, [isStudioEditPage, isStudioRunPage, initStudio, initStudioRun]);
}, [isStudioEditPage, isStudioRunPage, isNovelPage, initStudio, initStudioRun, initNovel]);
return (
<div className={`app ${layoutMode}-mode`}>
{/* ✅ TopBar 不再需要 props,直接从 Store 读取状态 */}
<TopBar />
<div className={`app ${layoutMode}-mode${hideTopBar ? ' novel-reading-immersive' : ''}`}>
{!hideTopBar && <TopBar />}
{/* 主内容容器 */}
{activePage === 'chat' ? (
@@ -243,6 +264,10 @@ function App() {
<div className="main-container studio-container">
<StudioRunPage />
</div>
) : activePage === 'novel' ? (
<div className="main-container novel-container">
<NovelPage />
</div>
) : (
<div className="main-container placeholder-container">
<PlaceholderPage page={activePage} />
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+37 -9
View File
@@ -17,8 +17,21 @@
padding: 0 var(--spacing-lg);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
gap: var(--spacing-md);
min-width: 0;
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: var(--color-scrollbar) transparent;
}
.top-bar-content::-webkit-scrollbar {
height: 6px;
}
.top-bar-content::-webkit-scrollbar-thumb {
background: var(--color-scrollbar);
border-radius: var(--radius-full);
}
/* Status Section - Left side */
@@ -26,10 +39,7 @@
display: flex;
align-items: center;
gap: var(--spacing-md);
flex: 1;
justify-content: flex-start;
min-width: 0;
overflow-x: auto;
flex-shrink: 0;
}
/* Actions Section - Right side */
@@ -38,6 +48,7 @@
align-items: center;
gap: var(--spacing-sm);
flex-shrink: 0;
margin-left: auto;
}
/* Status Badge Styles - Minimalist */
@@ -87,21 +98,25 @@
white-space: nowrap;
}
/* Action Button Styles - Badge-like (Similar to status-badge) */
/* Action Button Styles — compact square, matches theme toggle */
.action-btn {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm) var(--spacing-md);
width: 32px;
height: 32px;
min-width: 32px;
min-height: 32px;
padding: 0;
background-color: transparent;
color: var(--color-text-secondary);
border: none;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
cursor: pointer;
font-size: 1.4rem; /* 更大的图标 */
font-size: 1rem;
line-height: 1;
min-height: 36px; /* 与 status-badge 一致 */
flex-shrink: 0;
}
.action-btn:hover {
@@ -665,3 +680,16 @@
border-color: var(--color-text-muted);
color: var(--color-text-primary);
}
/* Mobile: hide TopBar horizontal scrollbar (keep swipe scroll) */
@media (max-width: 768px) {
.top-bar-content {
scrollbar-width: none;
-ms-overflow-style: none;
}
.top-bar-content::-webkit-scrollbar {
display: none;
height: 0;
}
}
+93 -47
View File
@@ -7,7 +7,8 @@ import useApiConfigStore from '../../Store/SideBarLeft/ApiConfigSlice';
import PageModeToggle from './items/PageModeToggle';
import StudioRunControls from './items/StudioRunControls';
import ThemeToggle from './items/ThemeToggle';
import RegexPanel from '../SideBarLeft/tabs/Regex/RegexPanel'; // ✅ 新增:导入正则管理组件
import RegexPanel from '../SideBarLeft/tabs/Regex/RegexPanel';
import ApiConfig from '../SideBarLeft/tabs/ApiConfig/ApiConfig';
import './TopBar.css';
const Toolbar = () => {
@@ -24,6 +25,7 @@ const Toolbar = () => {
} = useAppLayoutStore();
const isStudioRunPage = activePage === 'studio_run';
const isNovelPage = activePage === 'novel';
// ✅ 从 UserStore 获取用户角色和方法
const { currentUserRole, userRoles, updateRoleName, updateRoleDescription, selectUserRole, addUserRole } = useUserStore();
@@ -32,9 +34,16 @@ const Toolbar = () => {
const { globalWorldBooks } = useWorldBookStore();
// 从 ApiConfigStore 获取核心和辅助 API 配置
const { activeMap, fetchProfile } = useApiConfigStore();
const { currentProfile } = useApiConfigStore();
const [coreModel, setCoreModel] = useState('未设置');
const [assistModel, setAssistModel] = useState('未设置');
useEffect(() => {
const main = currentProfile?.apis?.mainLLM;
const secondary = currentProfile?.apis?.secondaryLLM;
setCoreModel(main?.model || main?.name || '未设置');
setAssistModel(secondary?.model || secondary?.name || '未设置');
}, [currentProfile]);
// 系统设置状态
const [settings, setSettings] = useState({
@@ -155,64 +164,84 @@ const Toolbar = () => {
<div className="status-section">
{isStudioRunPage ? <StudioRunControls /> : null}
{/* 当前玩家角色 */}
<div
className="status-badge"
title="当前玩家角色"
onClick={() => handlePanelToggle('currentRole')}
>
<span className="status-icon">😊</span>
<span className="status-label">{truncateText(currentUserRole.name || '未设置', 15)}</span>
</div>
{!isNovelPage && (
<>
{/* 当前玩家角色 */}
<div
className="status-badge"
title="当前玩家角色"
onClick={() => handlePanelToggle('currentRole')}
>
<span className="status-icon">😊</span>
<span className="status-label">{truncateText(currentUserRole.name || '未设置', 15)}</span>
</div>
</>
)}
{/* 核心配置 */}
<div className="status-badge" title={`核心配置: ${coreModel}`}>
<div
className="status-badge"
title={`核心配置: ${coreModel}`}
onClick={isNovelPage ? () => handlePanelToggle('apiConfig') : undefined}
>
<span className="status-icon">🔌</span>
<span className="status-label">{truncateText(coreModel, 12)}</span>
</div>
{/* 辅助配置 */}
<div className="status-badge" title={`辅助配置: ${assistModel}`}>
<div
className="status-badge"
title={`辅助配置: ${assistModel}`}
onClick={isNovelPage ? () => handlePanelToggle('apiConfig') : undefined}
>
<span className="status-icon">🔗</span>
<span className="status-label">{truncateText(assistModel, 12)}</span>
</div>
{/* 当前预设 */}
<div className="status-badge" title="当前预设名">
<span className="status-icon">⚙️</span>
<span className="status-label">默认预设</span>
</div>
{/* 激活的世界书 */}
<div className="status-badge world-info-badge" title="已激活全局世界书" onClick={() => handlePanelToggle('worldBook')}>
<span className="status-icon">📚</span>
<span className="status-label">
{globalWorldBooks.length > 0
? globalWorldBooks.map(wb => wb.name).join(', ')
: '无'}
</span>
</div>
{!isNovelPage && (
<>
{/* 当前预设 */}
<div className="status-badge" title="当前预设名">
<span className="status-icon">⚙️</span>
<span className="status-label">默认预设</span>
</div>
{/* 激活的世界书 */}
<div className="status-badge world-info-badge" title="已激活全局世界书" onClick={() => handlePanelToggle('worldBook')}>
<span className="status-icon">📚</span>
<span className="status-label">
{globalWorldBooks.length > 0
? globalWorldBooks.map(wb => wb.name).join(', ')
: '无'}
</span>
</div>
</>
)}
</div>
{/* 右侧:操作按钮区域 */}
<div className="actions-section">
{/* 设置按钮 */}
<button
className="action-btn"
title="设置"
onClick={() => handlePanelToggle('settings')}
>
⚙
</button>
{/* 扩展按钮 */}
<button
className="action-btn"
title="扩展"
onClick={() => handlePanelToggle('extensions')}
>
⊞
</button>
{!isNovelPage && (
<>
{/* 设置按钮 */}
<button
className="action-btn"
title="设置"
onClick={() => handlePanelToggle('settings')}
>
⚙
</button>
{/* 扩展按钮 */}
<button
className="action-btn"
title="扩展"
onClick={() => handlePanelToggle('extensions')}
>
⊞
</button>
</>
)}
{/* 页面模式 */}
<PageModeToggle />
@@ -223,6 +252,23 @@ const Toolbar = () => {
</div>
</div>
{/* API 配置面板(爽文模式) */}
{activePanel === 'apiConfig' && (
<div className="panel-overlay" ref={panelRef}>
<div className="panel-content settings-panel">
<div className="panel-header">
<h3>API 配置</h3>
<button className="close-panel-button" onClick={handleClosePanel} title="关闭">
✕
</button>
</div>
<div className="panel-body" style={{ padding: 0 }}>
<ApiConfig />
</div>
</div>
</div>
)}
{/* ✅ 用户角色设置面板 */}
{activePanel === 'currentRole' && (
<div className="panel-overlay" ref={panelRef}>
+9 -2
View File
@@ -28,15 +28,22 @@
}
.main-container.placeholder-container,
.main-container.studio-container {
.main-container.studio-container,
.main-container.novel-container {
flex-direction: column;
}
.main-container.studio-container {
.main-container.studio-container,
.main-container.novel-container {
display: flex;
min-height: 0;
}
.app.novel-reading-immersive .main-container.novel-container {
flex: 1;
min-height: 0;
}
/* Smooth transitions for theme changes - only apply to specific properties */
.app {
transition: background-color var(--transition-normal),