diff --git a/frontend/src/pages/MCPPlugins.tsx b/frontend/src/pages/MCPPlugins.tsx index 4d17914..f977deb 100644 --- a/frontend/src/pages/MCPPlugins.tsx +++ b/frontend/src/pages/MCPPlugins.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; import { Card, Button, @@ -27,7 +26,6 @@ import { ThunderboltOutlined, InfoCircleOutlined, ToolOutlined, - ArrowLeftOutlined, ApiOutlined, QuestionCircleOutlined, WarningOutlined, @@ -39,7 +37,6 @@ const { Paragraph, Text, Title } = Typography; const { TextArea } = Input; export default function MCPPluginsPage() { - const navigate = useNavigate(); const isMobile = window.innerWidth <= 768; const [form] = Form.useForm(); const [modal, contextHolder] = Modal.useModal(); @@ -592,11 +589,12 @@ export default function MCPPluginsPage() { <> {contextHolder}
- - - - - - - {/* 第二行:功能按钮 */} - - - - - - , - 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" - > - - - - - )} - - + {/* Logo 区域 - 保持 Primary Color 风格 */} +
+
+
+ +
+ + MuMuAINovel + +
+
+ {/* 侧边栏菜单 - 使用 Menu 组件以保持风格一致 */} +
+ {/* 模拟 Menu 样式 */} +
+
setActiveView('projects')} + style={{ + padding: '10px 16px', + fontSize: 14, + cursor: 'pointer', + borderRadius: 4, + color: activeView === 'projects' ? 'var(--color-primary)' : 'rgba(0,0,0,0.85)', + background: activeView === 'projects' ? '#e6f7ff' : 'transparent', + marginBottom: 4, + fontWeight: 500, + display: 'flex', + alignItems: 'center', + gap: 10, + transition: 'all 0.3s', + borderRight: activeView === 'projects' ? '3px solid var(--color-primary)' : '3px solid transparent' + }} + onMouseEnter={e => activeView !== 'projects' && (e.currentTarget.style.background = 'rgba(0,0,0,0.04)')} + onMouseLeave={e => activeView !== 'projects' && (e.currentTarget.style.background = 'transparent')} + > + + 我的书架 +
+ +
创作工具
+
setActiveView('prompts')} + style={{ + padding: '10px 16px', + fontSize: 14, + cursor: 'pointer', + borderRadius: 4, + color: activeView === 'prompts' ? 'var(--color-primary)' : 'rgba(0,0,0,0.85)', + background: activeView === 'prompts' ? '#e6f7ff' : 'transparent', + fontWeight: 500, + display: 'flex', + alignItems: 'center', + gap: 10, + transition: 'all 0.3s', + marginBottom: 4, + borderRight: activeView === 'prompts' ? '3px solid var(--color-primary)' : '3px solid transparent' + }} + onMouseEnter={e => activeView !== 'prompts' && (e.currentTarget.style.background = 'rgba(0,0,0,0.04)')} + onMouseLeave={e => activeView !== 'prompts' && (e.currentTarget.style.background = 'transparent')} + > + + 提示词管理 +
+
setActiveView('mcp')} + style={{ + padding: '10px 16px', + fontSize: 14, + cursor: 'pointer', + borderRadius: 4, + color: activeView === 'mcp' ? 'var(--color-primary)' : 'rgba(0,0,0,0.85)', + background: activeView === 'mcp' ? '#e6f7ff' : 'transparent', + fontWeight: 500, + display: 'flex', + alignItems: 'center', + gap: 10, + transition: 'all 0.3s', + marginBottom: 4, + borderRight: activeView === 'mcp' ? '3px solid var(--color-primary)' : '3px solid transparent' + }} + onMouseEnter={e => activeView !== 'mcp' && (e.currentTarget.style.background = 'rgba(0,0,0,0.04)')} + onMouseLeave={e => activeView !== 'mcp' && (e.currentTarget.style.background = 'transparent')} + > + + MCP 插件 +
+ +
系统设置
+
setActiveView('settings')} + style={{ + padding: '10px 16px', + fontSize: 14, + cursor: 'pointer', + borderRadius: 4, + color: activeView === 'settings' ? 'var(--color-primary)' : 'rgba(0,0,0,0.85)', + background: activeView === 'settings' ? '#e6f7ff' : 'transparent', + fontWeight: 500, + display: 'flex', + alignItems: 'center', + gap: 10, + transition: 'all 0.3s', + marginBottom: 4, + borderRight: activeView === 'settings' ? '3px solid var(--color-primary)' : '3px solid transparent' + }} + onMouseEnter={e => activeView !== 'settings' && (e.currentTarget.style.background = 'rgba(0,0,0,0.04)')} + onMouseLeave={e => activeView !== 'settings' && (e.currentTarget.style.background = 'transparent')} + > + + API 设置 +
+
+
+ + {/* 底部用户信息 */} +
+ +
+
+ )} + + {/* 主内容区域容器 */} +
+ + {/* 移动端顶部导航栏 */} + {isMobile && ( +
+
+
+ + + {activeView === 'projects' ? '我的书架' : + activeView === 'prompts' ? '提示词模板' : + activeView === 'mcp' ? 'MCP 插件' : 'API 设置'} + +
+ )} + + {/* 移动端侧边栏 Drawer */} + {isMobile && ( + +
+ +
+ MuMuAINovel +
+ } + closeIcon={null} + extra={ + + + +
+ + + {/* 底部用户信息 */} +
+ +
+ + )} + + {/* 桌面端顶部标题栏 */} + {!isMobile && ( +
+

+ {activeView === 'projects' ? '我的书架' : + activeView === 'prompts' ? '提示词模板' : + activeView === 'mcp' ? 'MCP 插件' : 'API 设置'} +

+ + {activeView === 'projects' && ( +
+ {/* 导入导出按钮 */} + + + + + + {/* 统计数据:创作中 已完结 总字数 */} + {projects.length > 0 && ( +
+ {[ + { label: '创作中', value: activeProjects, unit: '本' }, + { label: '已完结', value: completedProjects, unit: '本' }, + { label: '总字数', value: totalWords, unit: '字' }, + ].map((item, index) => ( +
{ + e.currentTarget.style.transform = 'translateY(-3px) scale(1.02)'; + e.currentTarget.style.boxShadow = 'inset 0 0 20px rgba(255, 255, 255, 0.25), 0 8px 16px rgba(0, 0, 0, 0.15)'; + e.currentTarget.style.border = '1px solid rgba(255, 255, 255, 0.1)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0) scale(1)'; + e.currentTarget.style.boxShadow = 'inset 0 0 15px rgba(255, 255, 255, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1)'; + }} + > + + {item.label} + + + {item.value > 10000 ? (item.value / 10000).toFixed(1) + 'w' : item.value} + {item.unit && {item.unit}} + +
+ ))} +
+ )} +
+ )} +
+ )} + + {/* 内容显示区 */} +
+ {activeView === 'settings' && } + {activeView === 'mcp' && } + {activeView === 'prompts' && } + + {activeView === 'projects' && ( +
+ {showApiTip && projects.length === 0 && ( - - - 首次使用提示 - - - } + message="欢迎使用 MuMuAINovel" description={ - - - 在开始创作之前,请先配置您的AI接口。系统支持OpenAI和Anthropic两种接口。 - - - - - - +
+ + 在开始创作之前,请先配置您的AI接口(支持 OpenAI / Anthropic)。 + + +
} type="info" - showIcon={false} + showIcon 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' + marginBottom: isMobile ? 16 : 24, + borderRadius: 12 }} /> )} - {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 }} - > + {Array.isArray(projects) && projects.map((project) => { + const progress = getProgress(project.current_words, project.target_words || 0); + const isWizardIncomplete = project.wizard_status === 'incomplete'; + // 解析标签(假设存储在 genre 字段,用逗号或顿号分隔) + const tags = project.genre ? project.genre.split(/[,、,]/).map((t: string) => t.trim()).filter((t: string) => t) : []; + + return ( +
handleEnterProject(project)} style={cardStyles.project} - styles={{ body: { padding: 0, overflow: 'hidden' } }} + styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column' } }} {...cardHoverHandlers} + onClick={() => handleEnterProject(project)} > - {/* 项目卡片头部 - 添加装饰元素 */} + {/* 卡片头部 - 参考图片样式 */}
- {/* 装饰性圆圈 */} -
- -
-
+ {/* 标题行:图标 + 标题 */} +
+ + +
+ {project.title} +
+
+
+ {/* 标签行 - 单行不换行 */} +
- -
- - {project.title} - + gap: isMobile ? 4 : 6, + overflow: 'hidden', + flexWrap: 'nowrap' + }}> + {tags.length > 0 ? tags.slice(0, 3).map((tag: string, idx: number) => ( + + {tag} + + )) : ( + + 未分类 + + )}
- {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)} - - -
+ + {/* 右上角状态标签 - 带文字和图标 */} +
+ {isWizardIncomplete ? ( + } + style={{ + margin: 0, + borderRadius: 4, + fontSize: isMobile ? 10 : 12, + padding: isMobile ? '0 6px' : '2px 10px', + fontWeight: 500 + }} + > + 生成中 + + ) : ( + getStatusTag(getDisplayStatus(project.status, progress)) + )}
+ + {/* 描述区域 */} +
+ + {project.description || '暂无描述...'} + +
+ + {/* 进度条区域 */} +
+
+ + 完成进度 + + + {progress}% + +
+ +
+ + {/* 字数统计区域 */} +
+
+
+ {project.current_words >= 1000 + ? (project.current_words / 1000).toFixed(1) + 'K' + : (project.current_words || 0)} +
+
+ 已写字数 +
+
+
+
+
+ {(project.target_words || 0) >= 1000 + ? ((project.target_words || 0) / 1000).toFixed(1) + 'K' + : (project.target_words || 0)} +
+
+ 目标字数 +
+
+
+ + {/* 卡片底部 - 时间和操作 */} +
+ + {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.warnings && validationResult.warnings.length > 0 && ( -
- 警告: - -
- )} - - - )} - - - - {/* 导出项目对话框 */} - 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' - } - }} - > - - {/* 导出选项 */} - + +
+

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

+ { + setSelectedFile(null); + setValidationResult(null); + }} + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fileList={selectedFile ? [{ uid: '-1', name: selectedFile.name, status: 'done' }] as any : []} > - - 导出选项 -
- 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' - }} - /> - 包含生成历史 - + + +
+ + {validating && ( +
+ +
+ )} + + {validationResult && ( + + +
+ + {validationResult.valid ? '✓ 文件验证通过' : '✗ 文件验证失败'} +
+ {validationResult.project_name && ( +
+ 项目名称: + {validationResult.project_name} +
+ )} + {validationResult.statistics && ( +
+ + {validationResult.statistics.chapters > 0 && 章节: {validationResult.statistics.chapters}} + {validationResult.statistics.characters > 0 && 角色: {validationResult.statistics.characters}} + +
+ )} + {validationResult.errors?.length > 0 && ( +
+ 错误: +
    + {validationResult.errors.map((e: string, i: number) =>
  • {e}
  • )} +
+
+ )} +
+
+ )} +
+ + + {/* 导出项目对话框 */} + 0 ? `导出 (${selectedProjectIds.length})` : '导出'} + cancelText="取消" + width={isMobile ? '90%' : 700} + centered + okButtonProps={{ disabled: selectedProjectIds.length === 0 }} + > + + + + 导出选项 + + setExportOptions(prev => ({...prev, includeWritingStyles: e.target.checked}))}>包含写作风格 + setExportOptions(prev => ({...prev, includeGenerationHistory: e.target.checked}))}>包含生成历史 + - - - {/* 项目列表 */}
-
- - 选择要导出的项目 {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) => ( - + 选择项目 ({exportableProjects.length}) + 0} + indeterminate={selectedProjectIds.length > 0 && selectedProjectIds.length < exportableProjects.length} + onChange={handleToggleAll} + > + 全选 + +
+
+ + {exportableProjects.map(p => ( +
handleToggleProject(project.id)} + onClick={() => handleToggleProject(p.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)} - - )} + +
+
{p.title}
+
{p.current_words || 0} 字 · {getStatusTag(getDisplayStatus(p.status, getProgress(p.current_words || 0, p.target_words || 0)))}
- +
))} - )} -
+
+
+
- {selectedProjectIds.length > 0 && ( - - )} - - - - -
); } \ No newline at end of file diff --git a/frontend/src/pages/PromptTemplates.tsx b/frontend/src/pages/PromptTemplates.tsx index b5e0fdc..1f567ad 100644 --- a/frontend/src/pages/PromptTemplates.tsx +++ b/frontend/src/pages/PromptTemplates.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; import { Card, Tabs, @@ -25,7 +24,6 @@ import { UploadOutlined, CheckCircleOutlined, FileSearchOutlined, - ArrowLeftOutlined, InfoCircleOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -56,7 +54,6 @@ interface CategoryGroup { } export default function PromptTemplates() { - const navigate = useNavigate(); const [modal, contextHolder] = Modal.useModal(); const [categories, setCategories] = useState([]); const [selectedCategory, setSelectedCategory] = useState('0'); @@ -72,8 +69,9 @@ export default function PromptTemplates() { setLoading(true); const response = await axios.get('/api/prompt-templates/categories'); setCategories(response.data); - } catch (error: any) { - message.error(error.response?.data?.detail || '加载失败'); + } catch (error: unknown) { + const err = error as { response?: { data?: { detail?: string } } }; + message.error(err.response?.data?.detail || '加载失败'); } finally { setLoading(false); } @@ -116,8 +114,9 @@ export default function PromptTemplates() { message.success('保存成功'); setEditorVisible(false); loadTemplates(); - } catch (error: any) { - message.error(error.response?.data?.detail || '保存失败'); + } catch (error: unknown) { + const err = error as { response?: { data?: { detail?: string } } }; + message.error(err.response?.data?.detail || '保存失败'); } finally { setLoading(false); } @@ -137,8 +136,9 @@ export default function PromptTemplates() { await axios.post(`/api/prompt-templates/${templateKey}/reset`); message.success('已重置为系统默认'); loadTemplates(); - } catch (error: any) { - message.error(error.response?.data?.detail || '重置失败'); + } catch (error: unknown) { + const err = error as { response?: { data?: { detail?: string } } }; + message.error(err.response?.data?.detail || '重置失败'); } finally { setLoading(false); } @@ -153,8 +153,9 @@ export default function PromptTemplates() { is_active: checked }); loadTemplates(); - } catch (error: any) { - message.error(error.response?.data?.detail || '操作失败'); + } catch (error: unknown) { + const err = error as { response?: { data?: { detail?: string } } }; + message.error(err.response?.data?.detail || '操作失败'); } }; @@ -180,8 +181,9 @@ export default function PromptTemplates() { } else { message.success('导出成功'); } - } catch (error: any) { - message.error(error.response?.data?.detail || '导出失败'); + } catch (error: unknown) { + const err = error as { response?: { data?: { detail?: string } } }; + message.error(err.response?.data?.detail || '导出失败'); } }; @@ -219,7 +221,7 @@ export default function PromptTemplates() {

以下模板内容与系统默认不一致,已转为自定义:

    - {result.converted_templates.map((t: any) => ( + {result.converted_templates.map((t: { template_key: string; template_name: string }) => (
  • {t.template_name} ({t.template_key})
  • @@ -236,8 +238,9 @@ export default function PromptTemplates() { } loadTemplates(); - } catch (error: any) { - message.error(error.response?.data?.detail || '导入失败'); + } catch (error: unknown) { + const err = error as { response?: { data?: { detail?: string } } }; + message.error(err.response?.data?.detail || '导入失败'); } return false; // 阻止默认上传行为 }; @@ -248,11 +251,12 @@ export default function PromptTemplates() { <> {contextHolder}
    -