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
+273 -6
View File
@@ -6,13 +6,21 @@ import { useCharacterSync } from '../store/hooks';
import { characterGridConfig } from '../components/CardStyles';
import { CharacterCard } from '../components/CharacterCard';
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
import type { Character, CharacterUpdate } from '../types';
import type { Character } from '../types';
import { characterApi } from '../services/api';
import { SSEPostClient } from '../utils/sseClient';
import axios from 'axios';
const { Title } = Typography;
const { TextArea } = Input;
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
interface Career {
id: string;
name: string;
type: 'main' | 'sub';
max_stage: number;
}
export default function Characters() {
const { currentProject, characters } = useStore();
@@ -28,6 +36,8 @@ export default function Characters() {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [createType, setCreateType] = useState<'character' | 'organization'>('character');
const [editingCharacter, setEditingCharacter] = useState<Character | null>(null);
const [mainCareers, setMainCareers] = useState<Career[]>([]);
const [subCareers, setSubCareers] = useState<Career[]>([]);
const {
refreshCharacters,
@@ -37,11 +47,26 @@ export default function Characters() {
useEffect(() => {
if (currentProject?.id) {
refreshCharacters();
fetchCareers();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentProject?.id]);
const [modal, contextHolder] = Modal.useModal();
const fetchCareers = async () => {
if (!currentProject?.id) return;
try {
const response = await axios.get(`${API_BASE_URL}/api/careers`, {
params: { project_id: currentProject.id },
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setMainCareers(response.data.main_careers || []);
setSubCareers(response.data.sub_careers || []);
} catch (error) {
console.error('获取职业列表失败:', error);
}
};
if (!currentProject) return null;
const handleDeleteCharacter = async (id: string) => {
@@ -170,6 +195,17 @@ export default function Characters() {
createData.appearance = values.appearance;
createData.relationships = values.relationships;
createData.background = values.background;
// 职业字段
if (values.main_career_id) {
createData.main_career_id = values.main_career_id;
createData.main_career_stage = values.main_career_stage || 1;
}
// 处理副职业数据
if (values.sub_career_data && Array.isArray(values.sub_career_data) && values.sub_career_data.length > 0) {
createData.sub_careers = JSON.stringify(values.sub_career_data);
}
} else {
// 组织字段
createData.organization_type = values.organization_type;
@@ -195,21 +231,45 @@ export default function Characters() {
const handleEditCharacter = (character: Character) => {
setEditingCharacter(character);
editForm.setFieldsValue(character);
// 提取副职业数据(包含职业ID和阶段)
const subCareerData = character.sub_careers?.map((sc: any) => ({
career_id: sc.career_id,
stage: sc.stage || 1
})) || [];
editForm.setFieldsValue({
...character,
sub_career_data: subCareerData
});
setIsEditModalOpen(true);
};
const handleUpdateCharacter = async (values: CharacterUpdate) => {
const handleUpdateCharacter = async (values: any) => {
if (!editingCharacter) return;
try {
await characterApi.updateCharacter(editingCharacter.id, values);
const updateData: any = { ...values };
// 处理副职业数据
const subCareerData = updateData.sub_career_data;
delete updateData.sub_career_data;
// 转换为sub_careers格式
if (subCareerData && Array.isArray(subCareerData) && subCareerData.length > 0) {
updateData.sub_careers = JSON.stringify(subCareerData);
} else {
updateData.sub_careers = JSON.stringify([]);
}
await characterApi.updateCharacter(editingCharacter.id, updateData);
message.success('更新成功');
setIsEditModalOpen(false);
editForm.resetFields();
setEditingCharacter(null);
await refreshCharacters();
} catch {
} catch (error) {
console.error('更新失败:', error);
message.error('更新失败');
}
};
@@ -657,6 +717,109 @@ export default function Characters() {
<TextArea rows={3} placeholder={`描述${editingCharacter?.is_organization ? '组织' : '角色'}的背景故事...`} />
</Form.Item>
{!editingCharacter?.is_organization && (mainCareers.length > 0 || subCareers.length > 0) && (
<>
<Divider></Divider>
{mainCareers.length > 0 && (
<Row gutter={16}>
<Col span={16}>
<Form.Item label="主职业" name="main_career_id" tooltip="角色的主要修炼职业">
<Select placeholder="选择主职业" allowClear>
{mainCareers.map(career => (
<Select.Option key={career.id} value={career.id}>
{career.name}{career.max_stage}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="当前阶段" name="main_career_stage" tooltip="主职业当前修炼到的阶段">
<InputNumber
min={1}
max={editForm.getFieldValue('main_career_id') ?
mainCareers.find(c => c.id === editForm.getFieldValue('main_career_id'))?.max_stage || 10
: 10}
style={{ width: '100%' }}
placeholder="阶段"
/>
</Form.Item>
</Col>
</Row>
)}
{subCareers.length > 0 && (
<Form.List name="sub_career_data">
{(fields, { add, remove }) => (
<>
<div style={{ marginBottom: 8 }}>
<Typography.Text strong></Typography.Text>
</div>
<div style={{ maxHeight: '100px', overflowY: 'auto', overflowX: 'hidden', marginBottom: 8, paddingRight: 8 }}>
{fields.map((field) => (
<Row key={field.key} gutter={8} style={{ marginBottom: 8 }}>
<Col span={16}>
<Form.Item
{...field}
name={[field.name, 'career_id']}
rules={[{ required: true, message: '请选择副职业' }]}
style={{ marginBottom: 0 }}
>
<Select placeholder="选择副职业">
{subCareers.map(career => (
<Select.Option key={career.id} value={career.id}>
{career.name}{career.max_stage}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
{...field}
name={[field.name, 'stage']}
rules={[{ required: true, message: '请输入阶段' }]}
style={{ marginBottom: 0 }}
>
<InputNumber
min={1}
max={(() => {
const careerId = editForm.getFieldValue(['sub_career_data', field.name, 'career_id']);
const career = subCareers.find(c => c.id === careerId);
return career?.max_stage || 10;
})()}
placeholder="阶段"
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
<Col span={2}>
<Button
type="text"
danger
onClick={() => remove(field.name)}
style={{ width: '100%' }}
>
</Button>
</Col>
</Row>
))}
</div>
<Button
type="dashed"
onClick={() => add({ career_id: undefined, stage: 1 })}
block
style={{ marginTop: 8 }}
>
+
</Button>
</>
)}
</Form.List>
)}
</>
)}
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => {
@@ -747,6 +910,110 @@ export default function Characters() {
<Form.Item label="角色背景" name="background">
<TextArea rows={3} placeholder="描述角色的背景故事..." />
</Form.Item>
{/* 职业信息 */}
{(mainCareers.length > 0 || subCareers.length > 0) && (
<>
<Divider></Divider>
{mainCareers.length > 0 && (
<Row gutter={16}>
<Col span={16}>
<Form.Item label="主职业" name="main_career_id" tooltip="角色的主要修炼职业">
<Select placeholder="选择主职业" allowClear>
{mainCareers.map(career => (
<Select.Option key={career.id} value={career.id}>
{career.name}{career.max_stage}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="当前阶段" name="main_career_stage" tooltip="主职业当前修炼到的阶段">
<InputNumber
min={1}
max={createForm.getFieldValue('main_career_id') ?
mainCareers.find(c => c.id === createForm.getFieldValue('main_career_id'))?.max_stage || 10
: 10}
style={{ width: '100%' }}
placeholder="阶段"
/>
</Form.Item>
</Col>
</Row>
)}
{subCareers.length > 0 && (
<Form.List name="sub_career_data">
{(fields, { add, remove }) => (
<>
<div style={{ marginBottom: 8 }}>
<Typography.Text strong></Typography.Text>
</div>
<div style={{ maxHeight: '100px', overflowY: 'auto', overflowX: 'hidden', marginBottom: 8, paddingRight: 8 }}>
{fields.map((field) => (
<Row key={field.key} gutter={8} style={{ marginBottom: 8 }}>
<Col span={16}>
<Form.Item
{...field}
name={[field.name, 'career_id']}
rules={[{ required: true, message: '请选择副职业' }]}
style={{ marginBottom: 0 }}
>
<Select placeholder="选择副职业">
{subCareers.map(career => (
<Select.Option key={career.id} value={career.id}>
{career.name}{career.max_stage}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
{...field}
name={[field.name, 'stage']}
rules={[{ required: true, message: '请输入阶段' }]}
style={{ marginBottom: 0 }}
>
<InputNumber
min={1}
max={(() => {
const careerId = createForm.getFieldValue(['sub_career_data', field.name, 'career_id']);
const career = subCareers.find(c => c.id === careerId);
return career?.max_stage || 10;
})()}
placeholder="阶段"
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
<Col span={2}>
<Button
type="text"
danger
onClick={() => remove(field.name)}
style={{ width: '100%' }}
>
</Button>
</Col>
</Row>
))}
</div>
<Button
type="dashed"
onClick={() => add({ career_id: undefined, stage: 1 })}
block
style={{ marginTop: 8 }}
>
+
</Button>
</>
)}
</Form.List>
)}
</>
)}
</>
) : (
<>