Files
MuMuAINovel/frontend/src/components/ExpansionPlanEditor.tsx
T

324 lines
9.9 KiB
TypeScript
Raw Normal View History

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>
);
}