Files
qihuaiflow/frontend/src/pages/front/NovelPreviewPage.tsx
T
qyyfw 78ff261b5a 初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物)
- 后端 Python FastAPI
- 数据库迁移 001-015
- Docker部署配置
- 微信ClawBot API频道绑定
- 项目文档 README.md
2026-07-02 14:40:46 +08:00

187 lines
8.2 KiB
TypeScript

import React, { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import type { Asset, NovelChapter } from '@/types';
import { mockAssets } from '@/mock/data';
import './NovelPreviewPage.css';
const NovelPreviewPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [activeChapter, setActiveChapter] = useState<string>('');
const [rightTab, setRightTab] = useState<'worldview' | 'characters' | 'hooks'>('worldview');
const novel: Asset | undefined = mockAssets.find(a => a.id === id && a.category === 'novel');
const chapters = novel?.chapters || [];
const meta = novel?.novelMeta;
const currentChapter: NovelChapter | undefined = chapters.find(c => c.id === (activeChapter || chapters[0]?.id));
if (!novel) {
return (
<div className="novel-preview-page">
<div className="novel-preview-empty">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--color-text-disabled)' }}><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<p>未找到该小说</p>
<button className="novel-back-btn" onClick={() => navigate('/assets')}>返回资产</button>
</div>
</div>
);
}
return (
<div className="novel-preview-page">
{/* Top: Novel Intro */}
<div className="novel-topbar">
<button className="novel-back-btn" onClick={() => navigate('/assets')}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
</button>
<div className="novel-intro">
<h1 className="novel-intro-title">{novel.name}</h1>
<p className="novel-intro-desc">{novel.description}</p>
<div className="novel-intro-stats">
<span>{chapters.length} 章</span>
<span>·</span>
<span>{chapters.reduce((s, c) => s + c.wordCount, 0).toLocaleString()} 字</span>
</div>
</div>
</div>
<div className="novel-body">
{/* Left: Chapter List */}
<aside className="novel-sidebar-left">
<h3 className="novel-sidebar-title">章节目录</h3>
<div className="novel-chapter-list">
{chapters.map(ch => (
<button
key={ch.id}
className={`novel-chapter-item ${(activeChapter || chapters[0]?.id) === ch.id ? 'active' : ''}`}
onClick={() => setActiveChapter(ch.id)}
>
<span className="chapter-item-num">{String(ch.sortOrder).padStart(2, '0')}</span>
<div className="chapter-item-info">
<span className="chapter-item-title">{ch.title}</span>
<span className="chapter-item-count">{ch.wordCount.toLocaleString()} 字</span>
</div>
</button>
))}
</div>
</aside>
{/* Center: Content */}
<main className="novel-content">
{currentChapter ? (
<>
<div className="novel-content-header">
<h2 className="novel-chapter-title">{currentChapter.title}</h2>
</div>
<div className="novel-content-body">
{currentChapter.content.split('\n\n').map((para, i) => (
<p key={i} className="novel-paragraph">{para}</p>
))}
</div>
<div className="novel-content-footer">
<span>本章字数:{currentChapter.wordCount.toLocaleString()} 字</span>
</div>
</>
) : (
<div className="novel-content-empty">
<p>暂无章节内容</p>
</div>
)}
</main>
{/* Right: World View, Characters, Hooks & Anchors */}
<aside className="novel-sidebar-right">
<div className="novel-right-tabs">
<button
className={`novel-right-tab ${rightTab === 'worldview' ? 'active' : ''}`}
onClick={() => setRightTab('worldview')}
>世界观</button>
<button
className={`novel-right-tab ${rightTab === 'characters' ? 'active' : ''}`}
onClick={() => setRightTab('characters')}
>人物</button>
<button
className={`novel-right-tab ${rightTab === 'hooks' ? 'active' : ''}`}
onClick={() => setRightTab('hooks')}
>钩子&锚点</button>
</div>
<div className="novel-right-content">
{rightTab === 'worldview' && meta?.worldView && (
<div className="novel-worldview">
<p className="worldview-summary">{meta.worldView.summary}</p>
<div className="worldview-section">
<h4>世界规则</h4>
<ul>
{meta.worldView.rules.map((r, i) => <li key={i}>{r}</li>)}
</ul>
</div>
<div className="worldview-section">
<h4>时间线</h4>
<div className="worldview-timeline">
{meta.worldView.timeline.map((t, i) => (
<div key={i} className="timeline-item">
<span className="timeline-era">{t.era}</span>
<span className="timeline-event">{t.events}</span>
</div>
))}
</div>
</div>
</div>
)}
{rightTab === 'characters' && meta?.characters && (
<div className="novel-characters">
{meta.characters.map(char => (
<div key={char.id} className="character-card">
<div className="character-avatar">{char.name.charAt(0)}</div>
<div className="character-info">
<div className="character-name-row">
<span className="character-name">{char.name}</span>
<span className={`character-role ${char.role === '主角' ? 'main' : char.role === '反派' ? 'villain' : ''}`}>{char.role}</span>
</div>
<p className="character-desc">{char.description}</p>
{char.relationships.length > 0 && (
<div className="character-rels">
{char.relationships.map((r, i) => (
<span key={i} className="character-rel-tag">{r}</span>
))}
</div>
)}
</div>
</div>
))}
</div>
)}
{rightTab === 'hooks' && meta && (
<div className="novel-hooks">
<div className="hooks-section">
<h4>故事钩子</h4>
{meta.hooks.map((h, i) => (
<div key={i} className="hook-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
<span>{h}</span>
</div>
))}
</div>
<div className="hooks-section">
<h4>故事锚点</h4>
{meta.anchors.map((a, i) => (
<div key={i} className="anchor-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
<span>{a}</span>
</div>
))}
</div>
</div>
)}
</div>
</aside>
</div>
</div>
);
};
export default NovelPreviewPage;