import { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, Slider, Input, Tabs, AutoComplete } from 'antd'; import { PlusOutlined, ApartmentOutlined, UserOutlined, EditOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import axios from 'axios'; const { TextArea } = Input; interface Relationship { id: string; character_from_id: string; character_to_id: string; relationship_name: string; intimacy_level: number; status: string; description?: string; source: string; } interface RelationshipType { id: number; name: string; category: string; reverse_name?: string; icon?: string; } interface Character { id: string; name: string; is_organization: boolean; } export default function Relationships() { const { projectId } = useParams<{ projectId: string }>(); const { currentProject } = useStore(); const [relationships, setRelationships] = useState([]); const [relationshipTypes, setRelationshipTypes] = useState([]); const [characters, setCharacters] = useState([]); const [loading, setLoading] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [editingRelationship, setEditingRelationship] = useState(null); const [form] = Form.useForm(); const [modal, contextHolder] = Modal.useModal(); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [pageSize, setPageSize] = useState(10); const [currentPage, setCurrentPage] = useState(1); useEffect(() => { const handleResize = () => { setIsMobile(window.innerWidth <= 768); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); useEffect(() => { if (projectId) { loadData(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId]); const loadData = async () => { setLoading(true); try { const [relsRes, typesRes, charsRes] = await Promise.all([ axios.get(`/api/relationships/project/${projectId}`), axios.get('/api/relationships/types'), axios.get(`/api/characters?project_id=${projectId}`) ]); setRelationships(relsRes.data); setRelationshipTypes(typesRes.data); setCharacters(charsRes.data.items || []); } catch (error) { message.error('加载数据失败'); console.error(error); } finally { setLoading(false); } }; const handleCreateRelationship = async (values: { character_from_id: string; character_to_id: string; relationship_name: string; intimacy_level: number; status: string; description?: string; }) => { try { await axios.post('/api/relationships/', { project_id: projectId, ...values }); message.success('关系创建成功'); setIsModalOpen(false); form.resetFields(); loadData(); } catch (error) { message.error('创建关系失败'); console.error(error); } }; const handleEditRelationship = (record: Relationship) => { setEditingRelationship(record); setIsEditMode(true); form.setFieldsValue({ character_from_id: record.character_from_id, character_to_id: record.character_to_id, relationship_name: record.relationship_name, intimacy_level: record.intimacy_level, status: record.status, description: record.description, }); setIsModalOpen(true); }; const handleUpdateRelationship = async (values: { character_from_id: string; character_to_id: string; relationship_name: string; intimacy_level: number; status: string; description?: string; }) => { if (!editingRelationship) return; try { await axios.put(`/api/relationships/${editingRelationship.id}`, { relationship_name: values.relationship_name, intimacy_level: values.intimacy_level, status: values.status, description: values.description, }); message.success('关系更新成功'); setIsModalOpen(false); setIsEditMode(false); setEditingRelationship(null); form.resetFields(); loadData(); } catch (error) { message.error('更新关系失败'); console.error(error); } }; const handleDeleteRelationship = async (id: string) => { modal.confirm({ title: '确认删除', content: '确定要删除这条关系吗?', centered: true, okText: '删除', okType: 'danger', cancelText: '取消', onOk: async () => { try { await axios.delete(`/api/relationships/${id}`); message.success('关系删除成功'); loadData(); } catch (error) { message.error('删除失败'); console.error(error); } } }); }; const getCharacterName = (id: string) => { const char = characters.find(c => c.id === id); return char?.name || '未知'; }; const getIntimacyColor = (level: number) => { if (level >= 75) return 'green'; if (level >= 50) return 'blue'; if (level >= 25) return 'orange'; if (level >= 0) return 'volcano'; return 'red'; }; const getStatusColor = (status: string) => { const colors: Record = { active: 'green', broken: 'red', past: 'default', complicated: 'orange' }; return colors[status] || 'default'; }; const getCategoryColor = (category: string) => { const colors: Record = { family: 'magenta', social: 'blue', hostile: 'red', professional: 'cyan' }; return colors[category] || 'default'; }; const columns = [ { title: '角色A', dataIndex: 'character_from_id', key: 'from', render: (id: string) => ( } color="blue"> {getCharacterName(id)} ), width: 120, }, { title: '关系', dataIndex: 'relationship_name', key: 'relationship', render: (name: string) => {name}, width: 120, }, { title: '角色B', dataIndex: 'character_to_id', key: 'to', render: (id: string) => ( } color="purple"> {getCharacterName(id)} ), width: 120, }, { title: '亲密度', dataIndex: 'intimacy_level', key: 'intimacy', render: (level: number) => ( {level} ), width: 80, }, { title: '状态', dataIndex: 'status', key: 'status', render: (status: string) => ( {status} ), width: 80, }, { title: '来源', dataIndex: 'source', key: 'source', render: (source: string) => ( {source === 'ai' ? 'AI生成' : '手动创建'} ), width: 100, }, { title: '操作', key: 'action', render: (_: unknown, record: Relationship) => ( ), width: 140, fixed: isMobile ? ('right' as const) : undefined, }, ]; // 按类别分组关系类型 const groupedTypes = relationshipTypes.reduce((acc, type) => { if (!acc[type.category]) { acc[type.category] = []; } acc[type.category].push(type); return acc; }, {} as Record); const categoryLabels: Record = { family: '家族关系', social: '社交关系', professional: '职业关系', hostile: '敌对关系' }; return ( <> {contextHolder}
关系管理 {!isMobile && {currentProject?.title}} } extra={ } > `共 ${total} 条`, simple: isMobile, onChange: (page, size) => { setCurrentPage(page); if (size !== pageSize) { setPageSize(size); setCurrentPage(1); // 切换每页条数时重置到第一页 } }, onShowSizeChange: (_, size) => { setPageSize(size); setCurrentPage(1); } }} scroll={{ x: 700, y: isMobile ? 'calc(100vh - 360px)' : 'calc(100vh - 440px)' }} size={isMobile ? 'small' : 'middle'} /> ), }, { key: 'types', label: `关系类型 (${relationshipTypes.length})`, children: (
{Object.entries(groupedTypes).map(([category, types]) => ( {types.map(type => ( {type.icon} {type.name} {type.reverse_name && ` ↔ ${type.reverse_name}`} ))} ))}
), }, ]} />
{ setIsModalOpen(false); setIsEditMode(false); setEditingRelationship(null); form.resetFields(); }} footer={null} centered={!isMobile} width={isMobile ? '100%' : 600} style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined} styles={isMobile ? { body: { maxHeight: 'calc(100vh - 110px)', overflowY: 'auto' } } : undefined} >
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()) } options={characters .filter(c => !c.is_organization) .map(c => ({ label: c.name, value: c.id }))} />