Files
ai-novel/src/pages/NovelList.tsx
2026-04-16 21:32:21 +08:00

414 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;