update:1.小说项目创建支持双模式生成,大纲-章节(一对一&一对多) 2.新增章节管理-编辑章节规划功能 3.修复灵感模式可重复点击选项问题,刷新对话内容丢失问题
This commit is contained in:
@@ -16,6 +16,7 @@ export interface GenerationConfig {
|
||||
target_words: number;
|
||||
chapter_count: number;
|
||||
character_count: number;
|
||||
outline_mode?: 'one-to-one' | 'one-to-many'; // 大纲章节模式
|
||||
}
|
||||
|
||||
interface AIProjectGeneratorProps {
|
||||
@@ -183,6 +184,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
target_words: data.target_words,
|
||||
chapter_count: data.chapter_count,
|
||||
character_count: data.character_count,
|
||||
outline_mode: data.outline_mode || 'one-to-many', // 传递大纲模式
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
@@ -328,6 +330,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
target_words: data.target_words,
|
||||
chapter_count: data.chapter_count,
|
||||
character_count: data.character_count,
|
||||
outline_mode: data.outline_mode || 'one-to-many', // 传递大纲模式
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
@@ -504,6 +507,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
target_words: generationData.target_words,
|
||||
chapter_count: generationData.chapter_count,
|
||||
character_count: generationData.character_count,
|
||||
outline_mode: generationData.outline_mode || 'one-to-many', // 传递大纲模式
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
|
||||
@@ -5,14 +5,17 @@ interface AnnouncementModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onDoNotShowToday: () => void;
|
||||
onNeverShow: () => void;
|
||||
}
|
||||
|
||||
export default function AnnouncementModal({ visible, onClose, onDoNotShowToday }: AnnouncementModalProps) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, onNeverShow }: AnnouncementModalProps) {
|
||||
const [qqImageError, setQqImageError] = useState(false);
|
||||
const [wxImageError, setWxImageError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setImageError(false);
|
||||
setQqImageError(false);
|
||||
setWxImageError(false);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
@@ -21,6 +24,11 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday }
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleNeverShow = () => {
|
||||
onNeverShow();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="🎉 欢迎使用 AI小说创作助手"
|
||||
@@ -28,15 +36,15 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday }
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<Space style={{ width: '100%', justifyContent: 'center' }}>
|
||||
<Button onClick={onClose} size="large">
|
||||
知道了
|
||||
<Button onClick={handleDoNotShowToday} size="large">
|
||||
今日内不再展示
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleDoNotShowToday} size="large">
|
||||
今天内不再提示
|
||||
<Button type="primary" onClick={handleNeverShow} size="large">
|
||||
永不再展示
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
width={600}
|
||||
width={800}
|
||||
centered
|
||||
styles={{
|
||||
body: {
|
||||
@@ -65,44 +73,120 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday }
|
||||
<li>📚 分享创作经验和灵感</li>
|
||||
</ul>
|
||||
<p style={{ fontWeight: 600, color: '#333', marginBottom: '16px' }}>
|
||||
扫描下方二维码加入QQ交流群:
|
||||
扫描下方二维码加入交流群:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!imageError ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
gap: '24px',
|
||||
padding: '20px',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: '8px',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
{/* QQ 二维码 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '20px',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: '8px',
|
||||
minWidth: '280px',
|
||||
}}>
|
||||
<img
|
||||
src="/qq.jpg"
|
||||
alt="QQ交流群二维码"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '360px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
padding: '40px',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: '8px',
|
||||
color: '#999',
|
||||
}}>
|
||||
<p>二维码加载失败</p>
|
||||
<p style={{ fontSize: '12px', marginTop: '8px' }}>
|
||||
请确保 qq.jpg 文件位于 frontend/public/ 目录下
|
||||
<p style={{ fontWeight: 600, color: '#333', marginBottom: '12px', fontSize: '15px' }}>
|
||||
QQ交流群
|
||||
</p>
|
||||
{!qqImageError ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
}}>
|
||||
<img
|
||||
src="/qq.jpg"
|
||||
alt="QQ交流群二维码"
|
||||
style={{
|
||||
maxWidth: '280px',
|
||||
maxHeight: '280px',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
onError={() => setQqImageError(true)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
width: '280px',
|
||||
height: '280px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
color: '#999',
|
||||
}}>
|
||||
<p>二维码加载失败</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 微信二维码 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
minWidth: '280px',
|
||||
}}>
|
||||
<p style={{ fontWeight: 600, color: '#333', marginBottom: '12px', fontSize: '15px' }}>
|
||||
微信交流群
|
||||
</p>
|
||||
{!wxImageError ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
}}>
|
||||
<img
|
||||
src="/WX.png"
|
||||
alt="微信交流群二维码"
|
||||
style={{
|
||||
maxWidth: '280px',
|
||||
maxHeight: '280px',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
onError={() => setWxImageError(true)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
width: '280px',
|
||||
height: '280px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
color: '#999',
|
||||
}}>
|
||||
<p>二维码加载失败</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
@@ -113,7 +197,7 @@ export default function AnnouncementModal({ visible, onClose, onDoNotShowToday }
|
||||
fontSize: '14px',
|
||||
color: '#ad6800',
|
||||
}}>
|
||||
💡 提示:点击"今天内不再提示"可在今天内不再显示此公告
|
||||
💡 提示:选择"今日内不再展示"当天不再显示,选择"永不再展示"将永久隐藏此公告
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Typography, Space, Divider, Badge, Tooltip } from 'antd';
|
||||
import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined } from '@ant-design/icons';
|
||||
import { Typography, Space, Divider, Badge, Tooltip, Button } from 'antd';
|
||||
import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons';
|
||||
import { VERSION_INFO, getVersionString } from '../config/version';
|
||||
import { checkLatestVersion } from '../services/versionService';
|
||||
|
||||
@@ -88,6 +88,26 @@ export default function AppFooter() {
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'rgba(255, 255, 255, 0.3)' }} />
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<GiftOutlined />}
|
||||
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none',
|
||||
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.4)',
|
||||
fontSize: 11,
|
||||
height: 24,
|
||||
padding: '0 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
赞助
|
||||
</Button>
|
||||
<Divider type="vertical" style={{ margin: '0 4px', borderColor: 'rgba(255, 255, 255, 0.3)' }} />
|
||||
<Link
|
||||
href={VERSION_INFO.githubUrl}
|
||||
target="_blank"
|
||||
@@ -190,6 +210,36 @@ export default function AppFooter() {
|
||||
LinuxDO 社区
|
||||
</Link>
|
||||
|
||||
{/* 赞助按钮 */}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<GiftOutlined style={{ fontSize: 14 }} />}
|
||||
onClick={() => window.open('https://mumuverse.space:1588/', '_blank')}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.5)',
|
||||
fontSize: 13,
|
||||
height: 32,
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
fontWeight: 600,
|
||||
transition: 'all 0.3s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.6)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.5)';
|
||||
}}
|
||||
>
|
||||
赞助支持
|
||||
</Button>
|
||||
|
||||
{/* 许可证 */}
|
||||
<Link
|
||||
href={VERSION_INFO.licenseUrl}
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import { Modal, Form, Input, InputNumber, Select, Tag, Space, Button, message } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { ExpansionPlanData, Character } from '../types';
|
||||
import { characterApi } from '../services/api';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface ExpansionPlanEditorProps {
|
||||
visible: boolean;
|
||||
planData: ExpansionPlanData | null;
|
||||
projectId: string;
|
||||
onSave: (data: ExpansionPlanData) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function ExpansionPlanEditor({
|
||||
visible,
|
||||
planData,
|
||||
projectId,
|
||||
onSave,
|
||||
onCancel
|
||||
}: ExpansionPlanEditorProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 关键事件标签输入
|
||||
const [keyEventInput, setKeyEventInput] = useState('');
|
||||
const [keyEvents, setKeyEvents] = useState<string[]>([]);
|
||||
|
||||
// 角色列表和选择
|
||||
const [availableCharacters, setAvailableCharacters] = useState<Character[]>([]);
|
||||
const [characters, setCharacters] = useState<string[]>([]);
|
||||
const [loadingCharacters, setLoadingCharacters] = useState(false);
|
||||
|
||||
// 加载项目角色列表
|
||||
useEffect(() => {
|
||||
if (visible && projectId) {
|
||||
loadCharacters();
|
||||
}
|
||||
}, [visible, projectId]);
|
||||
|
||||
const loadCharacters = async () => {
|
||||
try {
|
||||
setLoadingCharacters(true);
|
||||
setAvailableCharacters([]); // 重置为空数组
|
||||
const response = await characterApi.getCharacters(projectId);
|
||||
console.log('加载到的角色数据:', response);
|
||||
|
||||
// API返回的是 {total, items} 格式,需要提取items
|
||||
let chars: Character[] = [];
|
||||
if (Array.isArray(response)) {
|
||||
chars = response;
|
||||
} else if (response && typeof response === 'object' && 'items' in response && Array.isArray((response as any).items)) {
|
||||
chars = (response as any).items;
|
||||
} else {
|
||||
console.error('角色API返回格式异常:', response);
|
||||
message.warning('角色数据格式异常');
|
||||
}
|
||||
|
||||
setAvailableCharacters(chars);
|
||||
console.log('设置的角色列表:', chars);
|
||||
} catch (error: any) {
|
||||
console.error('加载角色列表失败:', error);
|
||||
setAvailableCharacters([]);
|
||||
message.error('加载角色列表失败: ' + (error?.message || '未知错误'));
|
||||
} finally {
|
||||
setLoadingCharacters(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 当planData变化时更新状态
|
||||
useEffect(() => {
|
||||
if (planData) {
|
||||
setKeyEvents(planData.key_events || []);
|
||||
setCharacters(planData.character_focus || []);
|
||||
form.setFieldsValue({
|
||||
emotional_tone: planData.emotional_tone,
|
||||
narrative_goal: planData.narrative_goal,
|
||||
conflict_type: planData.conflict_type,
|
||||
estimated_words: planData.estimated_words
|
||||
});
|
||||
} else {
|
||||
// 重置状态
|
||||
setKeyEvents([]);
|
||||
setCharacters([]);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [planData, form, visible]);
|
||||
|
||||
const handleAddKeyEvent = () => {
|
||||
if (keyEventInput.trim()) {
|
||||
setKeyEvents([...keyEvents, keyEventInput.trim()]);
|
||||
setKeyEventInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCharacter = (characterName: string) => {
|
||||
if (characterName && !characters.includes(characterName)) {
|
||||
setCharacters([...characters, characterName]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 验证至少有一个关键事件
|
||||
if (keyEvents.length === 0) {
|
||||
message.warning('请至少添加一个关键事件');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证至少有一个角色
|
||||
if (characters.length === 0) {
|
||||
message.warning('请至少添加一个涉及角色');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedPlan: ExpansionPlanData = {
|
||||
key_events: keyEvents,
|
||||
character_focus: characters,
|
||||
emotional_tone: values.emotional_tone,
|
||||
narrative_goal: values.narrative_goal,
|
||||
conflict_type: values.conflict_type,
|
||||
estimated_words: values.estimated_words,
|
||||
scenes: planData?.scenes || null
|
||||
};
|
||||
|
||||
await onSave(updatedPlan);
|
||||
// message.success('规划信息保存成功');
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
message.error('保存失败,请重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
setKeyEvents([]);
|
||||
setCharacters([]);
|
||||
setKeyEventInput('');
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="编辑章节规划"
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
width={700}
|
||||
centered
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleCancel} disabled={loading}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" loading={loading} onClick={handleSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
emotional_tone: '紧张激烈',
|
||||
conflict_type: '人物冲突',
|
||||
estimated_words: 3000
|
||||
}}
|
||||
>
|
||||
{/* 关键事件 */}
|
||||
<Form.Item
|
||||
label="关键事件"
|
||||
tooltip="至少添加一个关键事件"
|
||||
required
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="输入关键事件后按回车或点击添加"
|
||||
value={keyEventInput}
|
||||
onChange={(e) => setKeyEventInput(e.target.value)}
|
||||
onPressEnter={handleAddKeyEvent}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddKeyEvent}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Space wrap>
|
||||
{keyEvents.map((event, idx) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
closable
|
||||
onClose={(e) => {
|
||||
e.preventDefault();
|
||||
setKeyEvents(keyEvents.filter((_, i) => i !== idx));
|
||||
}}
|
||||
color="purple"
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
<span style={{ fontWeight: 'bold', marginRight: 4 }}>#{idx + 1}</span>
|
||||
{event}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
{/* 涉及角色 */}
|
||||
<Form.Item
|
||||
label="涉及角色"
|
||||
tooltip="从项目现有角色中选择"
|
||||
required
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Select
|
||||
placeholder="选择角色"
|
||||
style={{ width: '100%' }}
|
||||
loading={loadingCharacters}
|
||||
onChange={handleAddCharacter}
|
||||
value={undefined}
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={Array.isArray(availableCharacters)
|
||||
? availableCharacters
|
||||
.filter(char => !characters.includes(char.name))
|
||||
.map(char => ({
|
||||
label: char.name,
|
||||
value: char.name,
|
||||
}))
|
||||
: []}
|
||||
notFoundContent={
|
||||
loadingCharacters ? '加载中...' :
|
||||
!Array.isArray(availableCharacters) ? '加载角色失败' :
|
||||
availableCharacters.length === 0 ? '暂无角色,请先在角色管理中创建' :
|
||||
'所有角色已添加'
|
||||
}
|
||||
/>
|
||||
<Space wrap>
|
||||
{characters.map((char, idx) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
closable
|
||||
onClose={() => setCharacters(characters.filter((_, i) => i !== idx))}
|
||||
color="cyan"
|
||||
>
|
||||
{char}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
{/* 情感基调 */}
|
||||
<Form.Item
|
||||
label="情感基调"
|
||||
name="emotional_tone"
|
||||
rules={[{ required: true, message: '请输入情感基调' }]}
|
||||
tooltip="例如:紧张激烈、温馨感人、悬疑惊悚等"
|
||||
>
|
||||
<Input
|
||||
placeholder="输入情感基调,例如:紧张激烈、温馨感人等"
|
||||
maxLength={20}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 冲突类型 */}
|
||||
<Form.Item
|
||||
label="冲突类型"
|
||||
name="conflict_type"
|
||||
rules={[{ required: true, message: '请输入冲突类型' }]}
|
||||
tooltip="例如:人物冲突、内心冲突、环境冲突等"
|
||||
>
|
||||
<Input
|
||||
placeholder="输入冲突类型,例如:人物冲突、内心冲突等"
|
||||
maxLength={20}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 预估字数 */}
|
||||
<Form.Item
|
||||
label="预估字数"
|
||||
name="estimated_words"
|
||||
rules={[{ required: true, message: '请输入预估字数' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={500}
|
||||
max={10000}
|
||||
step={100}
|
||||
style={{ width: '100%' }}
|
||||
formatter={(value) => `${value} 字`}
|
||||
parser={(value) => value?.replace(' 字', '') as any}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 叙事目标 */}
|
||||
<Form.Item
|
||||
label="叙事目标"
|
||||
name="narrative_goal"
|
||||
rules={[{ required: true, message: '请输入叙事目标' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder="描述本章要达成的叙事目标,例如:推进主线剧情、深化角色关系、揭示重要信息等..."
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Modal, Spin } from 'antd';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Modal, Spin, Button } from 'antd';
|
||||
import { LoadingOutlined, StopOutlined } from '@ant-design/icons';
|
||||
|
||||
interface SSEProgressModalProps {
|
||||
visible: boolean;
|
||||
@@ -9,6 +9,8 @@ interface SSEProgressModalProps {
|
||||
title?: string;
|
||||
showPercentage?: boolean;
|
||||
showIcon?: boolean;
|
||||
onCancel?: () => void;
|
||||
cancelButtonText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,6 +24,8 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
title = 'AI生成中...',
|
||||
showPercentage = true,
|
||||
showIcon = true,
|
||||
onCancel,
|
||||
cancelButtonText = '取消任务',
|
||||
}) => {
|
||||
if (!visible) return null;
|
||||
|
||||
@@ -115,10 +119,28 @@ export const SSEProgressModal: React.FC<SSEProgressModalProps> = ({
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontSize: 13,
|
||||
color: '#8c8c8c'
|
||||
color: '#8c8c8c',
|
||||
marginBottom: onCancel ? 16 : 0
|
||||
}}>
|
||||
请勿关闭页面,生成过程需要一定时间
|
||||
</div>
|
||||
|
||||
{/* 取消按钮 */}
|
||||
{onCancel && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginTop: 16
|
||||
}}>
|
||||
<Button
|
||||
danger
|
||||
size="large"
|
||||
icon={<StopOutlined />}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{cancelButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user