2025-10-30 22:01:10 +08:00
|
|
|
|
import { useEffect, useState } from 'react';
|
2025-10-30 11:14:43 +08:00
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2025-11-07 22:14:20 +08:00
|
|
|
|
import { Card, Button, Empty, Modal, message, Spin, Row, Col, Statistic, Space, Tag, Progress, Typography, Tooltip, Badge, Alert, Upload, Checkbox, Divider, Switch, Dropdown } from 'antd';
|
|
|
|
|
|
import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined, ApiOutlined, MoreOutlined } from '@ant-design/icons';
|
2025-11-04 14:38:59 +08:00
|
|
|
|
import { projectApi } from '../services/api';
|
2025-10-30 11:14:43 +08:00
|
|
|
|
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();
|
2025-10-30 22:01:10 +08:00
|
|
|
|
const [showApiTip, setShowApiTip] = useState(true);
|
2025-11-04 14:38:59 +08:00
|
|
|
|
const [importModalVisible, setImportModalVisible] = useState(false);
|
|
|
|
|
|
const [exportModalVisible, setExportModalVisible] = useState(false);
|
|
|
|
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
|
|
|
|
const [validationResult, setValidationResult] = useState<any>(null);
|
|
|
|
|
|
const [importing, setImporting] = useState(false);
|
|
|
|
|
|
const [validating, setValidating] = useState(false);
|
|
|
|
|
|
const [exporting, setExporting] = useState(false);
|
|
|
|
|
|
const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);
|
|
|
|
|
|
const [exportOptions, setExportOptions] = useState({
|
|
|
|
|
|
includeWritingStyles: true,
|
|
|
|
|
|
includeGenerationHistory: true,
|
|
|
|
|
|
});
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
|
|
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<string, { color: string; text: string; icon: ReactNode }> = {
|
|
|
|
|
|
planning: { color: 'blue', text: '规划中', icon: <CalendarOutlined /> },
|
|
|
|
|
|
writing: { color: 'green', text: '创作中', icon: <EditOutlined /> },
|
|
|
|
|
|
revising: { color: 'orange', text: '修改中', icon: <FileTextOutlined /> },
|
|
|
|
|
|
completed: { color: 'purple', text: '已完成', icon: <TrophyOutlined /> },
|
|
|
|
|
|
};
|
|
|
|
|
|
const config = statusConfig[status] || statusConfig.planning;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Tag color={config.color} icon={config.icon}>
|
|
|
|
|
|
{config.text}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2025-11-04 14:38:59 +08:00
|
|
|
|
// 处理文件选择
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-30 11:14:43 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
minHeight: '100vh',
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
padding: window.innerWidth <= 768 ? '20px 16px' : '40px 24px'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
maxWidth: 1400,
|
|
|
|
|
|
margin: '0 auto',
|
|
|
|
|
|
marginBottom: window.innerWidth <= 768 ? 20 : 40
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
variant="borderless"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'rgba(255, 255, 255, 0.95)',
|
|
|
|
|
|
borderRadius: window.innerWidth <= 768 ? 12 : 16,
|
|
|
|
|
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Row align="middle" justify="space-between" gutter={[16, 16]}>
|
|
|
|
|
|
<Col xs={24} sm={12} md={10}>
|
|
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
|
<Title level={window.innerWidth <= 768 ? 3 : 2} style={{ margin: 0 }}>
|
|
|
|
|
|
<FireOutlined style={{ color: '#ff4d4f', marginRight: 8 }} />
|
|
|
|
|
|
我的创作空间
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: window.innerWidth <= 768 ? 12 : 14 }}>
|
|
|
|
|
|
开启你的小说创作之旅
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Col>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Col xs={24} sm={12} md={14}>
|
|
|
|
|
|
{window.innerWidth <= 768 ? (
|
|
|
|
|
|
// 移动端:按钮分两行显示
|
|
|
|
|
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
|
|
|
|
<Space size={8} style={{ width: '100%' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
icon={<RocketOutlined />}
|
|
|
|
|
|
onClick={() => navigate('/wizard')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.4)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
向导创建
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
icon={<SettingOutlined />}
|
|
|
|
|
|
onClick={() => navigate('/settings')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
borderColor: '#d9d9d9',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
API设置
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<UserMenu />
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
<Space size={8} style={{ width: '100%' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
icon={<DownloadOutlined />}
|
|
|
|
|
|
onClick={handleOpenExportModal}
|
|
|
|
|
|
disabled={exportableProjects.length === 0}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
borderColor: '#1890ff',
|
|
|
|
|
|
color: '#1890ff',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.2)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
导出
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
icon={<UploadOutlined />}
|
|
|
|
|
|
onClick={() => setImportModalVisible(true)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
borderColor: '#52c41a',
|
|
|
|
|
|
color: '#52c41a',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(82, 196, 26, 0.2)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
导入
|
|
|
|
|
|
</Button>
|
2025-11-07 22:14:20 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
icon={<ApiOutlined />}
|
|
|
|
|
|
onClick={() => navigate('/mcp-plugins')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
borderColor: '#722ed1',
|
|
|
|
|
|
color: '#722ed1',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(114, 46, 209, 0.2)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
MCP
|
|
|
|
|
|
</Button>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
) : (
|
2025-11-07 22:14:20 +08:00
|
|
|
|
// PC端:优化后的布局 - 主要按钮 + 下拉菜单
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Space size={12} style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<RocketOutlined />}
|
|
|
|
|
|
onClick={() => navigate('/wizard')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.4)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
向导创建
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<SettingOutlined />}
|
|
|
|
|
|
onClick={() => navigate('/settings')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
borderColor: '#d9d9d9',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
|
|
|
|
|
transition: 'all 0.3s ease'
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseEnter={(e) => {
|
|
|
|
|
|
e.currentTarget.style.borderColor = '#667eea';
|
|
|
|
|
|
e.currentTarget.style.color = '#667eea';
|
|
|
|
|
|
e.currentTarget.style.boxShadow = '0 2px 12px rgba(102, 126, 234, 0.3)';
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseLeave={(e) => {
|
|
|
|
|
|
e.currentTarget.style.borderColor = '#d9d9d9';
|
|
|
|
|
|
e.currentTarget.style.color = 'rgba(0, 0, 0, 0.88)';
|
|
|
|
|
|
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
API设置
|
|
|
|
|
|
</Button>
|
2025-11-07 22:14:20 +08:00
|
|
|
|
<Dropdown
|
|
|
|
|
|
menu={{
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'export',
|
|
|
|
|
|
label: '导出项目',
|
|
|
|
|
|
icon: <DownloadOutlined />,
|
|
|
|
|
|
onClick: handleOpenExportModal,
|
|
|
|
|
|
disabled: exportableProjects.length === 0
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'import',
|
|
|
|
|
|
label: '导入项目',
|
|
|
|
|
|
icon: <UploadOutlined />,
|
|
|
|
|
|
onClick: () => setImportModalVisible(true)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'divider'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'mcp',
|
|
|
|
|
|
label: 'MCP插件',
|
|
|
|
|
|
icon: <ApiOutlined />,
|
|
|
|
|
|
onClick: () => navigate('/mcp-plugins')
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}}
|
|
|
|
|
|
placement="bottomRight"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<MoreOutlined />}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
borderColor: '#d9d9d9',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
|
|
|
|
|
transition: 'all 0.3s ease'
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseEnter={(e) => {
|
|
|
|
|
|
e.currentTarget.style.borderColor = '#1890ff';
|
|
|
|
|
|
e.currentTarget.style.color = '#1890ff';
|
|
|
|
|
|
e.currentTarget.style.boxShadow = '0 2px 12px rgba(24, 144, 255, 0.3)';
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseLeave={(e) => {
|
|
|
|
|
|
e.currentTarget.style.borderColor = '#d9d9d9';
|
|
|
|
|
|
e.currentTarget.style.color = 'rgba(0, 0, 0, 0.88)';
|
|
|
|
|
|
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
更多
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Dropdown>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<UserMenu />
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
2025-10-30 11:14:43 +08:00
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
2025-10-30 22:01:10 +08:00
|
|
|
|
{showApiTip && projects.length === 0 && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message={
|
|
|
|
|
|
<Space align="center" style={{ width: '100%' }}>
|
|
|
|
|
|
<InfoCircleOutlined style={{ fontSize: 16, color: '#1890ff' }} />
|
|
|
|
|
|
<Text strong style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}>
|
|
|
|
|
|
首次使用提示
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
description={
|
|
|
|
|
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
|
|
|
|
<Text style={{ fontSize: window.innerWidth <= 768 ? 12 : 13 }}>
|
|
|
|
|
|
在开始创作之前,请先配置您的AI接口。系统支持OpenAI和Anthropic两种接口。
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Space size={8}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<SettingOutlined />}
|
|
|
|
|
|
onClick={() => navigate('/settings')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
border: 'none'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
立即配置
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => setShowApiTip(false)}
|
|
|
|
|
|
style={{ borderRadius: 6 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
暂不提醒
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon={false}
|
|
|
|
|
|
closable
|
|
|
|
|
|
closeIcon={<CloseOutlined style={{ fontSize: 12 }} />}
|
|
|
|
|
|
onClose={() => setShowApiTip(false)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginTop: window.innerWidth <= 768 ? 16 : 24,
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)',
|
|
|
|
|
|
border: '1px solid #91d5ff'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-30 11:14:43 +08:00
|
|
|
|
{projects.length > 0 && (
|
|
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: window.innerWidth <= 768 ? 16 : 24 }}>
|
|
|
|
|
|
<Col xs={24} sm={8}>
|
|
|
|
|
|
<Card variant="borderless" style={{ background: '#f0f5ff', borderRadius: 12 }}>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title={<span style={{ fontSize: window.innerWidth <= 768 ? 12 : 14, color: '#595959' }}>总项目数</span>}
|
|
|
|
|
|
value={projects.length}
|
|
|
|
|
|
prefix={<BookOutlined style={{ color: '#1890ff' }} />}
|
|
|
|
|
|
suffix="个"
|
|
|
|
|
|
valueStyle={{ color: '#1890ff', fontSize: window.innerWidth <= 768 ? 20 : 28, fontWeight: 'bold' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={8}>
|
|
|
|
|
|
<Card variant="borderless" style={{ background: '#f6ffed', borderRadius: 12 }}>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title={<span style={{ fontSize: window.innerWidth <= 768 ? 12 : 14, color: '#595959' }}>创作中</span>}
|
|
|
|
|
|
value={activeProjects}
|
|
|
|
|
|
prefix={<EditOutlined style={{ color: '#52c41a' }} />}
|
|
|
|
|
|
suffix="个"
|
|
|
|
|
|
valueStyle={{ color: '#52c41a', fontSize: window.innerWidth <= 768 ? 20 : 28, fontWeight: 'bold' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} sm={8}>
|
|
|
|
|
|
<Card variant="borderless" style={{ background: '#fff7e6', borderRadius: 12 }}>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title={<span style={{ fontSize: window.innerWidth <= 768 ? 12 : 14, color: '#595959' }}>总字数</span>}
|
|
|
|
|
|
value={totalWords}
|
|
|
|
|
|
prefix={<FileTextOutlined style={{ color: '#faad14' }} />}
|
|
|
|
|
|
suffix="字"
|
|
|
|
|
|
valueStyle={{ color: '#faad14', fontSize: window.innerWidth <= 768 ? 20 : 28, fontWeight: 'bold' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
|
|
|
|
|
|
<Spin spinning={loading}>
|
|
|
|
|
|
{!Array.isArray(projects) || projects.length === 0 ? (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
variant="borderless"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'rgba(255, 255, 255, 0.95)',
|
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Empty
|
|
|
|
|
|
description={
|
|
|
|
|
|
<Space direction="vertical" size={16}>
|
|
|
|
|
|
<Text style={{ fontSize: 16, color: '#8c8c8c' }}>
|
|
|
|
|
|
还没有项目,开始创建你的第一个小说项目吧!
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon={<RocketOutlined />}
|
|
|
|
|
|
onClick={() => navigate('/wizard')}
|
|
|
|
|
|
>
|
|
|
|
|
|
向导创建
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{ padding: '80px 0' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Row gutter={[16, 16]}>
|
|
|
|
|
|
{projects.map((project) => {
|
|
|
|
|
|
const progress = getProgress(project.current_words, project.target_words || 0);
|
|
|
|
|
|
const isWizardComplete = project.wizard_status === 'completed';
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Col {...gridConfig} key={project.id}>
|
|
|
|
|
|
<Badge.Ribbon
|
|
|
|
|
|
text={isWizardComplete ? getStatusTag(project.status) : <Tag color="orange" icon={<RocketOutlined />}>创建中</Tag>}
|
|
|
|
|
|
color="transparent"
|
|
|
|
|
|
style={{ top: 12, right: 12 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
hoverable
|
|
|
|
|
|
variant="borderless"
|
|
|
|
|
|
onClick={() => handleEnterProject(project.id)}
|
|
|
|
|
|
style={cardStyles.project}
|
|
|
|
|
|
styles={{ body: { padding: 0, overflow: 'hidden' } }}
|
|
|
|
|
|
{...cardHoverHandlers}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
padding: window.innerWidth <= 768 ? '16px' : '24px',
|
|
|
|
|
|
position: 'relative'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: window.innerWidth <= 768 ? 8 : 12 }}>
|
|
|
|
|
|
<BookOutlined style={{ fontSize: window.innerWidth <= 768 ? 20 : 28, color: '#fff' }} />
|
|
|
|
|
|
<Title level={window.innerWidth <= 768 ? 5 : 4} style={{ margin: 0, color: '#fff', flex: 1 }} ellipsis>
|
|
|
|
|
|
{project.title}
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{project.genre && (
|
|
|
|
|
|
<Tag color="rgba(255,255,255,0.3)" style={{ color: '#fff', border: 'none' }}>
|
|
|
|
|
|
{project.genre}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ padding: window.innerWidth <= 768 ? '16px' : '20px' }}>
|
|
|
|
|
|
<Paragraph
|
|
|
|
|
|
ellipsis={{ rows: 2 }}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: 'rgba(0,0,0,0.65)',
|
|
|
|
|
|
minHeight: 44,
|
|
|
|
|
|
marginBottom: 16
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{project.description || '暂无描述'}
|
|
|
|
|
|
</Paragraph>
|
|
|
|
|
|
|
|
|
|
|
|
{isWizardComplete ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{project.target_words && project.target_words > 0 && (
|
|
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>完成进度</Text>
|
|
|
|
|
|
<Text strong style={{ fontSize: 12 }}>{progress}%</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Progress
|
|
|
|
|
|
percent={progress}
|
|
|
|
|
|
strokeColor={getProgressColor(progress)}
|
|
|
|
|
|
showInfo={false}
|
|
|
|
|
|
size={{ height: 8 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={12}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
padding: '12px 0',
|
|
|
|
|
|
background: '#f5f5f5',
|
|
|
|
|
|
borderRadius: 8
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#1890ff' }}>
|
|
|
|
|
|
{(project.current_words / 1000).toFixed(1)}K
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>已写字数</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
padding: '12px 0',
|
|
|
|
|
|
background: '#f5f5f5',
|
|
|
|
|
|
borderRadius: 8
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#52c41a' }}>
|
|
|
|
|
|
{project.target_words ? (project.target_words / 1000).toFixed(0) + 'K' : '--'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>目标字数</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
padding: '24px 0',
|
|
|
|
|
|
background: '#f5f5f5',
|
|
|
|
|
|
borderRadius: 8
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<RocketOutlined style={{ fontSize: 32, color: '#faad14', marginBottom: 12 }} />
|
|
|
|
|
|
<div style={{ color: '#faad14', fontWeight: 'bold', marginBottom: 4 }}>
|
|
|
|
|
|
项目创建中
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
|
点击继续创建向导
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
marginTop: 16,
|
|
|
|
|
|
paddingTop: 16,
|
|
|
|
|
|
borderTop: '1px solid #f0f0f0',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
|
<CalendarOutlined style={{ marginRight: 4 }} />
|
|
|
|
|
|
{formatDate(project.updated_at)}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Space size={8}>
|
|
|
|
|
|
<Tooltip title="删除">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
danger
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
handleDelete(project.id);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Badge.Ribbon>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Spin>
|
|
|
|
|
|
</div>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 导入项目对话框 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="导入项目"
|
|
|
|
|
|
open={importModalVisible}
|
|
|
|
|
|
onOk={handleImport}
|
|
|
|
|
|
onCancel={handleCloseImportModal}
|
|
|
|
|
|
confirmLoading={importing}
|
|
|
|
|
|
okText="导入"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
width={window.innerWidth <= 768 ? '90%' : 500}
|
|
|
|
|
|
centered
|
|
|
|
|
|
okButtonProps={{ disabled: !validationResult?.valid }}
|
|
|
|
|
|
styles={{
|
|
|
|
|
|
body: {
|
|
|
|
|
|
maxHeight: window.innerWidth <= 768 ? '60vh' : 'auto',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
padding: window.innerWidth <= 768 ? '16px' : '24px'
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p style={{ marginBottom: '12px', color: '#666', fontSize: window.innerWidth <= 768 ? 13 : 14 }}>
|
|
|
|
|
|
选择之前导出的 JSON 格式项目文件
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<Upload
|
|
|
|
|
|
accept=".json"
|
|
|
|
|
|
beforeUpload={handleFileSelect}
|
|
|
|
|
|
maxCount={1}
|
|
|
|
|
|
onRemove={() => {
|
|
|
|
|
|
setSelectedFile(null);
|
|
|
|
|
|
setValidationResult(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
fileList={selectedFile ? [{ uid: '-1', name: selectedFile.name, status: 'done' }] as any : []}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button icon={<UploadOutlined />} block>选择文件</Button>
|
|
|
|
|
|
</Upload>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{validating && (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
|
|
|
|
|
<Spin tip="验证文件中..." />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{validationResult && (
|
|
|
|
|
|
<Card size="small" style={{ background: validationResult.valid ? '#f6ffed' : '#fff2f0' }}>
|
|
|
|
|
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text strong style={{
|
|
|
|
|
|
color: validationResult.valid ? '#52c41a' : '#ff4d4f',
|
|
|
|
|
|
fontSize: window.innerWidth <= 768 ? 13 : 14
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{validationResult.valid ? '✓ 文件验证通过' : '✗ 文件验证失败'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{validationResult.project_name && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: window.innerWidth <= 768 ? 12 : 14 }}>项目名称:</Text>
|
|
|
|
|
|
<Text strong style={{ fontSize: window.innerWidth <= 768 ? 12 : 14 }}>{validationResult.project_name}</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{validationResult.statistics && Object.keys(validationResult.statistics).length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: window.innerWidth <= 768 ? 12 : 14 }}>数据统计:</Text>
|
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
|
|
|
|
|
<Row gutter={[8, 8]}>
|
|
|
|
|
|
{validationResult.statistics.chapters > 0 && (
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Tag color="blue">章节: {validationResult.statistics.chapters}</Tag>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{validationResult.statistics.characters > 0 && (
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Tag color="green">角色: {validationResult.statistics.characters}</Tag>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{validationResult.statistics.outlines > 0 && (
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Tag color="purple">大纲: {validationResult.statistics.outlines}</Tag>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{validationResult.statistics.relationships > 0 && (
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Tag color="orange">关系: {validationResult.statistics.relationships}</Tag>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{validationResult.errors && validationResult.errors.length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text type="danger" strong style={{ fontSize: window.innerWidth <= 768 ? 12 : 14 }}>错误:</Text>
|
|
|
|
|
|
<ul style={{
|
|
|
|
|
|
margin: '4px 0 0 0',
|
|
|
|
|
|
paddingLeft: '20px',
|
|
|
|
|
|
color: '#ff4d4f',
|
|
|
|
|
|
fontSize: window.innerWidth <= 768 ? 12 : 13
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{validationResult.errors.map((error: string, index: number) => (
|
|
|
|
|
|
<li key={index}>{error}</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{validationResult.warnings && validationResult.warnings.length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text type="warning" strong style={{ fontSize: window.innerWidth <= 768 ? 12 : 14 }}>警告:</Text>
|
|
|
|
|
|
<ul style={{
|
|
|
|
|
|
margin: '4px 0 0 0',
|
|
|
|
|
|
paddingLeft: '20px',
|
|
|
|
|
|
color: '#faad14',
|
|
|
|
|
|
fontSize: window.innerWidth <= 768 ? 12 : 13
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{validationResult.warnings.map((warning: string, index: number) => (
|
|
|
|
|
|
<li key={index}>{warning}</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 导出项目对话框 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="导出项目"
|
|
|
|
|
|
open={exportModalVisible}
|
|
|
|
|
|
onOk={handleExport}
|
|
|
|
|
|
onCancel={handleCloseExportModal}
|
|
|
|
|
|
confirmLoading={exporting}
|
|
|
|
|
|
okText={selectedProjectIds.length > 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'
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
|
|
|
|
|
{/* 导出选项 */}
|
|
|
|
|
|
<Card
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style={{ background: '#f5f5f5' }}
|
|
|
|
|
|
styles={{ body: { padding: window.innerWidth <= 768 ? 12 : 16 } }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
|
|
|
|
|
<Text strong style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}>导出选项</Text>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
size={window.innerWidth <= 768 ? 'small' : 'default'}
|
|
|
|
|
|
checked={exportOptions.includeWritingStyles}
|
|
|
|
|
|
onChange={(checked) => 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'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Text style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}>包含写作风格</Text>
|
|
|
|
|
|
<Tooltip title="导出项目关联的写作风格数据">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: '#999', fontSize: window.innerWidth <= 768 ? 12 : 14 }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
size={window.innerWidth <= 768 ? 'small' : 'default'}
|
|
|
|
|
|
checked={exportOptions.includeGenerationHistory}
|
|
|
|
|
|
onChange={(checked) => 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'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Text style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}>包含生成历史</Text>
|
|
|
|
|
|
<Tooltip title="导出AI生成的历史记录(最多100条)">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: '#999', fontSize: window.innerWidth <= 768 ? 12 : 14 }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Divider style={{ margin: '8px 0' }} />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 项目列表 */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: window.innerWidth <= 768 ? 'wrap' : 'nowrap', gap: 8 }}>
|
|
|
|
|
|
<Text strong style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}>
|
|
|
|
|
|
选择要导出的项目 {exportableProjects.length > 0 && <Text type="secondary" style={{ fontSize: window.innerWidth <= 768 ? 12 : 14 }}>({exportableProjects.length}个可导出)</Text>}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={selectedProjectIds.length === exportableProjects.length && exportableProjects.length > 0}
|
|
|
|
|
|
indeterminate={selectedProjectIds.length > 0 && selectedProjectIds.length < exportableProjects.length}
|
|
|
|
|
|
onChange={handleToggleAll}
|
|
|
|
|
|
style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
全选
|
|
|
|
|
|
</Checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ maxHeight: window.innerWidth <= 768 ? 300 : 400, overflowY: 'auto' }}>
|
|
|
|
|
|
{exportableProjects.length === 0 ? (
|
|
|
|
|
|
<Empty
|
|
|
|
|
|
description="暂无可导出的项目"
|
|
|
|
|
|
style={{ padding: '40px 0' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
|
|
|
|
|
{exportableProjects.map((project) => (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
key={project.id}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
hoverable
|
|
|
|
|
|
style={{
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
border: selectedProjectIds.includes(project.id) ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
|
|
|
|
|
background: selectedProjectIds.includes(project.id) ? '#e6f7ff' : '#fff'
|
|
|
|
|
|
}}
|
|
|
|
|
|
onClick={() => handleToggleProject(project.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={selectedProjectIds.includes(project.id)}
|
|
|
|
|
|
onChange={() => handleToggleProject(project.id)}
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<BookOutlined style={{ fontSize: 20, color: '#1890ff' }} />
|
|
|
|
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4, flexWrap: 'wrap' }}>
|
|
|
|
|
|
<Text strong style={{ fontSize: window.innerWidth <= 768 ? 13 : 14 }}>{project.title}</Text>
|
|
|
|
|
|
{project.genre && (
|
|
|
|
|
|
<Tag color="blue" style={{ margin: 0, fontSize: window.innerWidth <= 768 ? 11 : 12 }}>{project.genre}</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{getStatusTag(project.status)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: window.innerWidth <= 768 ? 11 : 12 }}>
|
|
|
|
|
|
{project.current_words || 0} 字
|
|
|
|
|
|
{project.description && ` · ${project.description.substring(0, window.innerWidth <= 768 ? 30 : 50)}${project.description.length > (window.innerWidth <= 768 ? 30 : 50) ? '...' : ''}`}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{window.innerWidth > 768 && (
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
|
{formatDate(project.updated_at)}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{selectedProjectIds.length > 0 && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message={`已选择 ${selectedProjectIds.length} 个项目`}
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ marginTop: 8 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Modal>
|
2025-10-30 11:14:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|