feat: add NovelPage component with bookshelf, open book, and reading views
This commit is contained in:
@@ -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} />
|
||||
|
||||
1107
frontend/src/Store/Novel/NovelSlice.jsx
Normal file
1107
frontend/src/Store/Novel/NovelSlice.jsx
Normal file
File diff suppressed because it is too large
Load Diff
1041
frontend/src/components/Novel/NovelPage.css
Normal file
1041
frontend/src/components/Novel/NovelPage.css
Normal file
File diff suppressed because it is too large
Load Diff
1043
frontend/src/components/Novel/NovelPage.jsx
Normal file
1043
frontend/src/components/Novel/NovelPage.jsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user