import { useState, useEffect, useCallback, useRef } from 'react'; import { useParams } from 'react-router-dom'; import { Card, Table, Button, Tag, Space, Modal, Form, Input, Select, InputNumber, Switch, message, Tooltip, Popconfirm, Statistic, Row, Col, Empty, Divider, Badge, Alert, Pagination, Dropdown, theme } from 'antd'; import type { MenuProps } from 'antd'; import { PlusOutlined, SyncOutlined, EditOutlined, DeleteOutlined, CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, BulbOutlined, EyeOutlined, FlagOutlined, WarningOutlined, ClockCircleOutlined, MoreOutlined, ReloadOutlined, InfoCircleOutlined } from '@ant-design/icons'; import { foreshadowApi, chapterApi, characterApi } from '../services/api'; import type { Foreshadow, ForeshadowCreate, ForeshadowUpdate, ForeshadowStats, ForeshadowStatus, ForeshadowCategory, Chapter, Character } from '../types'; const { TextArea } = Input; const { Option } = Select; // 状态配置 const STATUS_CONFIG: Record = { pending: { label: '待埋入', color: 'default', icon: }, planted: { label: '已埋入', color: 'green', icon: }, resolved: { label: '已回收', color: 'blue', icon: }, partially_resolved: { label: '部分回收', color: 'orange', icon: }, abandoned: { label: '已废弃', color: 'default', icon: }, }; // 分类配置 const CATEGORY_CONFIG: Record = { identity: { label: '身世', color: 'purple' }, mystery: { label: '悬念', color: 'magenta' }, item: { label: '物品', color: 'gold' }, relationship: { label: '关系', color: 'cyan' }, event: { label: '事件', color: 'blue' }, ability: { label: '能力', color: 'green' }, prophecy: { label: '预言', color: 'volcano' }, }; export default function Foreshadows() { const { projectId } = useParams<{ projectId: string }>(); const [loading, setLoading] = useState(false); const [foreshadows, setForeshadows] = useState([]); const [stats, setStats] = useState(null); const [chapters, setChapters] = useState([]); const [characters, setCharacters] = useState([]); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(20); // 筛选条件 const [statusFilter, setStatusFilter] = useState(undefined); const [categoryFilter, setCategoryFilter] = useState(undefined); const [sourceFilter, setSourceFilter] = useState(undefined); // 模态框状态 const [editModalVisible, setEditModalVisible] = useState(false); const [syncModalVisible, setSyncModalVisible] = useState(false); const [detailModalVisible, setDetailModalVisible] = useState(false); const [plantModalVisible, setPlantModalVisible] = useState(false); const [resolveModalVisible, setResolveModalVisible] = useState(false); const [currentForeshadow, setCurrentForeshadow] = useState(null); const [form] = Form.useForm(); const [plantForm] = Form.useForm(); const [resolveForm] = Form.useForm(); const [syncing, setSyncing] = useState(false); // 表格容器引用,用于计算滚动高度 const tableContainerRef = useRef(null); const [tableScrollY, setTableScrollY] = useState(400); const { token } = theme.useToken(); // 加载伏笔列表 const loadForeshadows = useCallback(async () => { if (!projectId) return; setLoading(true); try { const response = await foreshadowApi.getProjectForeshadows(projectId, { status: statusFilter, category: categoryFilter, source_type: sourceFilter, page: currentPage, limit: pageSize, }); setForeshadows(response.items); setTotal(response.total); if (response.stats) { setStats(response.stats); } } catch (error) { console.error('加载伏笔列表失败:', error); } finally { setLoading(false); } }, [projectId, statusFilter, categoryFilter, sourceFilter, currentPage, pageSize]); // 加载章节列表(用于选择) const loadChapters = useCallback(async () => { if (!projectId) return; try { const chaptersData = await chapterApi.getChapters(projectId); setChapters(chaptersData); } catch (error) { console.error('加载章节列表失败:', error); } }, [projectId]); // 加载角色列表(用于关联角色) const loadCharacters = useCallback(async () => { if (!projectId) return; try { const charactersData = await characterApi.getCharacters(projectId); setCharacters(charactersData); } catch (error) { console.error('加载角色列表失败:', error); } }, [projectId]); // 加载统计 const loadStats = useCallback(async () => { if (!projectId) return; try { // 获取当前最大章节号(只计算有内容的章节,与表格显示逻辑保持一致) const chaptersWithContent = chapters.filter(c => c.content); const maxChapter = chaptersWithContent.length > 0 ? Math.max(...chaptersWithContent.map(c => c.chapter_number)) : undefined; const statsData = await foreshadowApi.getForeshadowStats(projectId, maxChapter); setStats(statsData); } catch (error) { console.error('加载统计失败:', error); } }, [projectId, chapters]); useEffect(() => { loadForeshadows(); loadChapters(); loadCharacters(); }, [loadForeshadows, loadChapters, loadCharacters]); // 计算表格滚动高度 useEffect(() => { const calculateTableHeight = () => { if (tableContainerRef.current) { // 获取容器高度,减去表头高度(约55px) const containerHeight = tableContainerRef.current.clientHeight; setTableScrollY(Math.max(containerHeight - 55, 200)); } }; calculateTableHeight(); window.addEventListener('resize', calculateTableHeight); // 延迟再计算一次,确保布局完成 const timer = setTimeout(calculateTableHeight, 100); return () => { window.removeEventListener('resize', calculateTableHeight); clearTimeout(timer); }; }, [stats]); // stats 变化时重新计算(因为统计卡片高度可能变化) useEffect(() => { if (chapters.length > 0) { loadStats(); } }, [chapters, loadStats]); // 创建/编辑伏笔 const handleSave = async (values: ForeshadowCreate | ForeshadowUpdate) => { try { if (currentForeshadow) { await foreshadowApi.updateForeshadow(currentForeshadow.id, values as ForeshadowUpdate); message.success('伏笔更新成功'); } else { await foreshadowApi.createForeshadow({ ...values, project_id: projectId!, } as ForeshadowCreate); message.success('伏笔创建成功'); } setEditModalVisible(false); form.resetFields(); setCurrentForeshadow(null); loadForeshadows(); } catch (error) { console.error('保存伏笔失败:', error); } }; // 删除伏笔 const handleDelete = async (id: string) => { try { await foreshadowApi.deleteForeshadow(id); message.success('伏笔删除成功'); loadForeshadows(); } catch (error) { console.error('删除伏笔失败:', error); } }; // 标记埋入 const handlePlant = async (values: { chapter_id: string; hint_text?: string }) => { if (!currentForeshadow) return; const chapter = chapters.find(c => c.id === values.chapter_id); if (!chapter) return; try { await foreshadowApi.plantForeshadow(currentForeshadow.id, { chapter_id: values.chapter_id, chapter_number: chapter.chapter_number, hint_text: values.hint_text, }); message.success('伏笔已标记为埋入'); setPlantModalVisible(false); plantForm.resetFields(); setCurrentForeshadow(null); loadForeshadows(); } catch (error) { console.error('标记埋入失败:', error); } }; // 标记回收 const handleResolve = async (values: { chapter_id: string; resolution_text?: string; is_partial?: boolean }) => { if (!currentForeshadow) return; const chapter = chapters.find(c => c.id === values.chapter_id); if (!chapter) return; try { await foreshadowApi.resolveForeshadow(currentForeshadow.id, { chapter_id: values.chapter_id, chapter_number: chapter.chapter_number, resolution_text: values.resolution_text, is_partial: values.is_partial, }); message.success('伏笔已标记为回收'); setResolveModalVisible(false); resolveForm.resetFields(); setCurrentForeshadow(null); loadForeshadows(); } catch (error) { console.error('标记回收失败:', error); } }; // 标记废弃 const handleAbandon = async (id: string) => { try { await foreshadowApi.abandonForeshadow(id); message.success('伏笔已标记为废弃'); loadForeshadows(); } catch (error) { console.error('标记废弃失败:', error); } }; // 从分析同步 const handleSync = async () => { if (!projectId) return; setSyncing(true); try { const result = await foreshadowApi.syncFromAnalysis(projectId, { auto_set_planted: true, }); message.success(`同步完成: 新增${result.synced_count}个伏笔, 跳过${result.skipped_count}个`); setSyncModalVisible(false); loadForeshadows(); } catch (error) { console.error('同步失败:', error); } finally { setSyncing(false); } }; // 打开编辑模态框 const openEditModal = (foreshadow?: Foreshadow) => { setCurrentForeshadow(foreshadow || null); if (foreshadow) { // 确保数组类型字段不为null form.setFieldsValue({ ...foreshadow, tags: foreshadow.tags || [], related_characters: foreshadow.related_characters || [], }); } else { form.resetFields(); } setEditModalVisible(true); }; // 打开详情模态框 const openDetailModal = (foreshadow: Foreshadow) => { setCurrentForeshadow(foreshadow); setDetailModalVisible(true); }; // 打开埋入模态框 const openPlantModal = (foreshadow: Foreshadow) => { setCurrentForeshadow(foreshadow); plantForm.resetFields(); setPlantModalVisible(true); }; // 打开回收模态框 const openResolveModal = (foreshadow: Foreshadow) => { setCurrentForeshadow(foreshadow); resolveForm.resetFields(); setResolveModalVisible(true); }; // 计算紧急程度 const getUrgencyBadge = (foreshadow: Foreshadow) => { if (foreshadow.status !== 'planted' || !foreshadow.target_resolve_chapter_number) { return null; } const chaptersWithContent = chapters.filter(c => c.content); const currentMaxChapter = chaptersWithContent.length > 0 ? Math.max(...chaptersWithContent.map(c => c.chapter_number)) : 0; const remaining = foreshadow.target_resolve_chapter_number - currentMaxChapter; if (remaining < 0) { return ; } else if (remaining <= 3) { return ; } return null; }; // 状态排序优先级 const statusOrder: Record = { planted: 1, // 已埋入优先(需要关注回收) pending: 2, // 待埋入次之 partially_resolved: 3, resolved: 4, abandoned: 5, }; // 表格列定义 const columns = [ { title: '状态', dataIndex: 'status', key: 'status', width: 100, sorter: (a: Foreshadow, b: Foreshadow) => statusOrder[a.status] - statusOrder[b.status], render: (status: ForeshadowStatus) => { const config = STATUS_CONFIG[status]; return ( {config.label} ); }, }, { title: '标题', dataIndex: 'title', key: 'title', ellipsis: true, sorter: (a: Foreshadow, b: Foreshadow) => a.title.localeCompare(b.title, 'zh-CN'), render: (title: string, record: Foreshadow) => ( openDetailModal(record)}>{title} {record.is_long_term && ( 长线 )} {getUrgencyBadge(record)} ), }, { title: '分类', dataIndex: 'category', key: 'category', width: 80, sorter: (a: Foreshadow, b: Foreshadow) => { const catA = a.category || ''; const catB = b.category || ''; return catA.localeCompare(catB, 'zh-CN'); }, render: (category?: ForeshadowCategory) => { if (!category) return '-'; const config = CATEGORY_CONFIG[category]; return config ? {config.label} : category; }, }, { title: '埋入章节', dataIndex: 'plant_chapter_number', key: 'plant_chapter_number', width: 120, sorter: (a: Foreshadow, b: Foreshadow) => { const valA = a.plant_chapter_number ?? 999999; const valB = b.plant_chapter_number ?? 999999; return valA - valB; }, defaultSortOrder: 'ascend' as const, render: (num?: number) => num ? `第${num}章` : '-', }, { title: '计划回收', dataIndex: 'target_resolve_chapter_number', key: 'target_resolve_chapter_number', width: 120, sorter: (a: Foreshadow, b: Foreshadow) => { const valA = a.target_resolve_chapter_number ?? 999999; const valB = b.target_resolve_chapter_number ?? 999999; return valA - valB; }, render: (num?: number) => num ? `第${num}章` : '-', }, { title: '重要性', dataIndex: 'importance', key: 'importance', width: 100, sorter: (a: Foreshadow, b: Foreshadow) => a.importance - b.importance, render: (importance: number) => { const stars = Math.round(importance * 5); return '★'.repeat(stars) + '☆'.repeat(5 - stars); }, }, { title: '来源', dataIndex: 'source_type', key: 'source_type', width: 80, sorter: (a: Foreshadow, b: Foreshadow) => { const srcA = a.source_type || ''; const srcB = b.source_type || ''; return srcA.localeCompare(srcB); }, render: (source?: string) => ( {source === 'analysis' ? '分析' : '手动'} ), }, { title: '操作', key: 'actions', width: 200, render: (_: unknown, record: Foreshadow) => ( {/* 伏笔列表 - 表格内容可滚动,表头固定 */}
, }} /> {/* 分页器 - 固定在底部居中 */}
{ setCurrentPage(page); if (size !== pageSize) { setPageSize(size); } }} showSizeChanger showTotal={(total) => `共 ${total} 条`} showQuickJumper />
{/* 创建/编辑模态框 */} { setEditModalVisible(false); setCurrentForeshadow(null); form.resetFields(); }} onOk={() => form.submit()} width={800} destroyOnClose >