414 lines
13 KiB
TypeScript
414 lines
13 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|||
|
|
import { Card, Button, Modal, Form, Input, message, Popconfirm, Row, Col, Tag, Select } from 'antd';
|
|||
|
|
import { PlusOutlined, EditOutlined, DeleteOutlined, BookOutlined, EyeOutlined } from '@ant-design/icons';
|
|||
|
|
import { useNavigate } from 'react-router-dom';
|
|||
|
|
import { storage } from '../utils/indexedDB';
|
|||
|
|
import { Novel } from '../types';
|
|||
|
|
|
|||
|
|
const { TextArea } = Input;
|
|||
|
|
const { Option } = Select;
|
|||
|
|
|
|||
|
|
// 小说题材选项
|
|||
|
|
const NOVEL_GENRES = [
|
|||
|
|
'穿越',
|
|||
|
|
'都市',
|
|||
|
|
'修仙',
|
|||
|
|
'武侠',
|
|||
|
|
'玄幻',
|
|||
|
|
'科幻',
|
|||
|
|
'言情',
|
|||
|
|
'历史',
|
|||
|
|
'游戏',
|
|||
|
|
'灵异',
|
|||
|
|
'军事',
|
|||
|
|
'悬疑',
|
|||
|
|
'其他'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const NovelList: React.FC = () => {
|
|||
|
|
const [novels, setNovels] = useState<Novel[]>([]);
|
|||
|
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
|||
|
|
const [viewModalVisible, setViewModalVisible] = useState(false);
|
|||
|
|
const [editingNovel, setEditingNovel] = useState<Novel | null>(null);
|
|||
|
|
const [viewingNovel, setViewingNovel] = useState<Novel | null>(null);
|
|||
|
|
const [form] = Form.useForm();
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadNovels();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const loadNovels = async () => {
|
|||
|
|
try {
|
|||
|
|
const loadedNovels = await storage.getNovels();
|
|||
|
|
setNovels(loadedNovels);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载小说列表失败:', error);
|
|||
|
|
message.error('加载小说列表失败');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleAddNovel = () => {
|
|||
|
|
setEditingNovel(null);
|
|||
|
|
form.resetFields();
|
|||
|
|
setIsModalVisible(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleViewNovel = (novel: Novel) => {
|
|||
|
|
setViewingNovel(novel);
|
|||
|
|
setViewModalVisible(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleEditNovel = (novel: Novel) => {
|
|||
|
|
setEditingNovel(novel);
|
|||
|
|
form.setFieldsValue({
|
|||
|
|
title: novel.title,
|
|||
|
|
genre: novel.genre === '未分类' ? undefined : novel.genre,
|
|||
|
|
outline: novel.outline
|
|||
|
|
});
|
|||
|
|
setIsModalVisible(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDeleteNovel = async (id: string) => {
|
|||
|
|
try {
|
|||
|
|
await storage.deleteNovel(id);
|
|||
|
|
await loadNovels();
|
|||
|
|
message.success('小说删除成功');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('删除小说失败:', error);
|
|||
|
|
message.error('删除失败,请重试');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleModalOk = async () => {
|
|||
|
|
try {
|
|||
|
|
const values = await form.validateFields();
|
|||
|
|
|
|||
|
|
if (editingNovel) {
|
|||
|
|
await storage.updateNovel(editingNovel.id, {
|
|||
|
|
...values,
|
|||
|
|
genre: values.genre || '未分类'
|
|||
|
|
});
|
|||
|
|
message.success('小说更新成功');
|
|||
|
|
} else {
|
|||
|
|
const newNovel: Novel = {
|
|||
|
|
id: Date.now().toString(),
|
|||
|
|
title: values.title,
|
|||
|
|
genre: values.genre || '未分类',
|
|||
|
|
outline: values.outline || '',
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
updatedAt: new Date().toISOString()
|
|||
|
|
};
|
|||
|
|
await storage.addNovel(newNovel);
|
|||
|
|
message.success('小说创建成功,请前往AI生成页面完善设定');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await loadNovels();
|
|||
|
|
setIsModalVisible(false);
|
|||
|
|
form.resetFields();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('保存小说失败:', error);
|
|||
|
|
message.error('保存失败,请重试');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleModalCancel = () => {
|
|||
|
|
setIsModalVisible(false);
|
|||
|
|
form.resetFields();
|
|||
|
|
setEditingNovel(null);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleCardClick = (id: string) => {
|
|||
|
|
navigate(`/novels/${id}`);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<div style={{ marginBottom: '24px' }}>
|
|||
|
|
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 600, color: '#262626' }}>我的小说</h2>
|
|||
|
|
<p style={{ margin: '8px 0 0 0', color: '#8c8c8c', fontSize: '14px' }}>管理和创作您的 AI 小说作品</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
|||
|
|
<div style={{ fontSize: '14px', color: '#595959' }}>
|
|||
|
|
共 {novels.length} 部小说
|
|||
|
|
</div>
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
icon={<PlusOutlined />}
|
|||
|
|
onClick={handleAddNovel}
|
|||
|
|
size="large"
|
|||
|
|
style={{ borderRadius: '6px' }}
|
|||
|
|
>
|
|||
|
|
新建小说
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{novels.length === 0 ? (
|
|||
|
|
<div style={{
|
|||
|
|
textAlign: 'center',
|
|||
|
|
padding: '80px 20px',
|
|||
|
|
background: 'white',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px dashed #d9d9d9'
|
|||
|
|
}}>
|
|||
|
|
<BookOutlined style={{ fontSize: '48px', color: '#d9d9d9', marginBottom: '16px' }} />
|
|||
|
|
<div style={{ fontSize: '16px', color: '#595959', marginBottom: '8px' }}>
|
|||
|
|
还没有小说,开始创作您的第一部作品吧!
|
|||
|
|
</div>
|
|||
|
|
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>
|
|||
|
|
点击"新建小说"按钮开始您的创作之旅
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<Row gutter={[16, 16]}>
|
|||
|
|
{novels.map((novel) => (
|
|||
|
|
<Col xs={24} sm={12} md={8} lg={6} key={novel.id}>
|
|||
|
|
<Card
|
|||
|
|
hoverable
|
|||
|
|
style={{
|
|||
|
|
height: '100%',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid #f0f0f0',
|
|||
|
|
transition: 'all 0.3s ease'
|
|||
|
|
}}
|
|||
|
|
onClick={() => handleCardClick(novel.id)}
|
|||
|
|
actions={[
|
|||
|
|
<EyeOutlined
|
|||
|
|
key="view"
|
|||
|
|
style={{ color: '#52c41a' }}
|
|||
|
|
onClick={(e) => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
handleViewNovel(novel);
|
|||
|
|
}}
|
|||
|
|
/>,
|
|||
|
|
<EditOutlined
|
|||
|
|
key="edit"
|
|||
|
|
style={{ color: '#1890ff' }}
|
|||
|
|
onClick={(e) => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
handleEditNovel(novel);
|
|||
|
|
}}
|
|||
|
|
/>,
|
|||
|
|
<Popconfirm
|
|||
|
|
title="确认删除"
|
|||
|
|
description="确定要删除这部小说吗?所有章节内容也将被删除。"
|
|||
|
|
onConfirm={(e) => {
|
|||
|
|
e?.stopPropagation();
|
|||
|
|
handleDeleteNovel(novel.id);
|
|||
|
|
}}
|
|||
|
|
onCancel={(e) => e?.stopPropagation()}
|
|||
|
|
okText="确定"
|
|||
|
|
cancelText="取消"
|
|||
|
|
>
|
|||
|
|
<DeleteOutlined
|
|||
|
|
key="delete"
|
|||
|
|
style={{ color: '#ff4d4f' }}
|
|||
|
|
onClick={(e) => e.stopPropagation()}
|
|||
|
|
/>
|
|||
|
|
</Popconfirm>
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<Card.Meta
|
|||
|
|
avatar={
|
|||
|
|
<div style={{
|
|||
|
|
width: '40px',
|
|||
|
|
height: '40px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
background: '#e6f7ff',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center'
|
|||
|
|
}}>
|
|||
|
|
<BookOutlined style={{ fontSize: '20px', color: '#1890ff' }} />
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
title={
|
|||
|
|
<div style={{
|
|||
|
|
fontSize: '16px',
|
|||
|
|
fontWeight: 500,
|
|||
|
|
color: '#262626',
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
textOverflow: 'ellipsis',
|
|||
|
|
whiteSpace: 'nowrap'
|
|||
|
|
}}>
|
|||
|
|
{novel.title}
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
description={
|
|||
|
|
<div>
|
|||
|
|
<div style={{ marginBottom: '8px' }}>
|
|||
|
|
<Tag color="blue">{novel.genre}</Tag>
|
|||
|
|
{novel.generatedSettings && (
|
|||
|
|
<Tag color="green">已完善设定</Tag>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div style={{
|
|||
|
|
fontSize: '12px',
|
|||
|
|
color: '#8c8c8c',
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
textOverflow: 'ellipsis',
|
|||
|
|
display: '-webkit-box',
|
|||
|
|
WebkitLineClamp: 2,
|
|||
|
|
WebkitBoxOrient: 'vertical',
|
|||
|
|
lineHeight: '1.5'
|
|||
|
|
}}>
|
|||
|
|
{novel.outline || '暂无大纲'}
|
|||
|
|
</div>
|
|||
|
|
<div style={{ marginTop: '8px', fontSize: '11px', color: '#bfbfbf' }}>
|
|||
|
|
创建于 {new Date(novel.createdAt).toLocaleDateString()}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</Card>
|
|||
|
|
</Col>
|
|||
|
|
))}
|
|||
|
|
</Row>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<Modal
|
|||
|
|
title={editingNovel ? '编辑小说' : '新建小说'}
|
|||
|
|
open={isModalVisible}
|
|||
|
|
onOk={handleModalOk}
|
|||
|
|
onCancel={handleModalCancel}
|
|||
|
|
width={600}
|
|||
|
|
okText="确定"
|
|||
|
|
cancelText="取消"
|
|||
|
|
>
|
|||
|
|
<Form
|
|||
|
|
form={form}
|
|||
|
|
layout="vertical"
|
|||
|
|
>
|
|||
|
|
<Form.Item
|
|||
|
|
label="书名"
|
|||
|
|
name="title"
|
|||
|
|
rules={[{ required: true, message: '请输入书名' }]}
|
|||
|
|
>
|
|||
|
|
<Input placeholder="请输入书名" />
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<Form.Item
|
|||
|
|
label="题材类型"
|
|||
|
|
name="genre"
|
|||
|
|
rules={[{ required: true, message: '请选择题材类型' }]}
|
|||
|
|
>
|
|||
|
|
<Select
|
|||
|
|
placeholder="请选择题材类型"
|
|||
|
|
size="large"
|
|||
|
|
showSearch
|
|||
|
|
allowClear
|
|||
|
|
>
|
|||
|
|
{NOVEL_GENRES.map(genre => (
|
|||
|
|
<Option key={genre} value={genre}>
|
|||
|
|
{genre}
|
|||
|
|
</Option>
|
|||
|
|
))}
|
|||
|
|
</Select>
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<Form.Item
|
|||
|
|
label="初步想法"
|
|||
|
|
name="outline"
|
|||
|
|
extra="可以简单描述您的初步想法,也可以留空,后续让AI帮您生成"
|
|||
|
|
>
|
|||
|
|
<TextArea
|
|||
|
|
rows={4}
|
|||
|
|
placeholder="简单描述您的创作想法..."
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
</Form>
|
|||
|
|
</Modal>
|
|||
|
|
|
|||
|
|
<Modal
|
|||
|
|
title="小说详情"
|
|||
|
|
open={viewModalVisible}
|
|||
|
|
onCancel={() => setViewModalVisible(false)}
|
|||
|
|
width={700}
|
|||
|
|
footer={[
|
|||
|
|
<Button key="close" onClick={() => setViewModalVisible(false)}>
|
|||
|
|
关闭
|
|||
|
|
</Button>
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{viewingNovel && (
|
|||
|
|
<div style={{ padding: '16px 0' }}>
|
|||
|
|
<div style={{ marginBottom: '24px' }}>
|
|||
|
|
<div style={{ fontSize: '14px', color: '#595959', marginBottom: '8px' }}>
|
|||
|
|
书名
|
|||
|
|
</div>
|
|||
|
|
<div style={{
|
|||
|
|
padding: '12px',
|
|||
|
|
background: '#f5f5f5',
|
|||
|
|
borderRadius: '6px',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
fontWeight: 500
|
|||
|
|
}}>
|
|||
|
|
{viewingNovel.title}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '24px' }}>
|
|||
|
|
<div style={{ fontSize: '14px', color: '#595959', marginBottom: '8px' }}>
|
|||
|
|
题材
|
|||
|
|
</div>
|
|||
|
|
<div style={{
|
|||
|
|
padding: '12px',
|
|||
|
|
background: '#f5f5f5',
|
|||
|
|
borderRadius: '6px'
|
|||
|
|
}}>
|
|||
|
|
<Tag color="blue">{viewingNovel.genre}</Tag>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{viewingNovel.generatedSettings && (
|
|||
|
|
<>
|
|||
|
|
<div style={{ marginBottom: '24px' }}>
|
|||
|
|
<div style={{ fontSize: '14px', color: '#595959', marginBottom: '8px' }}>
|
|||
|
|
AI生成设定
|
|||
|
|
</div>
|
|||
|
|
<div style={{
|
|||
|
|
padding: '12px',
|
|||
|
|
background: '#f0f9ff',
|
|||
|
|
borderRadius: '6px',
|
|||
|
|
marginBottom: '12px'
|
|||
|
|
}}>
|
|||
|
|
<div><strong>目标字数:</strong>{viewingNovel.generatedSettings.targetWordCount}字</div>
|
|||
|
|
<div><strong>章节数量:</strong>{viewingNovel.generatedSettings.chapterCount}章</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '24px' }}>
|
|||
|
|
<div style={{ fontSize: '14px', color: '#595959', marginBottom: '8px' }}>
|
|||
|
|
大纲
|
|||
|
|
</div>
|
|||
|
|
<div style={{
|
|||
|
|
padding: '12px',
|
|||
|
|
background: '#f5f5f5',
|
|||
|
|
borderRadius: '6px',
|
|||
|
|
lineHeight: '1.8',
|
|||
|
|
maxHeight: '200px',
|
|||
|
|
overflowY: 'auto'
|
|||
|
|
}}>
|
|||
|
|
{viewingNovel.outline || '暂无大纲'}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginTop: '24px', paddingTop: '16px', borderTop: '1px solid #f0f0f0' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>
|
|||
|
|
创建时间:{new Date(viewingNovel.createdAt).toLocaleString()}
|
|||
|
|
</div>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#8c8c8c', marginTop: '4px' }}>
|
|||
|
|
更新时间:{new Date(viewingNovel.updatedAt).toLocaleString()}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</Modal>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default NovelList;
|