Files
MuMuAINovel/frontend/src/pages/WritingStyles.tsx
T
xiamuceer 1cde345ed9 1.优化AI请求替换OpenAI SDK调用,使用httpx和自定义头请求,避免触发部分公益站的cloudflare
2.修复deepseek模型调用问题,舍弃思考过程AI响应内容,只获取结果内容
3.新增会话过期机制,更新后添加到.env中
4.支持用户在生成章节内容时设置字数
2025-11-03 15:28:51 +08:00

436 lines
14 KiB
TypeScript

import { useState, useEffect } from 'react';
import {
Button,
Modal,
Form,
Input,
message,
Card,
Space,
Tag,
Popconfirm,
Empty,
Typography,
Row,
Col,
Tooltip
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
StarOutlined,
StarFilled
} from '@ant-design/icons';
import { useStore } from '../store';
import { writingStyleApi } from '../services/api';
import type { WritingStyle, WritingStyleCreate, WritingStyleUpdate } from '../types';
const { TextArea } = Input;
const { Text, Paragraph } = Typography;
export default function WritingStyles() {
const { currentProject } = useStore();
const [styles, setStyles] = useState<WritingStyle[]>([]);
const [loading, setLoading] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [editingStyle, setEditingStyle] = useState<WritingStyle | null>(null);
const [createForm] = Form.useForm();
const [editForm] = Form.useForm();
const isMobile = window.innerWidth <= 768;
// 卡片网格配置
const gridConfig = {
gutter: isMobile ? 8 : 16, // 卡片之间的间距
xs: 24,
sm: 24,
md: 12,
lg: 8,
xl: 6,
};
// 加载项目风格
useEffect(() => {
if (currentProject?.id) {
loadProjectStyles();
}
}, [currentProject?.id]);
const loadProjectStyles = async () => {
if (!currentProject?.id) return;
try {
setLoading(true);
const response = await writingStyleApi.getProjectStyles(currentProject.id);
// 对风格列表进行排序:默认风格优先,然后按原有顺序
const sortedStyles = (response.styles || []).sort((a, b) => {
// 默认风格排在前面
if (a.is_default && !b.is_default) return -1;
if (!a.is_default && b.is_default) return 1;
return 0;
});
setStyles(sortedStyles);
} catch {
message.error('加载风格列表失败');
} finally {
setLoading(false);
}
};
const handleCreate = async (values: { name: string; description?: string; prompt_content: string }) => {
if (!currentProject?.id) return;
try {
const createData: WritingStyleCreate = {
project_id: currentProject.id,
name: values.name,
style_type: 'custom',
description: values.description,
prompt_content: values.prompt_content,
};
await writingStyleApi.createStyle(createData);
message.success('创建成功');
setIsCreateModalOpen(false);
createForm.resetFields();
await loadProjectStyles();
} catch {
message.error('创建失败');
}
};
const handleEdit = (style: WritingStyle) => {
setEditingStyle(style);
editForm.setFieldsValue({
name: style.name,
description: style.description,
prompt_content: style.prompt_content,
});
setIsEditModalOpen(true);
};
const handleUpdate = async (values: WritingStyleUpdate) => {
if (!editingStyle) return;
try {
await writingStyleApi.updateStyle(editingStyle.id, values);
message.success('更新成功');
setIsEditModalOpen(false);
editForm.resetFields();
setEditingStyle(null);
await loadProjectStyles();
} catch {
message.error('更新失败');
}
};
const handleDelete = async (styleId: number) => {
try {
await writingStyleApi.deleteStyle(styleId);
message.success('删除成功');
await loadProjectStyles();
} catch {
message.error('删除失败');
}
};
const handleSetDefault = async (styleId: number) => {
if (!currentProject?.id) return;
try {
await writingStyleApi.setDefaultStyle(styleId, currentProject.id);
message.success('设置默认风格成功');
await loadProjectStyles();
} catch {
message.error('设置失败');
}
};
const showCreateModal = () => {
createForm.resetFields();
setIsCreateModalOpen(true);
};
if (!currentProject) return null;
const getStyleTypeColor = (styleType: string) => {
return styleType === 'preset' ? 'blue' : 'purple';
};
const getStyleTypeLabel = (styleType: string) => {
return styleType === 'preset' ? '预设' : '自定义';
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
padding: isMobile ? '12px 0' : '16px 0',
marginBottom: isMobile ? 12 : 16,
borderBottom: '1px solid #f0f0f0',
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 12 : 0,
justifyContent: 'space-between',
alignItems: isMobile ? 'stretch' : 'center'
}}>
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}></h2>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={showCreateModal}
block={isMobile}
>
</Button>
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
{styles.length === 0 ? (
<Empty description="暂无风格数据" />
) : (
<Row
gutter={[0, gridConfig.gutter]}
style={{ marginLeft: 0, marginRight: 0 }}
>
{styles.map((style) => (
<Col
xs={gridConfig.xs}
sm={gridConfig.sm}
md={gridConfig.md}
lg={gridConfig.lg}
xl={gridConfig.xl}
key={style.id}
style={{
paddingLeft: 0,
paddingRight: gridConfig.gutter / 2,
marginBottom: gridConfig.gutter
}}
>
<Card
hoverable
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
borderRadius: 12,
border: style.is_default ? '2px solid #1890ff' : '1px solid #f0f0f0',
}}
bodyStyle={{
flex: 1,
display: 'flex',
flexDirection: 'column',
padding: '16px',
}}
actions={[
<Tooltip key="default" title={style.is_default ? '当前默认' : '设为默认'}>
<span
onClick={() => !style.is_default && handleSetDefault(style.id)}
style={{ cursor: style.is_default ? 'default' : 'pointer' }}
>
{style.is_default ? (
<StarFilled style={{ color: '#faad14', fontSize: 18 }} />
) : (
<StarOutlined style={{ fontSize: 18 }} />
)}
</span>
</Tooltip>,
<Tooltip key="edit" title={style.project_id === null ? '预设风格不可编辑' : '编辑'}>
<EditOutlined
onClick={() => style.project_id !== null && handleEdit(style)}
style={{
fontSize: 18,
cursor: style.project_id === null ? 'not-allowed' : 'pointer',
color: style.project_id === null ? '#ccc' : undefined
}}
/>
</Tooltip>,
<Popconfirm
key="delete"
title="确定删除这个风格吗?"
description={style.is_default ? '这是默认风格,删除后需要设置新的默认风格' : undefined}
onConfirm={() => handleDelete(style.id)}
okText="确定"
cancelText="取消"
disabled={style.project_id === null || styles.length === 1}
>
<Tooltip title={
style.project_id === null
? '预设风格不可删除'
: styles.length === 1
? '至少保留一个风格'
: '删除'
}>
<DeleteOutlined
style={{
fontSize: 18,
color: (style.project_id === null || styles.length === 1) ? '#ccc' : undefined,
cursor: (style.project_id === null || styles.length === 1) ? 'not-allowed' : 'pointer'
}}
/>
</Tooltip>
</Popconfirm>,
]}
>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Space style={{ marginBottom: 12 }} wrap>
<Text strong style={{ fontSize: 16 }}>{style.name}</Text>
<Tag color={getStyleTypeColor(style.style_type)}>
{getStyleTypeLabel(style.style_type)}
</Tag>
{style.is_default && <Tag color="gold"></Tag>}
</Space>
{style.description && (
<Paragraph
type="secondary"
style={{ fontSize: 13, marginBottom: 12 }}
ellipsis={{ rows: 2, tooltip: style.description }}
>
{style.description}
</Paragraph>
)}
<Paragraph
type="secondary"
style={{
fontSize: 12,
marginBottom: 0,
backgroundColor: '#fafafa',
padding: 8,
borderRadius: 4,
flex: 1,
minHeight: 60,
}}
ellipsis={{ rows: 3, tooltip: style.prompt_content }}
>
{style.prompt_content}
</Paragraph>
</div>
</Card>
</Col>
))}
</Row>
)}
</div>
{/* 创建自定义风格 Modal */}
<Modal
title="创建自定义风格"
open={isCreateModalOpen}
onCancel={() => {
setIsCreateModalOpen(false);
createForm.resetFields();
}}
footer={null}
centered
width={isMobile ? 'calc(100vw - 32px)' : 600}
style={isMobile ? { maxWidth: 'calc(100vw - 32px)', margin: '0 16px' } : undefined}
>
<Form
form={createForm}
layout="vertical"
onFinish={handleCreate}
style={{ marginTop: 16 }}
>
<Form.Item
label="风格名称"
name="name"
rules={[{ required: true, message: '请输入风格名称' }]}
>
<Input placeholder="如:武侠风、科幻风" />
</Form.Item>
<Form.Item label="风格描述" name="description">
<TextArea rows={2} placeholder="简要描述这个风格的特点..." />
</Form.Item>
<Form.Item
label="提示词内容"
name="prompt_content"
rules={[{ required: true, message: '请输入提示词内容' }]}
>
<TextArea
rows={6}
placeholder="输入风格的提示词,用于引导AI生成符合该风格的内容..."
/>
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => {
setIsCreateModalOpen(false);
createForm.resetFields();
}}>
</Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 编辑风格 Modal */}
<Modal
title="编辑写作风格"
open={isEditModalOpen}
onCancel={() => {
setIsEditModalOpen(false);
editForm.resetFields();
setEditingStyle(null);
}}
footer={null}
centered
width={isMobile ? 'calc(100vw - 32px)' : 600}
style={isMobile ? { maxWidth: 'calc(100vw - 32px)', margin: '0 16px' } : undefined}
>
<Form form={editForm} layout="vertical" onFinish={handleUpdate} style={{ marginTop: 16 }}>
<Form.Item
label="风格名称"
name="name"
rules={[{ required: true, message: '请输入风格名称' }]}
>
<Input placeholder="输入风格名称" />
</Form.Item>
<Form.Item label="风格描述" name="description">
<TextArea rows={2} placeholder="简要描述这个风格的特点..." />
</Form.Item>
<Form.Item
label="提示词内容"
name="prompt_content"
rules={[{ required: true, message: '请输入提示词内容' }]}
>
<TextArea
rows={6}
placeholder="输入风格的提示词..."
/>
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => {
setIsEditModalOpen(false);
editForm.resetFields();
setEditingStyle(null);
}}>
</Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
}