import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, Button, Empty, Modal, message, Spin, Row, Col, Statistic, Space, Tag, Progress, Typography, Tooltip, Badge, Alert, Upload, Checkbox, Divider, Switch } from 'antd'; import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons'; import { projectApi } from '../services/api'; import { useStore } from '../store'; import { useProjectSync } from '../store/hooks'; import type { ReactNode } from 'react'; import { cardStyles, cardHoverHandlers, gridConfig } from '../components/CardStyles'; import UserMenu from '../components/UserMenu'; const { Title, Text, Paragraph } = Typography; export default function ProjectList() { const navigate = useNavigate(); const { projects, loading } = useStore(); const [showApiTip, setShowApiTip] = useState(true); const [importModalVisible, setImportModalVisible] = useState(false); const [exportModalVisible, setExportModalVisible] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [validationResult, setValidationResult] = useState(null); const [importing, setImporting] = useState(false); const [validating, setValidating] = useState(false); const [exporting, setExporting] = useState(false); const [selectedProjectIds, setSelectedProjectIds] = useState([]); const [exportOptions, setExportOptions] = useState({ includeWritingStyles: true, includeGenerationHistory: true, }); const { refreshProjects, deleteProject } = useProjectSync(); useEffect(() => { refreshProjects(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const handleVisibilityChange = () => { if (!document.hidden) { refreshProjects(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleDelete = (id: string) => { const isMobile = window.innerWidth <= 768; Modal.confirm({ title: '确认删除', content: '删除项目将同时删除所有相关数据,此操作不可恢复。确定要删除吗?', okText: '确定', cancelText: '取消', okType: 'danger', centered: true, ...(isMobile && { style: { top: 'auto' } }), onOk: async () => { try { await deleteProject(id); message.success('项目删除成功'); } catch { message.error('删除项目失败'); } }, }); }; const handleEnterProject = (id: string) => { const project = projects.find(p => p.id === id); if (project) { console.log('项目信息:', { id: project.id, title: project.title, wizard_status: project.wizard_status, wizard_step: project.wizard_step }); if (project.wizard_status === 'incomplete' || !project.wizard_status) { console.log('向导未完成,跳转到向导页面'); navigate(`/wizard?projectId=${id}&step=${project.wizard_step || 0}`); } else { console.log('向导已完成,进入项目管理界面'); navigate(`/project/${id}`); } } }; const getStatusTag = (status: string) => { const statusConfig: Record = { planning: { color: 'blue', text: '规划中', icon: }, writing: { color: 'green', text: '创作中', icon: }, revising: { color: 'orange', text: '修改中', icon: }, completed: { color: 'purple', text: '已完成', icon: }, }; const config = statusConfig[status] || statusConfig.planning; return ( {config.text} ); }; const getProgress = (current: number, target: number) => { if (!target) return 0; return Math.min(Math.round((current / target) * 100), 100); }; const getProgressColor = (progress: number) => { if (progress >= 80) return '#52c41a'; if (progress >= 50) return '#1890ff'; if (progress >= 20) return '#faad14'; return '#ff4d4f'; }; const formatDate = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const diff = now.getTime() - date.getTime(); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) return '今天'; if (days === 1) return '昨天'; if (days < 7) return `${days}天前`; if (days < 30) return `${Math.floor(days / 7)}周前`; return date.toLocaleDateString('zh-CN'); }; const totalWords = projects.reduce((sum, p) => sum + (p.current_words || 0), 0); const activeProjects = projects.filter(p => p.status === 'writing').length; // 处理文件选择 const handleFileSelect = async (file: File) => { setSelectedFile(file); setValidationResult(null); // 验证文件 try { setValidating(true); const result = await projectApi.validateImportFile(file); setValidationResult(result); if (!result.valid) { message.error('文件验证失败'); } } catch (error) { console.error('验证失败:', error); message.error('文件验证失败'); } finally { setValidating(false); } return false; // 阻止自动上传 }; // 处理导入 const handleImport = async () => { if (!selectedFile || !validationResult?.valid) { message.warning('请选择有效的导入文件'); return; } try { setImporting(true); const result = await projectApi.importProject(selectedFile); if (result.success) { message.success(`项目导入成功!${result.message}`); setImportModalVisible(false); setSelectedFile(null); setValidationResult(null); // 刷新项目列表 await refreshProjects(); // 跳转到新项目 if (result.project_id) { navigate(`/project/${result.project_id}`); } } else { message.error(result.message || '导入失败'); } } catch (error) { console.error('导入失败:', error); message.error('导入失败,请重试'); } finally { setImporting(false); } }; // 关闭导入对话框 const handleCloseImportModal = () => { setImportModalVisible(false); setSelectedFile(null); setValidationResult(null); }; // 打开导出对话框 const handleOpenExportModal = () => { setExportModalVisible(true); setSelectedProjectIds([]); }; // 获取可导出的项目(过滤掉向导未完成的项目) const exportableProjects = projects.filter(p => p.wizard_status === 'completed'); // 关闭导出对话框 const handleCloseExportModal = () => { setExportModalVisible(false); setSelectedProjectIds([]); }; // 切换项目选择 const handleToggleProject = (projectId: string) => { setSelectedProjectIds(prev => prev.includes(projectId) ? prev.filter(id => id !== projectId) : [...prev, projectId] ); }; // 全选/取消全选 const handleToggleAll = () => { if (selectedProjectIds.length === exportableProjects.length) { setSelectedProjectIds([]); } else { setSelectedProjectIds(exportableProjects.map(p => p.id)); } }; // 执行导出 const handleExport = async () => { if (selectedProjectIds.length === 0) { message.warning('请至少选择一个项目'); return; } try { setExporting(true); if (selectedProjectIds.length === 1) { // 单个项目导出 const projectId = selectedProjectIds[0]; const project = projects.find(p => p.id === projectId); await projectApi.exportProjectData(projectId, { include_generation_history: exportOptions.includeGenerationHistory, include_writing_styles: exportOptions.includeWritingStyles }); message.success(`项目 "${project?.title}" 导出成功`); } else { // 批量导出 let successCount = 0; let failCount = 0; for (const projectId of selectedProjectIds) { try { await projectApi.exportProjectData(projectId, { include_generation_history: exportOptions.includeGenerationHistory, include_writing_styles: exportOptions.includeWritingStyles }); successCount++; // 添加延迟避免浏览器阻止多个下载 await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) { console.error(`导出项目 ${projectId} 失败:`, error); failCount++; } } if (failCount === 0) { message.success(`成功导出 ${successCount} 个项目`); } else { message.warning(`导出完成:成功 ${successCount} 个,失败 ${failCount} 个`); } } handleCloseExportModal(); } catch (error) { console.error('导出失败:', error); message.error('导出失败,请重试'); } finally { setExporting(false); } }; return (
<FireOutlined style={{ color: '#ff4d4f', marginRight: 8 }} /> 我的创作空间 开启你的小说创作之旅 {window.innerWidth <= 768 ? ( // 移动端:按钮分两行显示 ) : ( // PC端:原有布局 )} {showApiTip && projects.length === 0 && ( 首次使用提示 } description={ 在开始创作之前,请先配置您的AI接口。系统支持OpenAI和Anthropic两种接口。 } type="info" showIcon={false} closable closeIcon={} onClose={() => setShowApiTip(false)} style={{ marginTop: window.innerWidth <= 768 ? 16 : 24, borderRadius: 12, background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)', border: '1px solid #91d5ff' }} /> )} {projects.length > 0 && ( 总项目数} value={projects.length} prefix={} suffix="个" valueStyle={{ color: '#1890ff', fontSize: window.innerWidth <= 768 ? 20 : 28, fontWeight: 'bold' }} /> 创作中} value={activeProjects} prefix={} suffix="个" valueStyle={{ color: '#52c41a', fontSize: window.innerWidth <= 768 ? 20 : 28, fontWeight: 'bold' }} /> 总字数} value={totalWords} prefix={} suffix="字" valueStyle={{ color: '#faad14', fontSize: window.innerWidth <= 768 ? 20 : 28, fontWeight: 'bold' }} /> )}
{!Array.isArray(projects) || projects.length === 0 ? ( 还没有项目,开始创建你的第一个小说项目吧! } style={{ padding: '80px 0' }} /> ) : ( {projects.map((project) => { const progress = getProgress(project.current_words, project.target_words || 0); const isWizardComplete = project.wizard_status === 'completed'; return ( }>创建中} color="transparent" style={{ top: 12, right: 12 }} > handleEnterProject(project.id)} style={cardStyles.project} styles={{ body: { padding: 0, overflow: 'hidden' } }} {...cardHoverHandlers} >
{project.title}
{project.genre && ( {project.genre} )}
{project.description || '暂无描述'} {isWizardComplete ? ( <> {project.target_words && project.target_words > 0 && (
完成进度 {progress}%
)}
{(project.current_words / 1000).toFixed(1)}K
已写字数
{project.target_words ? (project.target_words / 1000).toFixed(0) + 'K' : '--'}
目标字数
) : (
项目创建中
点击继续创建向导
)}
{formatDate(project.updated_at)}
); })}
)}
{/* 导入项目对话框 */}

选择之前导出的 JSON 格式项目文件

{ setSelectedFile(null); setValidationResult(null); }} fileList={selectedFile ? [{ uid: '-1', name: selectedFile.name, status: 'done' }] as any : []} >
{validating && (
)} {validationResult && (
{validationResult.valid ? '✓ 文件验证通过' : '✗ 文件验证失败'}
{validationResult.project_name && (
项目名称: {validationResult.project_name}
)} {validationResult.statistics && Object.keys(validationResult.statistics).length > 0 && (
数据统计:
{validationResult.statistics.chapters > 0 && ( 章节: {validationResult.statistics.chapters} )} {validationResult.statistics.characters > 0 && ( 角色: {validationResult.statistics.characters} )} {validationResult.statistics.outlines > 0 && ( 大纲: {validationResult.statistics.outlines} )} {validationResult.statistics.relationships > 0 && ( 关系: {validationResult.statistics.relationships} )}
)} {validationResult.errors && validationResult.errors.length > 0 && (
错误:
    {validationResult.errors.map((error: string, index: number) => (
  • {error}
  • ))}
)} {validationResult.warnings && validationResult.warnings.length > 0 && (
警告:
    {validationResult.warnings.map((warning: string, index: number) => (
  • {warning}
  • ))}
)}
)}
{/* 导出项目对话框 */} 0 ? `导出 (${selectedProjectIds.length})` : '导出'} cancelText="取消" width={window.innerWidth <= 768 ? '90%' : 700} centered okButtonProps={{ disabled: selectedProjectIds.length === 0 }} styles={{ body: { maxHeight: window.innerWidth <= 768 ? '70vh' : 'auto', overflowY: 'auto', padding: window.innerWidth <= 768 ? '16px' : '24px' } }} > {/* 导出选项 */} 导出选项
setExportOptions(prev => ({ ...prev, includeWritingStyles: checked }))} style={{ flexShrink: 0, height: window.innerWidth <= 768 ? 16 : 22, minHeight: window.innerWidth <= 768 ? 16 : 22, lineHeight: window.innerWidth <= 768 ? '16px' : '22px' }} /> 包含写作风格
setExportOptions(prev => ({ ...prev, includeGenerationHistory: checked }))} style={{ flexShrink: 0, height: window.innerWidth <= 768 ? 16 : 22, minHeight: window.innerWidth <= 768 ? 16 : 22, lineHeight: window.innerWidth <= 768 ? '16px' : '22px' }} /> 包含生成历史
{/* 项目列表 */}
选择要导出的项目 {exportableProjects.length > 0 && ({exportableProjects.length}个可导出)} 0} indeterminate={selectedProjectIds.length > 0 && selectedProjectIds.length < exportableProjects.length} onChange={handleToggleAll} style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }} > 全选
{exportableProjects.length === 0 ? ( ) : ( {exportableProjects.map((project) => ( handleToggleProject(project.id)} >
handleToggleProject(project.id)} onClick={(e) => e.stopPropagation()} />
{project.title} {project.genre && ( {project.genre} )} {getStatusTag(project.status)}
{project.current_words || 0} 字 {project.description && ` · ${project.description.substring(0, window.innerWidth <= 768 ? 30 : 50)}${project.description.length > (window.innerWidth <= 768 ? 30 : 50) ? '...' : ''}`}
{window.innerWidth > 768 && ( {formatDate(project.updated_at)} )}
))}
)}
{selectedProjectIds.length > 0 && ( )}
); }