update: 1.新增职业管理模块和角色职业关联 2.章节分析自动更新角色职业状态 3.优化章节生成的角色信息构建 4.批量生成强制开启同步分析 5.章节内容批量生成增加系统提示

This commit is contained in:
xiamuceer
2025-12-22 19:53:31 +08:00
parent 6886d903fe
commit b2dec41464
25 changed files with 4635 additions and 89 deletions
+46 -7
View File
@@ -32,6 +32,7 @@ type GenerationStep = 'pending' | 'processing' | 'completed' | 'error';
interface GenerationSteps {
worldBuilding: GenerationStep;
careers: GenerationStep;
characters: GenerationStep;
outline: GenerationStep;
}
@@ -55,6 +56,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
const [errorDetails, setErrorDetails] = useState<string>('');
const [generationSteps, setGenerationSteps] = useState<GenerationSteps>({
worldBuilding: 'pending',
careers: 'pending',
characters: 'pending',
outline: 'pending'
});
@@ -126,12 +128,12 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
if (wizardStep === 0) {
// 从世界观开始
message.info('从世界观步骤开始生成...');
setGenerationSteps({ worldBuilding: 'processing', characters: 'pending', outline: 'pending' });
setGenerationSteps({ worldBuilding: 'processing', careers: 'pending', characters: 'pending', outline: 'pending' });
await resumeFromWorldBuilding(data);
} else if (wizardStep === 1) {
// 世界观已完成,从角色开始
message.info('世界观已完成,从角色步骤继续...');
setGenerationSteps({ worldBuilding: 'completed', characters: 'processing', outline: 'pending' });
setGenerationSteps({ worldBuilding: 'completed', careers: 'completed', characters: 'processing', outline: 'pending' });
// 获取世界观数据
const worldResult = {
@@ -148,7 +150,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
} else if (wizardStep === 2) {
// 世界观和角色已完成,从大纲开始
message.info('世界观和角色已完成,从大纲步骤继续...');
setGenerationSteps({ worldBuilding: 'completed', characters: 'completed', outline: 'processing' });
setGenerationSteps({ worldBuilding: 'completed', careers: 'completed', characters: 'completed', outline: 'processing' });
setProgress(66);
await resumeFromOutline(data, projectIdParam);
} else {
@@ -334,13 +336,31 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
},
{
onProgress: (msg, prog) => {
setProgress(Math.floor(prog / 3));
// 世界观生成占0%-20%,职业生成占20%-30%
const baseProgress = Math.floor(prog / 5);
setProgress(baseProgress);
setProgressMessage(msg);
// 检测职业体系生成阶段 - 必须包含"职业体系"才算职业阶段
if (msg.includes('职业体系')) {
if (msg.includes('开始') || msg.includes('生成')) {
// 职业开始时,世界观应该已完成
setGenerationSteps(prev => ({
...prev,
worldBuilding: 'completed',
careers: 'processing'
}));
}
if (msg.includes('完成') || msg.includes('✅')) {
setGenerationSteps(prev => ({ ...prev, careers: 'completed' }));
}
}
},
onResult: (result) => {
setProjectId(result.project_id);
setWorldBuildingResult(result);
setGenerationSteps(prev => ({ ...prev, worldBuilding: 'completed' }));
// 职业体系状态已在onProgress中更新
},
onError: (error) => {
console.error('世界观生成失败:', error);
@@ -383,7 +403,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
},
{
onProgress: (msg, prog) => {
setProgress(33 + Math.floor(prog / 3));
// 角色生成占40%-70%
setProgress(40 + Math.floor(prog * 0.3));
setProgressMessage(msg);
},
onResult: (result) => {
@@ -416,7 +437,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
},
{
onProgress: (msg, prog) => {
setProgress(66 + Math.floor(prog / 3));
// 大纲生成占70%-100%
setProgress(70 + Math.floor(prog * 0.3));
setProgressMessage(msg);
},
onResult: () => {
@@ -511,8 +533,23 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
},
{
onProgress: (msg, prog) => {
setProgress(Math.floor(prog / 3));
const baseProgress = Math.floor(prog / 5);
setProgress(baseProgress);
setProgressMessage(msg);
// 检测职业体系生成阶段
if (msg.includes('职业体系')) {
if (msg.includes('开始') || msg.includes('生成')) {
setGenerationSteps(prev => ({
...prev,
worldBuilding: 'completed',
careers: 'processing'
}));
}
if (msg.includes('完成') || msg.includes('✅')) {
setGenerationSteps(prev => ({ ...prev, careers: 'completed' }));
}
}
},
onResult: (result) => {
setProjectId(result.project_id);
@@ -755,6 +792,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
};
const hasError = generationSteps.worldBuilding === 'error' ||
generationSteps.careers === 'error' ||
generationSteps.characters === 'error' ||
generationSteps.outline === 'error';
@@ -843,6 +881,7 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
>
{[
{ key: 'worldBuilding', label: '生成世界观', step: generationSteps.worldBuilding },
{ key: 'careers', label: '生成职业体系', step: generationSteps.careers },
{ key: 'characters', label: '生成角色', step: generationSteps.characters },
{ key: 'outline', label: '生成大纲', step: generationSteps.outline },
].map(({ key, label, step }) => {
@@ -0,0 +1,398 @@
import { useState, useEffect } from 'react';
import { Card, Button, Modal, Form, Select, InputNumber, Input, message, Progress, Tag, Space, Divider, Typography } from 'antd';
import { EditOutlined, PlusOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
import axios from 'axios';
const { TextArea } = Input;
const { Text, Paragraph } = Typography;
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
interface CareerDetail {
id: string;
character_id: string;
career_id: string;
career_name: string;
career_type: 'main' | 'sub';
current_stage: number;
stage_name: string;
stage_description?: string;
stage_progress: number;
max_stage: number;
started_at?: string;
reached_current_stage_at?: string;
notes?: string;
}
interface Career {
id: string;
name: string;
type: 'main' | 'sub';
max_stage: number;
}
interface Props {
characterId: string;
projectId: string;
editable?: boolean;
onUpdate?: () => void;
}
export const CharacterCareerCard: React.FC<Props> = ({
characterId,
projectId,
editable = false,
onUpdate
}) => {
const [mainCareer, setMainCareer] = useState<CareerDetail | null>(null);
const [subCareers, setSubCareers] = useState<CareerDetail[]>([]);
const [allCareers, setAllCareers] = useState<Career[]>([]);
const [loading, setLoading] = useState(true);
const [isMainModalOpen, setIsMainModalOpen] = useState(false);
const [isSubModalOpen, setIsSubModalOpen] = useState(false);
const [isProgressModalOpen, setIsProgressModalOpen] = useState(false);
const [selectedCareer, setSelectedCareer] = useState<CareerDetail | null>(null);
const [mainForm] = Form.useForm();
const [subForm] = Form.useForm();
const [progressForm] = Form.useForm();
useEffect(() => {
fetchCharacterCareers();
if (editable) {
fetchAllCareers();
}
}, [characterId]);
const fetchCharacterCareers = async () => {
try {
setLoading(true);
const response = await axios.get(
`${API_BASE_URL}/api/careers/character/${characterId}/careers`,
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
);
setMainCareer(response.data.main_career || null);
setSubCareers(response.data.sub_careers || []);
} catch (error: any) {
message.error(error.response?.data?.detail || '获取职业信息失败');
} finally {
setLoading(false);
}
};
const fetchAllCareers = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/api/careers`, {
params: { project_id: projectId },
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
const main = response.data.main_careers || [];
const sub = response.data.sub_careers || [];
setAllCareers([...main, ...sub]);
} catch (error: any) {
console.error('获取职业列表失败:', error);
}
};
const handleSetMainCareer = async (values: any) => {
try {
await axios.post(
`${API_BASE_URL}/api/careers/character/${characterId}/careers/main`,
values,
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
);
message.success('主职业设置成功');
setIsMainModalOpen(false);
mainForm.resetFields();
fetchCharacterCareers();
onUpdate?.();
} catch (error: any) {
message.error(error.response?.data?.detail || '设置主职业失败');
}
};
const handleAddSubCareer = async (values: any) => {
try {
await axios.post(
`${API_BASE_URL}/api/careers/character/${characterId}/careers/sub`,
values,
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
);
message.success('副职业添加成功');
setIsSubModalOpen(false);
subForm.resetFields();
fetchCharacterCareers();
onUpdate?.();
} catch (error: any) {
message.error(error.response?.data?.detail || '添加副职业失败');
}
};
const handleUpdateProgress = async (values: any) => {
if (!selectedCareer) return;
try {
await axios.put(
`${API_BASE_URL}/api/careers/character/${characterId}/careers/${selectedCareer.career_id}/stage`,
values,
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
);
message.success('职业阶段更新成功');
setIsProgressModalOpen(false);
progressForm.resetFields();
fetchCharacterCareers();
onUpdate?.();
} catch (error: any) {
message.error(error.response?.data?.detail || '更新职业阶段失败');
}
};
const handleRemoveSubCareer = (careerId: string) => {
Modal.confirm({
title: '确认删除',
content: '确定要移除这个副职业吗?',
onOk: async () => {
try {
await axios.delete(
`${API_BASE_URL}/api/careers/character/${characterId}/careers/${careerId}`,
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
);
message.success('副职业删除成功');
fetchCharacterCareers();
onUpdate?.();
} catch (error: any) {
message.error(error.response?.data?.detail || '删除副职业失败');
}
}
});
};
const openEditProgress = (career: CareerDetail) => {
setSelectedCareer(career);
progressForm.setFieldsValue({
current_stage: career.current_stage,
stage_progress: career.stage_progress,
reached_current_stage_at: career.reached_current_stage_at || '',
notes: career.notes || ''
});
setIsProgressModalOpen(true);
};
const renderCareerInfo = (career: CareerDetail, isMain: boolean = false) => (
<div key={career.id} style={{ marginBottom: 16 }}>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Space>
<TrophyOutlined style={{ color: isMain ? '#1890ff' : '#8c8c8c' }} />
<Text strong={isMain}>{career.career_name}</Text>
{isMain && <Tag color="blue"></Tag>}
</Space>
{editable && (
<Space>
<Button size="small" icon={<EditOutlined />} onClick={() => openEditProgress(career)} />
{!isMain && (
<Button
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveSubCareer(career.career_id)}
/>
)}
</Space>
)}
</Space>
<div style={{ marginLeft: 24, marginTop: 8 }}>
<Text type="secondary">
{career.stage_name}{career.current_stage}/{career.max_stage}
</Text>
{career.stage_description && (
<Paragraph type="secondary" style={{ fontSize: 12, marginTop: 4 }}>
{career.stage_description}
</Paragraph>
)}
<Progress
percent={career.stage_progress}
size="small"
style={{ marginTop: 8 }}
format={(percent) => `${percent}%`}
/>
{career.started_at && (
<Text type="secondary" style={{ fontSize: 12 }}>
{career.started_at}
</Text>
)}
{career.notes && (
<Paragraph type="secondary" style={{ fontSize: 12, marginTop: 4 }}>
{career.notes}
</Paragraph>
)}
</div>
</div>
);
if (loading) {
return <Card loading />;
}
return (
<>
<Card
title={
<Space>
<TrophyOutlined />
</Space>
}
extra={
editable && !mainCareer && (
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => {
mainForm.resetFields();
setIsMainModalOpen(true);
}}
>
</Button>
)
}
>
{mainCareer ? (
<>
{renderCareerInfo(mainCareer, true)}
{subCareers.length > 0 && (
<>
<Divider />
<Text type="secondary"></Text>
<div style={{ marginTop: 8 }}>
{subCareers.map(career => renderCareerInfo(career, false))}
</div>
</>
)}
{editable && subCareers.length < 5 && (
<div style={{ textAlign: 'center', marginTop: 16 }}>
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => {
subForm.resetFields();
setIsSubModalOpen(true);
}}
>
</Button>
</div>
)}
</>
) : (
<Text type="secondary" style={{ display: 'block', textAlign: 'center', padding: '20px 0' }}>
</Text>
)}
</Card>
{/* 设置主职业 */}
<Modal
title="设置主职业"
open={isMainModalOpen}
onCancel={() => setIsMainModalOpen(false)}
footer={null}
>
<Form form={mainForm} layout="vertical" onFinish={handleSetMainCareer}>
<Form.Item label="选择主职业" name="career_id" rules={[{ required: true }]}>
<Select placeholder="选择职业">
{allCareers.filter(c => c.type === 'main').map(career => (
<Select.Option key={career.id} value={career.id}>
{career.name}{career.max_stage}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="当前阶段" name="current_stage" initialValue={1}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="开始时间" name="started_at">
<Input placeholder="如:修仙历3000年" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsMainModalOpen(false)}></Button>
<Button type="primary" htmlType="submit"></Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 添加副职业 */}
<Modal
title="添加副职业"
open={isSubModalOpen}
onCancel={() => setIsSubModalOpen(false)}
footer={null}
>
<Form form={subForm} layout="vertical" onFinish={handleAddSubCareer}>
<Form.Item label="选择副职业" name="career_id" rules={[{ required: true }]}>
<Select placeholder="选择职业">
{allCareers.filter(c => c.type === 'sub').map(career => (
<Select.Option key={career.id} value={career.id}>
{career.name}{career.max_stage}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="当前阶段" name="current_stage" initialValue={1}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="开始时间" name="started_at">
<Input placeholder="如:修仙历3000年" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsSubModalOpen(false)}></Button>
<Button type="primary" htmlType="submit"></Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 更新职业进度 */}
<Modal
title="更新职业阶段"
open={isProgressModalOpen}
onCancel={() => setIsProgressModalOpen(false)}
footer={null}
>
{selectedCareer && (
<Form form={progressForm} layout="vertical" onFinish={handleUpdateProgress}>
<Text>{selectedCareer.career_name}</Text>
<Divider style={{ margin: '12px 0' }} />
<Form.Item label="当前阶段" name="current_stage" rules={[{ required: true }]}>
<InputNumber min={1} max={selectedCareer.max_stage} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="阶段进度(0-100" name="stage_progress" rules={[{ required: true }]}>
<InputNumber min={0} max={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="到达时间" name="reached_current_stage_at">
<Input placeholder="如:修仙历3001年" />
</Form.Item>
<Form.Item label="备注" name="notes">
<TextArea rows={2} placeholder="如:突破至金丹期" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsProgressModalOpen(false)}></Button>
<Button type="primary" htmlType="submit"></Button>
</Space>
</Form.Item>
</Form>
)}
</Modal>
</>
);
};
export default CharacterCareerCard;