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, Dropdown, Form, Input, InputNumber } from 'antd'; import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined, ApiOutlined, MoreOutlined, BulbOutlined, LoadingOutlined, FileSearchOutlined } 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'; import ChangelogFloatingButton from '../components/ChangelogFloatingButton'; const { Title, Text, Paragraph } = Typography; export default function ProjectList() { const navigate = useNavigate(); const { projects, loading } = useStore(); const [modal, contextHolder] = Modal.useModal(); 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 [editModalVisible, setEditModalVisible] = useState(false); const [editingProject, setEditingProject] = useState(null); const [editForm] = Form.useForm(); const [updating, setUpdating] = useState(false); 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 handleEditProject = (project: any) => { setEditingProject(project); editForm.setFieldsValue({ description: project.description || '', target_words: project.target_words || 0, }); setEditModalVisible(true); }; const handleCloseEditModal = () => { setEditModalVisible(false); setEditingProject(null); editForm.resetFields(); }; const handleUpdateProject = async () => { try { const values = await editForm.validateFields(); setUpdating(true); await projectApi.updateProject(editingProject.id, { description: values.description, target_words: values.target_words, }); message.success('项目更新成功'); handleCloseEditModal(); await refreshProjects(); } catch (error: any) { if (error.errorFields) { message.error('请检查表单填写'); } else { message.error('更新失败,请重试'); } } finally { setUpdating(false); } }; const handleEnterProject = async (project: any) => { // 检查项目是否未完成生成(wizard_status为incomplete) if (project.wizard_status === 'incomplete') { // 未完成的项目跳转到生成页面继续生成 navigate(`/wizard?project_id=${project.id}`); } else { // 已完成的项目进入项目详情页 navigate(`/project/${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; // 关闭导出对话框 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); } }; // 计算页脚高度和页面内边距 const isMobile = window.innerWidth <= 768; const footerHeight = isMobile ? 48 : 52; const topPadding = isMobile ? 20 : 32; const sidePadding = isMobile ? 16 : 24; return (
{contextHolder} {/* 固定头部区域 */}
{/* 现代化头部区域 */} {/* 装饰性背景元素 */}
<FireOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 12 }} /> 我的创作空间 ✨ 开启你的小说创作之旅 {window.innerWidth <= 768 ? ( // 移动端:优化布局 {/* 第一行:主要创建按钮 */} {/* 第二行:功能按钮 */} , onClick: handleOpenExportModal, disabled: exportableProjects.length === 0 }, { key: 'import', label: '导入项目', icon: , onClick: () => setImportModalVisible(true) }, { type: 'divider' }, { key: 'prompt-templates', label: '提示词管理', icon: , onClick: () => navigate('/prompt-templates') }, { key: 'mcp', label: 'MCP插件', icon: , onClick: () => navigate('/mcp-plugins') } ] }} placement="bottomRight" trigger={['click']} >
) : ( // PC端:优化后的布局 - 主要按钮 + 下拉菜单 , onClick: handleOpenExportModal, disabled: exportableProjects.length === 0 }, { key: 'import', label: '导入项目', icon: , onClick: () => setImportModalVisible(true) }, { type: 'divider' }, { key: 'prompt-templates', label: '提示词管理', icon: , onClick: () => navigate('/prompt-templates') }, { key: 'mcp', label: 'MCP插件', icon: , onClick: () => navigate('/mcp-plugins') } ] }} placement="bottomRight" > )}
{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 && ( 📚 {!isMobile && 总项目数} {isMobile &&
项目
}
} value={projects.length} valueStyle={{ color: '#fff', fontSize: isMobile ? 18 : 32, fontWeight: 'bold', textShadow: '0 1px 2px rgba(0,0,0,0.1)', textAlign: 'center' }} /> ✍️ {!isMobile && 创作中} {isMobile &&
创作
}
} value={activeProjects} valueStyle={{ color: '#fff', fontSize: isMobile ? 18 : 32, fontWeight: 'bold', textShadow: '0 1px 2px rgba(0,0,0,0.1)', textAlign: 'center' }} /> 📝 {!isMobile && 总字数} {isMobile &&
字数
}
} value={totalWords} formatter={(value) => { const val = Number(value); return isMobile && val > 10000 ? `${(val / 10000).toFixed(1)}w` : val; }} valueStyle={{ color: '#fff', fontSize: isMobile ? 18 : 32, fontWeight: 'bold', textShadow: '0 1px 2px rgba(0,0,0,0.1)', textAlign: 'center' }} />
)}
{/* 可滚动的项目列表区域 */}
{!Array.isArray(projects) || projects.length === 0 ? ( 还没有项目,开始创建你的第一个小说项目吧! } style={{ padding: '80px 0' }} /> ) : ( {projects.map((project) => { const progress = getProgress(project.current_words, project.target_words || 0); return ( }>生成中断 ) : getStatusTag(project.status)} color="transparent" style={{ top: 12, right: 12 }} > handleEnterProject(project)} style={cardStyles.project} styles={{ body: { padding: 0, overflow: 'hidden' } }} {...cardHoverHandlers} > {/* 项目卡片头部 - 添加装饰元素 */}
{/* 装饰性圆圈 */}
{project.title}
{project.genre && ( {project.genre} )}
{project.description || '暂无描述'} {project.target_words && project.target_words > 0 && (
完成进度 {progress}%
)}
{project.current_words >= 1000000 ? (project.current_words / 1000000).toFixed(1) + 'M' : project.current_words >= 1000 ? (project.current_words / 1000).toFixed(1) + 'K' : project.current_words }
已写字数
{project.target_words ? (project.target_words >= 1000000 ? (project.target_words / 1000000).toFixed(1) + 'M' : project.target_words >= 1000 ? (project.target_words / 1000).toFixed(1) + 'K' : project.target_words) : '--' }
目标字数
{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 && ( )}
{/* 编辑项目对话框 */}
); }