import { useState, useEffect, useRef } from 'react'; import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space, InputNumber, Checkbox } from 'antd'; import { ThunderboltOutlined, UserOutlined, TeamOutlined, PlusOutlined, ExportOutlined, ImportOutlined, DownloadOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { useCharacterSync } from '../store/hooks'; import { characterGridConfig } from '../components/CardStyles'; import { CharacterCard } from '../components/CharacterCard'; import { SSELoadingOverlay } from '../components/SSELoadingOverlay'; import type { Character, ApiError } from '../types'; import { characterApi } from '../services/api'; import { SSEPostClient } from '../utils/sseClient'; import api from '../services/api'; const { Title } = Typography; const { TextArea } = Input; interface Career { id: string; name: string; type: 'main' | 'sub'; max_stage: number; } // 副职业数据类型 interface SubCareerData { career_id: string; stage: number; } // 角色创建表单值类型 interface CharacterFormValues { name: string; age?: string; gender?: string; role_type?: string; personality?: string; appearance?: string; relationships?: string; background?: string; main_career_id?: string; main_career_stage?: number; sub_career_data?: SubCareerData[]; // 组织字段 organization_type?: string; organization_purpose?: string; organization_members?: string; power_level?: number; location?: string; motto?: string; color?: string; } // 角色创建数据类型 interface CharacterCreateData { project_id: string; name: string; is_organization: boolean; age?: string; gender?: string; role_type?: string; personality?: string; appearance?: string; relationships?: string; background?: string; main_career_id?: string; main_career_stage?: number; sub_careers?: string; organization_type?: string; organization_purpose?: string; organization_members?: string; power_level?: number; location?: string; motto?: string; color?: string; } // 角色更新数据类型 interface CharacterUpdateData { name?: string; age?: string; gender?: string; role_type?: string; personality?: string; appearance?: string; relationships?: string; background?: string; main_career_id?: string; main_career_stage?: number; sub_careers?: string; organization_type?: string; organization_purpose?: string; organization_members?: string; power_level?: number; location?: string; motto?: string; color?: string; } export default function Characters() { const { currentProject, characters } = useStore(); const [isGenerating, setIsGenerating] = useState(false); const [progress, setProgress] = useState(0); const [progressMessage, setProgressMessage] = useState(''); const [activeTab, setActiveTab] = useState<'all' | 'character' | 'organization'>('all'); const [generateForm] = Form.useForm(); const [generateOrgForm] = Form.useForm(); const [createForm] = Form.useForm(); const [editForm] = Form.useForm(); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [createType, setCreateType] = useState<'character' | 'organization'>('character'); const [editingCharacter, setEditingCharacter] = useState(null); const [mainCareers, setMainCareers] = useState([]); const [subCareers, setSubCareers] = useState([]); const [selectedCharacters, setSelectedCharacters] = useState([]); const [isImportModalOpen, setIsImportModalOpen] = useState(false); const fileInputRef = useRef(null); const { refreshCharacters, deleteCharacter } = useCharacterSync(); 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 api.get('/careers', { params: { project_id: currentProject.id } }); setMainCareers(response.main_careers || []); setSubCareers(response.sub_careers || []); } catch (error) { console.error('获取职业列表失败:', error); } }; if (!currentProject) return null; const handleDeleteCharacter = async (id: string) => { try { await deleteCharacter(id); message.success('删除成功'); } catch { message.error('删除失败'); } }; const handleGenerate = async (values: { name?: string; role_type: string; background?: string }) => { try { setIsGenerating(true); setProgress(0); setProgressMessage('准备生成角色...'); const client = new SSEPostClient( '/api/characters/generate-stream', { project_id: currentProject.id, name: values.name, role_type: values.role_type, background: values.background, }, { onProgress: (msg, prog) => { setProgress(prog); setProgressMessage(msg); }, onResult: (data) => { console.log('角色生成完成:', data); }, onError: (error) => { message.error(`生成失败: ${error}`); }, onComplete: () => { setProgress(100); setProgressMessage('生成完成!'); } } ); await client.connect(); message.success('AI生成角色成功'); Modal.destroyAll(); await refreshCharacters(); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'AI生成失败'; message.error(errorMessage); } finally { setTimeout(() => { setIsGenerating(false); setProgress(0); setProgressMessage(''); }, 500); } }; const handleGenerateOrganization = async (values: { name?: string; organization_type?: string; background?: string; requirements?: string; }) => { try { setIsGenerating(true); setProgress(0); setProgressMessage('准备生成组织...'); const client = new SSEPostClient( '/api/organizations/generate-stream', { project_id: currentProject.id, name: values.name, organization_type: values.organization_type, background: values.background, requirements: values.requirements, }, { onProgress: (msg, prog) => { setProgress(prog); setProgressMessage(msg); }, onResult: (data) => { console.log('组织生成完成:', data); }, onError: (error) => { message.error(`生成失败: ${error}`); }, onComplete: () => { setProgress(100); setProgressMessage('生成完成!'); } } ); await client.connect(); message.success('AI生成组织成功'); Modal.destroyAll(); await refreshCharacters(); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'AI生成失败'; message.error(errorMessage); } finally { setTimeout(() => { setIsGenerating(false); setProgress(0); setProgressMessage(''); }, 500); } }; const handleCreateCharacter = async (values: CharacterFormValues) => { try { const createData: CharacterCreateData = { project_id: currentProject.id, name: values.name, is_organization: createType === 'organization', }; if (createType === 'character') { // 角色字段 createData.age = values.age; createData.gender = values.gender; createData.role_type = values.role_type || 'supporting'; createData.personality = values.personality; 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; createData.organization_purpose = values.organization_purpose; createData.organization_members = values.organization_members; createData.background = values.background; createData.power_level = values.power_level; createData.location = values.location; createData.motto = values.motto; createData.color = values.color; createData.role_type = 'supporting'; // 组织默认为配角 } await characterApi.createCharacter(createData); message.success(`${createType === 'character' ? '角色' : '组织'}创建成功`); setIsCreateModalOpen(false); createForm.resetFields(); await refreshCharacters(); } catch { message.error('创建失败'); } }; const handleEditCharacter = (character: Character) => { setEditingCharacter(character); // 提取副职业数据(包含职业ID和阶段) const subCareerData: SubCareerData[] = character.sub_careers?.map((sc) => ({ career_id: sc.career_id, stage: sc.stage || 1 })) || []; editForm.setFieldsValue({ ...character, sub_career_data: subCareerData }); setIsEditModalOpen(true); }; const handleUpdateCharacter = async (values: CharacterFormValues) => { if (!editingCharacter) return; try { // 提取副职业数据,剩余的作为更新数据 const { sub_career_data: subCareerData, ...restValues } = values; const updateData: CharacterUpdateData = { ...restValues }; // 转换为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 (error) { console.error('更新失败:', error); message.error('更新失败'); } }; const handleDeleteCharacterWrapper = (id: string) => { handleDeleteCharacter(id); }; // 导出选中的角色/组织 const handleExportSelected = async () => { if (selectedCharacters.length === 0) { message.warning('请至少选择一个角色或组织'); return; } try { await characterApi.exportCharacters(selectedCharacters); message.success(`成功导出 ${selectedCharacters.length} 个角色/组织`); setSelectedCharacters([]); } catch (error) { message.error('导出失败'); console.error('导出错误:', error); } }; // 导出单个角色/组织 const handleExportSingle = async (characterId: string) => { try { await characterApi.exportCharacters([characterId]); message.success('导出成功'); } catch (error) { message.error('导出失败'); console.error('导出错误:', error); } }; // 处理文件选择 const handleFileSelect = async (file: File) => { try { // 验证文件 const validation = await characterApi.validateImportCharacters(file); if (!validation.valid) { modal.error({ title: '文件验证失败', centered: true, content: (
{validation.errors.map((error, index) => (
• {error}
))}
), }); return; } // 显示预览对话框 modal.confirm({ title: '导入预览', width: 500, centered: true, content: (

文件版本: {validation.version}

将要导入:

  • 角色: {validation.statistics.characters} 个
  • 组织: {validation.statistics.organizations} 个
{validation.warnings.length > 0 && ( <>

⚠️ 警告:

    {validation.warnings.map((warning, index) => (
  • {warning}
  • ))}
)}
), okText: '确认导入', cancelText: '取消', onOk: async () => { try { const result = await characterApi.importCharacters(currentProject.id, file); if (result.success) { // 显示导入结果 modal.success({ title: '导入完成', width: 600, centered: true, content: (

✅ 成功导入: {result.statistics.imported} 个

{result.details.imported_characters.length > 0 && ( <>

角色:

    {result.details.imported_characters.map((name, index) => (
  • {name}
  • ))}
)} {result.details.imported_organizations.length > 0 && ( <>

组织:

    {result.details.imported_organizations.map((name, index) => (
  • {name}
  • ))}
)} {result.statistics.skipped > 0 && ( <>

⚠️ 跳过: {result.statistics.skipped} 个

    {result.details.skipped.map((name, index) => (
  • {name}
  • ))}
)} {result.warnings.length > 0 && ( <>

⚠️ 警告:

    {result.warnings.map((warning, index) => (
  • {warning}
  • ))}
)} {result.details.errors.length > 0 && ( <>

❌ 失败: {result.statistics.errors} 个

    {result.details.errors.map((error, index) => (
  • {error}
  • ))}
)}
), }); // 刷新列表 await refreshCharacters(); setIsImportModalOpen(false); } else { message.error(result.message || '导入失败'); } } catch (error: unknown) { const apiError = error as ApiError; message.error(apiError.response?.data?.detail || '导入失败'); console.error('导入错误:', error); } }, }); } catch (error: unknown) { const apiError = error as ApiError; message.error(apiError.response?.data?.detail || '文件验证失败'); console.error('验证错误:', error); } }; // 切换选择 const toggleSelectCharacter = (id: string) => { setSelectedCharacters(prev => prev.includes(id) ? prev.filter(cid => cid !== id) : [...prev, id] ); }; // 全选/取消全选 const toggleSelectAll = () => { if (selectedCharacters.length === displayList.length) { setSelectedCharacters([]); } else { setSelectedCharacters(displayList.map(c => c.id)); } }; const showGenerateModal = () => { modal.confirm({ title: 'AI生成角色', width: 600, centered: true, content: (